The Microsoft Graph API provides access to Microsoft 365 data (users, groups, mail, calendar, and more). This example covers OAuth 2.0 client credentials for tenant-wide read queries and OAuth 2.0 device code for delegated read/write on /me/… resources.
java -jar restjdbc.jar graph-oauth2.sql
tenant, clientId, and clientSecret in graph-oauth2.sqlconnect 'jdbc:rest:https://graph.microsoft.com/v1.0|spec=graph-spec.json,auth=oauth2,tenant=…,clientId=…,clientSecret=…,scope=https://graph.microsoft.com/.default'
The /me/messages endpoint requires a delegated token (signed-in user). Use the device code flow built into the driver:
java -jar restjdbc.jar graph-devicecode.sql
Mail.Readtenant and clientId in graph-devicecode.sqlconnect 'jdbc:rest:https://graph.microsoft.com/v1.0|spec=graph-spec.json,auth=oauth2,oauth2GrantType=device_code,tenant=…,clientId=…,scope=Mail.Read offline_access'
Alternatively, use a manual bearer token: graph-bearer.sql
Read and write the signed-in user's events (INSERT, UPDATE, DELETE):
java -jar restjdbc.jar graph-calendar.sql
Calendars.ReadWrite in the Azure apptenant and clientId in graph-calendar.sqlCalendars.ReadWrite offline_access User.Readconnect 'jdbc:rest:https://graph.microsoft.com/v1.0|spec=graph-spec.json,auth=oauth2,oauth2GrantType=device_code,tenant=…,clientId=…,scope=Calendars.ReadWrite offline_access User.Read'
Nested Graph fields (start, end, body, location) are mapped via jsonPath and defaultWrite in the spec — see entity calendar_events in graph-spec.json.
Read and write calendar events for any tenant user (/users/{userId}/events):
java -jar restjdbc.jar graph-user-calendar.sql
Calendars.ReadWrite and grant admin consenttenant, clientId, clientSecret, and USER-OBJECT-ID in graph-user-calendar.sqlhttps://graph.microsoft.com/.defaultPath parameter userId is supplied as a column or via FILTER userId='…' (placeholder {userId} in the spec). Example:
INSERT INTO user_calendar_events ("userId", subject, "start_dateTime", "end_dateTime")
VALUES ('USER-OBJECT-ID', 'Meeting', '2026-07-01T10:00:00', '2026-07-01T11:00:00');
UPDATE user_calendar_events SET subject = 'Meeting (updated)' FILTER userId='USER-OBJECT-ID' AND id=EVENT-ID;
jdbc:rest:https://graph.microsoft.com/v1.0
The API base URL is set in the JDBC URL.
| Part | Value |
|---|---|
| Driver prefix | jdbc:rest: |
| API base URL | https://graph.microsoft.com/v1.0 |
| SQL schema | public (default) |
Connection example (Java, OAuth2):
Properties props = new Properties();
props.setProperty("spec", "/path/to/graph-spec.json");
props.setProperty("auth", "oauth2");
props.setProperty("tenant", System.getenv("AZURE_TENANT_ID"));
props.setProperty("clientId", System.getenv("AZURE_CLIENT_ID"));
props.setProperty("clientSecret", System.getenv("AZURE_CLIENT_SECRET"));
props.setProperty("scope", "https://graph.microsoft.com/.default");
Connection conn = DriverManager.getConnection(
"jdbc:rest:https://graph.microsoft.com/v1.0", props);
| Property | Value | Required |
|---|---|---|
spec |
Path to spec | Yes |
auth |
oauth2 |
Yes |
tenant |
Azure AD tenant ID | Yes (or tokenUrl) |
clientId |
App registration client ID | Yes |
oauth2GrantType |
client_credentials (default) |
No |
clientSecret |
Client secret | Yes |
scope |
https://graph.microsoft.com/.default |
Yes |
Alternatively, instead of tenant:
props.setProperty("tokenUrl",
"https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/token");
The driver fetches and refreshes the access token automatically.
| Property | Value | Required |
|---|---|---|
auth |
oauth2 |
Yes |
oauth2GrantType |
device_code |
Yes |
tenant |
Azure AD tenant ID | Yes |
clientId |
App registration (public client) | Yes |
scope |
Mail.Read offline_access |
Yes |
clientSecret |
— | No (public client) |
The driver prints the sign-in URL, waits for user authentication, and renews the token via refresh token.
| Property | Value | Required |
|---|---|---|
auth |
bearer |
Yes |
token |
Delegated access token | Yes |
Spec file: graph-spec.json
File: graph-spec.json
| SQL table | REST path | Auth type | Notes |
|---|---|---|---|
users |
/users |
OAuth2 client credentials | User.Read.All |
groups |
/groups |
OAuth2 client credentials | Group.Read.All |
messages |
/me/messages |
OAuth2 device code or bearer | Mail.Read delegated |
calendar_events |
/me/events |
OAuth2 device code or bearer | Calendars.ReadWrite delegated, INSERT/UPDATE/DELETE |
user_calendar_events |
/users/{userId}/events |
OAuth2 client credentials | Calendars.ReadWrite app, path param userId |
Shared OData settings in the spec:
"dataPath": "/value",
"filterParam": "$filter",
"selectParam": "$select",
"orderByParam": "$orderby",
"pagination": {
"type": "nextLink",
"limitParam": "$top",
"nextLinkPath": "/@odata.nextLink",
"defaultLimit": 100
}
nextLink: the driver follows @odata.nextLink automatically across all pagesfilterParam: FILTER content is sent as OData $filter (native syntax)selectParam: only columns named in SELECT are requested via $selectjsonPath / selectName: nested fields (e.g. sender on messages)"write": false — writes are disabled in this example.
User.Read.AllGroup.Read.AllFor user calendar (user_calendar_events), also add application permission: Calendars.ReadWrite.
Also add Delegated permissions: Mail.Read (mailbox), Calendars.ReadWrite (calendar). Enable public client flows for device code, or obtain a token manually for bearer auth.
Column names from the spec with mixed case (camelCase, e.g. displayName, start_dateTime) must be double-quoted in SELECT, INSERT, and UPDATE — the SQL parser otherwise normalizes unquoted identifiers to lowercase. FILTER and ORDERBY are not affected: use OData syntax with Graph property names there (displayName, accountEnabled).
SELECT id, "displayName", mail, "userPrincipalName" FROM users;
SELECT id, "displayName", mail FROM users
FILTER startswith(displayName,'M') AND accountEnabled eq true
ORDERBY displayName asc;
| SQL | HTTP (simplified) |
|---|---|
SELECT id, "displayName", mail FROM users |
GET /users?$top=100&$select=id,displayName,mail |
… FILTER startswith(…) |
…&$filter=startswith(displayName,'M') AND accountEnabled eq true |
FILTER contains an OData expression — not param=value (because filterParam is set in the spec).
SELECT id, "displayName", "mailEnabled" FROM groups
FILTER mailEnabled eq true AND securityEnabled eq true;
SELECT id, subject, from_address, "receivedDateTime" FROM messages
FILTER isRead eq false;
Use graph-devicecode.sql (OAuth2 device code) or graph-bearer.sql (pre-obtained token).
Columns from_address and from_name use jsonPath; $select requests the parent field from (selectName).
INSERT INTO calendar_events (subject, "start_dateTime", "end_dateTime", body_content)
VALUES ('Team meeting', '2026-07-01T10:00:00', '2026-07-01T11:00:00', 'Agenda …');
SELECT id, subject, "start_dateTime", "end_dateTime" FROM calendar_events
FILTER startswith(subject,'Team meeting');
UPDATE calendar_events SET subject = 'Team meeting (moved)' FILTER id=EVENT-ID;
DELETE FROM calendar_events FILTER id=EVENT-ID;
| SQL | HTTP (simplified) |
|---|---|
INSERT … |
POST /me/events with JSON {subject, start:{dateTime,timeZone}, end:…} |
UPDATE … FILTER id=… |
PATCH /me/events/{id} |
DELETE … FILTER id=… |
DELETE /me/events/{id} |
If start_timeZone/end_timeZone are omitted, the spec applies Europe/Berlin via defaultWrite. Full example: graph-calendar.sql.
INSERT INTO user_calendar_events ("userId", subject, "start_dateTime", "end_dateTime")
VALUES ('USER-OBJECT-ID', 'Team meeting', '2026-07-01T10:00:00', '2026-07-01T11:00:00');
SELECT id, subject FROM user_calendar_events
FILTER userId='USER-OBJECT-ID' AND startswith(subject,'Team meeting');
UPDATE user_calendar_events SET subject = 'Team meeting (moved)'
FILTER userId='USER-OBJECT-ID' AND id=EVENT-ID;
DELETE FROM user_calendar_events FILTER userId='USER-OBJECT-ID' AND id=EVENT-ID;
Path parameters (userId) come before the OData filter part, separated by AND. Full example: graph-user-calendar.sql.
SELECT table_name, remarks FROM system.table_list;
SELECT column_name, type_name FROM system.column_list WHERE table_name = 'users';
SELECT column_name FROM system.pk_list WHERE table_name = 'users';
/me/…: requires a delegated user token, not client credentials.dataPath, filterParam, nextLink).