REST-JDBC-Treiber

Einleitung

Der RestJDBC-Treiber ermöglicht das Lesen und Schreiben von Daten aus bzw. in REST-APIs über JDBC mit SQL.

Es gelten folgende Konventionen:

  • als Datenbank wird die Verbindung jdbc:rest: verwendet
  • die JDBC-URL enthält die Basis-URL der REST-API (jdbc:rest:https://…)
  • die API wird mit Hilfe einer Spec-Datei (JSON oder YAML) spezifiziert; optional kann baseUrl in der Spec als Fallback gesetzt werden, wenn die JDBC-URL keine HTTP-URL enthält
  • als Schema wird public verwendet (Standard)
  • als Tabellen werden Entity-Namen in der Spec angegeben (z.B. users, orders)
  • die Spalten werden in der Spec definiert; standardmäßig entsprechen sie Top-Level-JSON-Feldern (für verschachtelte Felder: jsonPointer)
  • der Treiber unterstützt SELECT für das Lesen und INSERT, UPDATE und DELETE für das Schreiben

Schnellstart

Der freie REST-Dienst JSONPlaceholder liefert Dummy-Benutzer als JSON-Array. Damit lässt sich ein einfaches Beispiel aufbauen, um den Treiber auszuprobieren.

Als Antwort von GET /users erhalten wir eine Liste, die in etwa so aussieht:

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": { "city": "Gwenborough" }
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "username": "Antonette",
    "email": "Shanna@melissa.tv",
    "address": { "city": "Wisokyburgh" }
  }
]

Mit einer Spec-Datei definiert man, wie die flachen Felder auf eine Tabelle abgebildet werden sollen. Das "GET" wird dann über ein SELECT erreicht.

{
  "entities": [
    {
      "name": "users",
      "path": "/users",
      "columns": [
        { "name": "id", "type": "BIGINT", "primaryKey": true },
        { "name": "name", "type": "VARCHAR" },
        { "name": "email", "type": "VARCHAR" }
      ]
    }
  ]
}

Die Datei liegt zum Beispiel als users-spec.json im Arbeitsverzeichnis beim Aufruf. Die Basis-URL steht in der JDBC-URL.

Properties props = new Properties();
props.setProperty("spec", "users-spec.json");

Connection conn = DriverManager.getConnection(
    "jdbc:rest:https://jsonplaceholder.typicode.com", props);

Im JDBC-Client sieht das so aus:

connect 'jdbc:rest:https://jsonplaceholder.typicode.com|spec=users-spec.json'

SELECT id, name, email FROM users;

Der Text nach | ist ein Connection-Property des datasqill-Treibers. Im Simple Client öffnet man dieselbe Verbindung mit properties(...), die URL bleibt ohne Suffix:

connect 'jdbc:rest:https://jsonplaceholder.typicode.com' properties(spec=users-spec.json)

Siehe Properties in der JDBC-URL und Simple Client.

id name email
1 Leanne Graham Sincere@april.biz
2 Ervin Howell Shanna@melissa.tv

Das SELECT wird zu GET https://jsonplaceholder.typicode.com/users.

Weitergehende Details und weitere Beispiele dieser Test-API findet man im JSONPlaceholder-Beispiel.

Features

Die Festlegung der Details einer REST API erfolgt über Connection Properties sowie eine Spec-Datei in JSON oder YAML. Diese Datei definiert mögliche Tabellen, Spalten, Typen, Primärschlüssel usw. Gemeinsame Einstellungen und Spaltenlisten können über defaults und structures wiederverwendet werden.

Es stehen sieben Authentifizierungsverfahren zur Verfügung:

  • none - keine Authentifizierung
  • basic - HTTP Basic-Auth mit User und Passwort
  • bearer - Authentifizierung mit Bearer Token
  • jwt - lokal signiertes HS256-JWT (zum Beispiel Momentus Enterprise / Ungerboeck)
  • oauth2 - OAuth 2.0 Client Credentials (Token wird automatisch geholt und erneuert)
  • apikey - API-Schlüssel als Query-Parameter oder HTTP-Header
  • clientcert - Mutual TLS mit Client-Zertifikat (Keystore)

Ein Lesen mit einem SELECT über den Treiber wird auf ein HTTP GET mit einer JSON-Antwort abgebildet. Dabei wird ein Array in der JSON-Antwort erwartet, dass die Ergebniszeilen enthält. Standard sind Objekt-Zeilen. Mit rowFormat: "tabular" liest dieselbe Entity stattdessen einen Spaltenkopf und Wertezeilen; siehe Tabellarische Zeilen.

Der Treiber unterstützt verschiedene Paginierungsverfahren

  • none - kein Paginierung
  • offset - Paginierung mit Limit und Offset
  • page - Paginierung mit Angabe einer Seitennummer
  • nextLink - OData-/REST-Paginierung über eine opaque URL in der JSON-Antwort (z. B. @odata.nextLink)

Das Einfügen von Datensätzen mit INSERT, resultiert in einem HTTP POST.

Das Löschen von Datensätzen kann mit DELETE erfolgen. Es führt zu einem HTTP DELETE.

Und schließlich unterstützt der Treiber UPDATEs. Diese münden in HTTP PUT oder HTTP PATCH Aufrufe.

Mit Hilfe einer FILTER-Klausel können Daten bei SELECT, UPDATE oder DELETE serverseitig gefiltert werden.

HTTP-Redirects (301, 302, 303, 307, 308) werden automatisch verfolgt; der Authorization-Header bleibt auch bei einem Wechsel des Hosts erhalten (z. B. Power BI preferClientRouting=true).

Der Treiber bietet Systemtabellen für verfügbare Tabellen, Spalten und Primärschlüssel.

JDBC URL

jdbc:rest:https://api.example.com/v1

Der Teil nach jdbc:rest: ist die Basis-URL der REST-API. Er hat Vorrang vor einem optionalen baseUrl in der Spec. Fehlt die HTTP-URL in der JDBC-URL, kann baseUrl in der Spec als Fallback dienen.

Beispiel-Verbindung (Java)

Properties props = new Properties();
props.setProperty("spec", "/pfad/zu/api-spec.json");
props.setProperty("auth", "bearer");
props.setProperty("token", "mein-bearer-token");

Connection conn = DriverManager.getConnection(
    "jdbc:rest:https://api.example.com/v1", props);

Kann der Client nur die URL übergeben, folgen dieselben Properties dem ersten |, zum Beispiel jdbc:rest:https://api.example.com/v1|spec=/pfad/zu/api-spec.json,auth=bearer,token=mein-bearer-token. Die Regeln stehen unter Properties in der JDBC-URL. Der Simple Client verwendet stattdessen properties(...).

Connection Properties

Es werden folgende Connection Properties unterstützt:

Property Gruppe Beschreibung Erforderlich
spec Spec-Datei Pfad zur Spec-Datei (.json, .yaml oder .yml; Dateisystem oder classpath:...) Ja
auth Auth-Property Auth-Verfahren: none, bearer, basic, jwt, oauth2, apikey, clientcert (siehe unten) Nein
password Auth-Property Passwort Bei basic
user Auth-Property Benutzername Bei basic
token Auth-Property Bearer-Token Bei bearer
tokenurl Auth-Property OAuth2-Token-Endpunkt Bei oauth2 (oder tenant)
tenant Auth-Property Azure AD Tenant-ID oder -Name; leitet tokenurl für Microsoft Graph ab Bei oauth2 (Alternative zu tokenurl)
clientid Auth-Property OAuth2 Client ID (client_id als Alias) Bei oauth2
clientsecret Auth-Property OAuth2 Client Secret (client_secret als Alias) Bei oauth2
scope Auth-Property OAuth2 Scope (z. B. https://graph.microsoft.com/.default) Bei oauth2, falls von der API verlangt
oauth2granttype Auth-Property client_credentials (Standard) oder device_code Bei oauth2
devicecodeurl Auth-Property OAuth2 Device-Code-Endpunkt Bei device_code (oder tenant)
jwtissuer Auth-Property JWT-Claim iss (Momentus: API User ID) Bei jwt
jwtkey Auth-Property JWT-Claim key (Momentus: API User Key) Bei jwt
jwtsecret Auth-Property HMAC-SHA256-Secret (Momentus: API User Secret); kein Claim Bei jwt
jwtttl Auth-Property Lebensdauer des Tokens in Sekunden (Standard: 60) Nein
jwtsubject Auth-Property Optionaler JWT-Claim sub (Momentus: Proxied User) Nein
apikey Auth-Property API-Schlüssel Bei apikey
apikeylocation Auth-Property Übergabe: query (Standard) oder header Bei apikey
apikeyparam Auth-Property Name des Query-Parameters oder HTTP-Headers (Standard: apiKey) Bei apikey
keystore Auth-Property Pfad zum Keystore mit Client-Zertifikat und Private Key Bei clientcert
keystorepassword Auth-Property Passwort für den Keystore Bei clientcert
keystoretype Auth-Property Keystore-Typ (Standard: PKCS12) Nein
keypassword Auth-Property Passwort für den Private Key, falls abweichend von keystorepassword Nein
requestintervalms Retry-Property Mindestabstand zwischen HTTP-Requests in Millisekunden (0 = aus) Nein
retryon429 Retry-Property Bei HTTP 429 automatisch erneut versuchen (Standard: true) Nein
maxretries Retry-Property Maximale Wiederholungen bei HTTP 429 (Standard: 5) Nein
stdoutlog Misc Debug-Ausgabe auf stdout: cursor, http, rows, warn oder all (kombinierbar mit +, z. B. cursor+http+rows) Nein

Zu den Authentifizierungs-Properties siehe Abschnitt Authentifizierung, zu den Retry-Properties siehe Abschnitt Rate Limits.

Property-Namen sind case-insensitive. CamelCase wie clientId oder requestIntervalMs funktioniert ebenfalls.

Spec-Pfad:

/pfad/absolut/api-spec.json
/pfad/absolut/api-spec.yaml
classpath:de/softquadrat/jdbc/rest/mock-spec.json

SQL-Syntax

SELECT

SELECT id, name, email
FROM users

Diese Abfrage führt der Treiber als GET {baseUrl}/users aus.

INSERT

INSERT schreibt neue Datensätze über die REST-API. Im Standard führt dies zu einem POST auf den Entity-path.

Das folgende Beispiel legt über JSONPlaceholder einen Benutzer an. Die Tabelle users hat in der Spec den Pfad /users.

INSERT INTO users (name, username, email)
VALUES ('Ada Lovelace', 'ada', 'ada@example.com')

Der Treiber sendet dafür POST https://jsonplaceholder.typicode.com/users. Der JSON-Body ist {"name":"Ada Lovelace","username":"ada","email":"ada@example.com"}.

Achtung: Die JSONPlaceholder Test-API speichert den Benutzer nicht dauerhaft.

Der Pfad kann bei Bedarf überschrieben werden:

"insert": { "path": "/v2/posts", "method": "POST" }
INSERT INTO posts ("userId", title, body)
VALUES (1, 'Neuer Post', 'Inhalt')

Das Beispiel führt zu POST {baseUrl}/v2/posts. Der JSON-Body wird aus den Spalten und Werten der INSERT-Anweisung aufgebaut.

UPDATE

UPDATE aktualisiert Datensätze über die REST-API. Die Angabe eines FILTER ist Pflicht. Im Standard wird ein HTTP PUT verwendet, etwa PUT auf {path}/{id}.

Das folgende Beispiel ändert bei JSONPlaceholder die E-Mail-Adresse eines Benutzers. Die Tabelle users hat in der Spec den Pfad /users.

UPDATE users SET email = 'ada@example.com' FILTER id=1

Der Treiber sendet dafür PUT https://jsonplaceholder.typicode.com/users/1. Der JSON-Body ist {"email":"ada@example.com"}.

Der Body enthält nur die Spalten aus SET. FILTER id=1 ersetzt {id} in der URL und erscheint nicht im JSON. PUT und PATCH verwenden denselben Body. Der Treiber liest den Datensatz nicht vorher und setzt fehlende Felder nicht ein. Ein UPDATE mit nur einer gesetzten Spalte bleibt damit ein Teilobjekt, auch wenn die Methode PUT ist.

Die Methode kann in der Spec mit PATCH überschrieben werden:

"update": { "method": "PATCH" }
UPDATE posts SET title = 'Neuer Titel', body = 'Neuer Inhalt' FILTER id=1

Das Beispiel führt zu PUT {baseUrl}/posts/1. Der JSON-Body ist {"title":"Neuer Titel","body":"Neuer Inhalt"}. Steht in der Spec "method": "PATCH", sendet der Treiber stattdessen PATCH mit demselben Body.

DELETE

Mit DELETE können Datensätze in der REST-API gelöscht werden. Damit wird ein HTTP DELETE abgesetzt, der im Standard den Pfad aus der Spec verwendet ({path}/{id}).

Das folgende Beispiel löscht bei JSONPlaceholder einen Benutzer. Die Tabelle users hat in der Spec den Pfad /users.

DELETE FROM users FILTER id=1

Der Treiber sendet dafür DELETE https://jsonplaceholder.typicode.com/users/1.

Der Pfad kann in der Spec überschrieben werden:

"delete": { "path": "/posts/{id}" }
DELETE FROM posts FILTER id=1

Der Wert aus id=1 ersetzt den Platzhalter {id} im Pfad. Der Treiber sendet deshalb DELETE {baseUrl}/posts/1.

Spaltennamen und Anführungszeichen

Unquoted Identifier werden vom SQL-Parser in Kleinbuchstaben normalisiert (userId → userid). Spaltennamen in der Spec mit gemischter Schreibweise (z. B. userId aus JSON-APIs) müssen in INSERT und UPDATE in Anführungszeichen stehen:

INSERT INTO posts ("userId", title, body)
VALUES (1, 'Neuer Post', 'Inhalt')

UPDATE posts SET title = 'Neuer Titel' FILTER id=1

In SELECT gilt dasselbe für Spalten mit gemischter Schreibweise:

SELECT id, "userId", title FROM posts

FILTER-Parameter werden als Text an die API weitergegeben und sind von der Spaltennamen-Normalisierung nicht betroffen (FILTER userId=1 bleibt userId).

FILTER, ORDERBY und weitere Klauseln

Die FILTER-Klausel gibt Bedingungen an die REST-API weiter, serverseitig und nicht lokal wie WHERE. ORDERBY übergibt die Sortierung an die API (datasqill-Dialekt, nicht ORDER BY). Mit selectParam und expandParam legt die Spec fest, welche Spalten und verwandten Ressourcen angefordert werden. Platzhalter im Pfad, etwa {userId}, werden aus FILTER oder INSERT gebunden.

Diese weitergehenden Features stehen im Kapitel Weitergehende SQL-Features.

Spec-Datei

Jede anzubindende REST-API benötigt eine Spec-Datei im Format JSON oder YAML. Das Format wird an der Dateiendung erkannt: .json → JSON, .yaml / .yml → YAML. Vollständige Beispiele: spec-example.json, spec-example.yaml.

Struktur (JSON)

Die vollständige Beispieldatei ist spec-example.json.

Auszug (die Basis-URL steht in der JDBC-URL, nicht in der Spec):

{
  "entities": [
    {
      "name": "users",
      "path": "/users",
      "pagination": {
        "type": "offset",
        "limitParam": "limit",
        "offsetParam": "offset",
        "defaultLimit": 100
      },
      "columns": [
        { "name": "id", "type": "BIGINT", "primaryKey": true },
        { "name": "name", "type": "VARCHAR" },
        { "name": "email", "type": "VARCHAR" }
      ]
    }
  ]
}

Struktur (YAML)

Die vollständige Beispieldatei ist spec-example.yaml.

Inhalt und Feldnamen sind dieselben wie im JSON-Beispiel:

entities:
  - name: users
    path: /users
    pagination:
      type: offset
      limitParam: limit
      offsetParam: offset
      defaultLimit: 100
    columns:
      - name: id
        type: BIGINT
        primaryKey: true
      - name: name
        type: VARCHAR
      - name: email
        type: VARCHAR

Spec-Felder

Die jeweiligen Entities-Einträge definieren eine Tabelle.

Feld Beschreibung
baseUrl Basis-URL der API (optionaler Fallback, wenn die JDBC-URL keine HTTP-URL enthält)
defaults Vorgabewerte für Entity-Felder (write, pagination, dataPath, rowFormat, columnsPath, unknownColumns, filterParam, selectParam, expandParam, structure, …); Entity-Werte haben Vorrang
structures Benannte Spaltenlisten, die sich mehrere Tabellen teilen können
entities Liste der JDBC-„Tabellen“
entities[].name Tabellenname
entities[].columns Spalten mit JDBC-Typ (alternativ zu structure)
entities[].structure Name einer gemeinsamen Struktur aus structures (alternativ zu columns)
entities[].path REST-Pfad; {name}-Platzhalter werden aus FILTER (param=wert) gefüllt. Ein statischer Query-String ist erlaubt (/groups?preferClientRouting=true); weitere Parameter hängen mit & an. Für Schreiben besser vermeiden — Defaults würden {path}/{id} hinter dem ? erzeugen. Konstante Query-Parameter lieber als Spalte mit binding: "query" modellieren
entities[].dataPath JSON-Pointer auf die Zeilenliste
entities[].rowFormat object (Standard) oder tabular. tabular liest einen Spaltenkopf und Wertezeilen statt Objekt-Zeilen, siehe Tabellarische Zeilen
entities[].columnsPath JSON Pointer auf den Spaltenkopf. Pflicht bei rowFormat: "tabular", muss mit / beginnen
entities[].unknownColumns Umgang mit Kopfspalten ohne Spec-Spalte: error (Standard), warn oder ignore. Nur bei rowFormat: "tabular"
entities[].filterParam Query-Parameter für den restlichen FILTER nach Pfad-/Query-Bindings (Inhalt wird unverändert übergeben)
entities[].selectParam Query-Parameter für Spaltenauswahl aus dem SELECT (z. B. OData $select)
entities[].expandParam Query-Parameter für verwandte Ressourcen (z. B. OData $expand)
entities[].expand Default-$expand-Wert: String oder Liste von Strings
entities[].orderByParam Query-Parameter für ORDERBY-Klausel
entities[].pagination Paginierung (siehe unten)
entities[].write false = alle Schreiboperationen deaktivieren (Standard: true)
entities[].insert Optional: Override für INSERT, oder false zum Deaktivieren
entities[].update Optional: Override für UPDATE, oder false zum Deaktivieren
entities[].delete Optional: Override für DELETE, oder false zum Deaktivieren

Pflicht sind diese Felder:

  • name - der Tabellenname wie er in SQL verwendet wird
  • columns oder structure - Spalten mit Namen, Datentypen und optionaler Primärschlüssel-Kennzeichnung, oder Verweis auf eine gemeinsame Struktur
  • path - der REST-Pfad für den Aufruf der REST-API
  • dataPath - JSON-Pointer zur Zeilen-Liste im Ergebnis einer Abfrage (siehe unten)

Alle weiteren Einträge sind optionale Ergänzungen.

Spalten-Typen

Die Spec kennt diese Typen.

Typ Syntax JDBC-Typ Hinweis
BIGINT BIGINT BIGINT —
INTEGER INTEGER INTEGER Alias INT
BOOLEAN BOOLEAN BOOLEAN —
DOUBLE DOUBLE DOUBLE —
TIMESTAMP TIMESTAMP TIMESTAMP —
DATE DATE TIMESTAMP Typname bleibt DATE. Gelesen wie TIMESTAMP.
JSON JSON VARCHAR Typname bleibt JSON. Kein SQL-Typ.
VARCHAR VARCHAR oder VARCHAR(n) VARCHAR column_size = n
CHAR CHAR oder CHAR(n) CHAR column_size = n
VARBINARY VARBINARY oder VARBINARY(n) VARBINARY column_size = n
DECIMAL DECIMAL, DECIMAL(p) oder DECIMAL(p,s) DOUBLE oder DECIMAL Ohne Klammern DOUBLE. Mit Klammern DECIMAL; column_size = p, decimal_digits = s.
NUMERIC NUMERIC(p) oder NUMERIC(p,s) DECIMAL Wie DECIMAL mit Klammern. Ohne Klammern VARCHAR.

JSON ist ein REST-Spec-Typ, kein SQL-Typ. JDBC sieht VARCHAR (Typname JSON). Beim Lesen kommen Objekte und Arrays wie bei VARCHAR als JSON-Text. Beim Schreiben wird der String als JSON-Baum ins Request-Body gesetzt (Array/Objekt), nicht als JSON-String.

Primärschlüssel werden durch ein "primaryKey": true an der Spalte gekennzeichnet.

Länge und Precision

Bei VARCHAR, CHAR, VARBINARY, DECIMAL und NUMERIC kann die Länge bzw. Precision direkt im type-Feld stehen. n, p und s erscheinen in den JDBC-Metadaten (column_size, decimal_digits) und in system.column_list. Zur Laufzeit werden sie nicht geprüft.

  • Groß-/Kleinschreibung ist egal (varchar(255) = VARCHAR(255)).
  • Fehlt bei DECIMAL(p) oder NUMERIC(p) die Scale, gilt 0. Die Scale darf die Precision nicht überschreiten.
  • Ein leeres type oder ein unbekannter Typ ohne Klammern wird als VARCHAR geladen.
  • Ungültige Angaben (z. B. VARCHAR(0), DECIMAL(2,5), INTEGER(10)) führen beim Laden der Spec zu einem Fehler.

Ein Beispiel für solche Typangaben:

"columns": [
  { "name": "code", "type": "CHAR(10)" },
  { "name": "email", "type": "VARCHAR(255)" },
  { "name": "payload", "type": "VARBINARY(256)" },
  { "name": "amount", "type": "DECIMAL(10,2)" }
]

Optionale Spaltenfelder

Für jede Spalte sind die folgenden Felder optional:

Feld Bedeutung
jsonPointer JSON Pointer für Lesen und Schreiben (RFC 6901, gleiche Syntax wie dataPath). "" = ganzes Zeilenobjekt
readPath Nur Lesen; Standard: jsonPointer
writePath Nur Schreiben; Standard: jsonPointer; null = nur lesbar
readOnly Spalte bei INSERT/UPDATE ignorieren
writeOnly Nicht aus API-Antwort befüllen
defaultWrite Standardwert bei INSERT, wenn Spalte fehlt
selectName API-Feldname für $select (Standard: Spaltenname bzw. erstes Segment von jsonPointer)
expand $expand-Fragment, das gesendet wird, wenn diese Spalte im SELECT steht (z. B. attachments)
binding Request-Routing: path (URI-Platzhalter) oder query (eigener Query-Parameter). Alias: parameterType
required Bei binding: "query": FILTER muss diesen Parameter liefern
sourceName Exakter Name im Antwortkopf bei rowFormat: "tabular". Ohne Angabe gilt name. Schließt sourcePosition aus
sourcePosition Index im Kopf bei rowFormat: "tabular". 0 ist die erste Spalte, -1 die letzte
sourceMeta Nur columnName: die Spalte liefert den Kopfnamen an dieser Position, nicht den Zellwert

Ohne jsonPointer gilt der Spaltenname als Top-Level-JSON-Feld (bisheriges Verhalten). sourceName, sourcePosition und sourceMeta gelten für Tabellarische Zeilen. Bei Objekt-Zeilen sind sie ein Fehler beim Laden der Spec, ebenso columnsPath und unknownColumns an der Entity.

Der leere Pointer "" bezeichnet laut RFC 6901 das gesamte Zeilenobjekt (nach dataPath), nicht den kompletten HTTP-Body:

{ "name": "row", "type": "VARCHAR", "jsonPointer": "" }
SELECT id, row
FROM users

Die Spalte liefert das Zeilenobjekt als JSON-Text — also genau das, was die API nach dataPath (und ggf. $select) zurückgibt. SELECT row holt das volle Objekt, weil kein $select gesendet wird. SELECT id, row sendet $select=id; row enthält dann das reduzierte JSON. Schreiben über jsonPointer: "" ist nicht möglich (das Wurzelobjekt lässt sich nicht per Pointer ersetzen); ohne eigenes writePath ist die Spalte nur lesbar. Als Typ genügt VARCHAR, weil die Spalte den JSON-Text nur liest. Der Spec-Typ JSON würde sich erst beim Schreiben unterscheiden: dort setzt der Treiber den Wert als JSON-Baum ein und nicht als Text.

jsonPointer — verschachtelte JSON-Felder

Viele APIs (z. B. Microsoft Graph) liefern verschachtelte JSON-Strukturen. Mit jsonPointer lassen sich SQL-Spalten darauf abbilden:

{
  "name": "start_dateTime",
  "type": "TIMESTAMP",
  "jsonPointer": "/start/dateTime"
},
{
  "name": "start_timeZone",
  "type": "VARCHAR",
  "jsonPointer": "/start/timeZone",
  "defaultWrite": "Europe/Berlin"
}
INSERT INTO calendar_events (subject, "start_dateTime", "end_dateTime")
VALUES ('Review', '2026-06-26T10:00:00', '2026-06-26T11:00:00')

Der Request-Body sieht so aus:

{
  "subject": "Review",
  "start": { "dateTime": "2026-06-26T10:00:00", "timeZone": "Europe/Berlin" },
  "end":   { "dateTime": "2026-06-26T11:00:00", "timeZone": "Europe/Berlin" }
}

Für komplexe Strukturen, zum Beispiel Teilnehmerlisten, verwendet man den Typ JSON und übergibt in SQL ein JSON-Literal:

{ "name": "attendees", "type": "JSON", "jsonPointer": "/attendees", "writeOnly": true }

Beim SELECT liest der Treiber die Werte von denselben Pfaden, die er beim Schreiben benutzt. Ein Spaltenname mit gemischter Schreibweise muss in SQL in Anführungszeichen stehen. Aus start_dateTime ohne Anführungszeichen macht der Parser start_datetime, und der Name passt nicht mehr zur Spec. In der Abfrage schreibt man deshalb "start_dateTime".

dataPath – wo stehen die Zeilen im JSON?

Zum Parsen der JSON-Antwort muss per JSON-Pointer zum Array mit den Datensätzen navigiert werden. Zur Syntax siehe die RFC zu Json-Pointer.

API-Antwort dataPath
Flaches Array [{...},{...}] "" (oder Feld weglassen)
{ "data": [{...}] } /data
{ "items": [{...}] } /items

Der leere JSON-Pointer "" bezeichnet laut RFC 6901 das Wurzelelement — bei einer direkten Array-Antwort also das Array selbst. / ist nicht die Wurzel.

Flaches Array. Die Antwort ist direkt ein JSON-Array. Das ist der Standard. dataPath kann entfallen oder auf "" gesetzt werden.

[
  { "id": 1, "title": "Hello" },
  { "id": 2, "title": "World" }
]

Verschachtelt unter data. Die Zeilen stehen im Feld data. Dafür setzt man dataPath auf "/data".

{
  "data": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}

Verschachtelt unter items. Die Zeilen stehen im Feld items. Dafür setzt man dataPath auf "/items".

{
  "items": [
    { "sku": "A-100", "qty": 5 },
    { "sku": "B-200", "qty": 12 }
  ]
}

Bei Objekt-Zeilen (Standard, rowFormat fehlt oder ist object) werden nur flache Felder auf oberster Ebene jedes Array-Elements als Spalten gelesen — sofern kein jsonPointer gesetzt ist. Verschachtelte Objekte und Arrays werden sonst als String serialisiert (siehe jsonPointer oben).

API-Antwort:

[
  {
    "id": 1,
    "name": "Alice",
    "address": { "city": "Berlin", "zip": "10115" },
    "tags": ["vip", "beta"]
  }
]

Ergebnis als JDBC-Zeile (eine Spalte pro Top-Level-Feld):

id name address tags
1 Alice {"city":"Berlin","zip":"10115"} ["vip","beta"]

Die Felder city und zip innerhalb von address werden nur mit jsonPointer zu eigenen Spalten (z. B. "jsonPointer": "/address/city").

Weitere Themen

defaults und structures fassen gemeinsame Spaltenlisten zusammen. rowFormat: "tabular" liest einen Spaltenkopf und Wertezeilen; solche Tabellen sind nur lesbar. Schreiben ist im Standard POST, PUT und DELETE auf dem Entity-Pfad und lässt sich in der Spec überschreiben oder abschalten. Diese weitergehenden Features stehen im Kapitel Weitergehende Spec-Features.

Die Paginierung (none, offset, page, nextLink) hat ein eigenes Kapitel Pagination.

Authentifizierung

Auth-Verfahren und Zugangsdaten werden über Connection Properties konfiguriert.

Die Property auth legt das Verfahren fest: none, bearer, basic, jwt, oauth2, apikey oder clientcert. Ist auth nicht gesetzt, wird automatisch erkannt:

  • user und password gesetzt → basic
  • nur token gesetzt → bearer
  • nur apikey gesetzt → apikey
  • nur keystore gesetzt → clientcert
  • sonst → none

oauth2 und jwt werden nicht aus ihren Properties erkannt. auth muss explizit gesetzt werden.

Die Client-Zertifikat-Authentifizierung erfolgt auf der TLS-Ebene (Mutual TLS). Sie lässt sich mit bearer oder basic kombinieren, wenn die API sowohl mTLS als auch einen Authorization-Header verlangt.

Die Prüfung des Server-Zertifikats nutzt den JVM-Standard-Truststore (cacerts bzw. -Djavax.net.ssl.trustStore=...). Separate Truststore-Properties gibt es im Treiber nicht.

Keine Authentifizierung

props.setProperty("auth", "none");

Alternativ setzt man keine Auth-Properties. Dann erkennt der Treiber das Verfahren none.

Bearer Token

props.setProperty("auth", "bearer");
props.setProperty("token", "eyJhbGciOiJIUzI1NiIs...");

Der Treiber sendet den Header Authorization: Bearer <token>.

JWT (lokal signiert)

Für APIs, die ein kurzlebiges HS256-Token prüfen, das der Client selbst signiert, zum Beispiel Momentus Enterprise (Ungerboeck). Der Treiber ruft keinen Token-Endpunkt auf. Er baut das JWT und sendet Authorization: Bearer <jwt>.

Claim Property Momentus-Feld
iss jwtissuer API User ID aus dem Fenster API User
key jwtkey API User Key (GUID)
sub jwtsubject Proxied User; Property weglassen, wenn ungenutzt
iat, exp jwtttl Lebensdauer in Sekunden; Standard 60

jwtsecret ist der HMAC-Schlüssel (das API User Secret) und steht nicht im Token. Header: {"typ":"JWT","alg":"HS256"}. iat und exp sind Unix-Sekunden.

Dasselbe Token wird wiederverwendet und fünf Sekunden vor exp neu signiert. Ein seitenweises SELECT bricht dadurch nicht ab, wenn das erste Token abläuft. Die veröffentlichten Momentus-Beispiele nutzen 30 oder 60 Sekunden. Lehnt der Server eine längere Lebensdauer ab, setzt man jwtttl=30.

props.setProperty("auth", "jwt");
props.setProperty("jwtissuer", "APIUSERID");
props.setProperty("jwtkey", "4Fe7f631-2224-4098-82b9-38bb89c7247e");
props.setProperty("jwtsecret", "2fa24f34-1b2c-4ecf-8645-44ba766d6b89");
props.setProperty("jwtttl", "60");
// optional, Momentus Proxied User:
// props.setProperty("jwtsubject", "PROXYUSER");
jdbc:rest:https://your-site.ungerboeck.com/PROD/api/v1

Die API-Hilfe einer Installation liegt typischerweise unter https://your-site.ungerboeck.com/PROD/api/help. Aufrufe gehen an /PROD/api/v1, nicht an die Hilfe-Seite. Listen-Endpunkte erwarten meist pageSize, page und search; siehe Seiten-Paginierung.

OAuth 2.0 Client Credentials

Für APIs wie Microsoft Graph mit Anwendungsberechtigungen (App-only, z. B. /users, /groups):

props.setProperty("auth", "oauth2");
props.setProperty("oauth2granttype", "client_credentials"); // Standard, kann entfallen
props.setProperty("tenant", "Ihre-Tenant-ID");
props.setProperty("clientid", "Ihre-App-Client-ID");
props.setProperty("clientsecret", "Ihr-Client-Secret");
props.setProperty("scope", "https://graph.microsoft.com/.default");

Alternativ mit explizitem Token-Endpunkt statt tenant:

props.setProperty("auth", "oauth2");
props.setProperty("tokenurl", "https://login.microsoftonline.com/Ihre-Tenant-ID/oauth2/v2.0/token");
props.setProperty("clientid", "Ihre-App-Client-ID");
props.setProperty("clientsecret", "Ihr-Client-Secret");
props.setProperty("scope", "https://graph.microsoft.com/.default");

Der Treiber holt beim Verbindungsaufbau ein Access Token per POST (grant_type=client_credentials) und sendet es als Authorization: Bearer …. Vor Ablauf (expires_in, mit 60 Sekunden Puffer) wird das Token automatisch erneuert.

OAuth 2.0 Device Code (delegiert)

Für delegierte Berechtigungen mit Benutzer-Anmeldung (z. B. Microsoft Graph /me/messages). Der Treiber zeigt URL und Code an, wartet auf die Anmeldung und nutzt danach Refresh Tokens:

props.setProperty("auth", "oauth2");
props.setProperty("oauth2granttype", "device_code");
props.setProperty("tenant", "Ihre-Tenant-ID");
props.setProperty("clientid", "Ihre-App-Client-ID");
props.setProperty("scope", "Mail.Read offline_access");

Bei einem Public Client kann clientsecret entfallen. Der Scope offline_access erlaubt es, das Token später über ein Refresh Token zu erneuern.

Siehe auch: Microsoft Graph-Beispiel.

HTTP Basic

props.setProperty("auth", "basic");
props.setProperty("user", "api-user");
props.setProperty("password", "geheim");

Der Treiber sendet den Header Authorization: Basic <base64(user:password)>.

API-Schlüssel

Mit auth=apikey wird der Schlüssel entweder als Query-Parameter oder als HTTP-Header gesendet. Die Property apikeylocation legt das fest (query oder header, Standard: query). apikeyparam ist der Name des Parameters bzw. Headers (Standard: apiKey).

Query-Parameter

Für APIs, die den Schlüssel in der URL erwarten (z. B. OpenWeather mit appid):

props.setProperty("auth", "apikey");
props.setProperty("apikey", "Ihr-API-Schlüssel");
props.setProperty("apikeylocation", "query");
props.setProperty("apikeyparam", "appid");

Der Treiber hängt bei jedem Request appid=Ihr-API-Schlüssel an die URL an. apikeylocation=query kann entfallen, da es der Standard ist.

Siehe auch: OpenWeatherMap-Beispiel.

HTTP-Header

Für APIs, die den Schlüssel im Header erwarten (z. B. Header-Name apikey):

props.setProperty("auth", "apikey");
props.setProperty("apikey", "Ihr-API-Schlüssel");
props.setProperty("apikeylocation", "header");
props.setProperty("apikeyparam", "apikey");

Der Treiber sendet bei jedem Request: apikey: Ihr-API-Schlüssel

Client-Zertifikat (mTLS)

Für APIs, die beim TLS-Handshake ein Client-Zertifikat verlangen:

props.setProperty("auth", "clientcert");
props.setProperty("keystore", "/pfad/zu/client.p12");
props.setProperty("keystorepassword", "geheim");
// optional:
props.setProperty("keystoretype", "PKCS12");
props.setProperty("keypassword", "geheim");

Ohne keystoretype wird PKCS12 verwendet (üblich für .p12-/.pfx-Dateien). Weitere Typen wie JKS sind möglich, sofern die JVM sie unterstützt (KeyStore.getInstance(...)).

Das Client-Zertifikat lässt sich mit einem Bearer-Token kombinieren:

props.setProperty("keystore", "/pfad/zu/client.p12");
props.setProperty("keystorepassword", "geheim");
props.setProperty("auth", "bearer");
props.setProperty("token", "mein-bearer-token");

Hinweis: OAuth2 Client Credentials und Device Code werden wie oben konfiguriert. Ein separat beschafftes Token kann weiterhin mit auth=bearer übergeben werden.

Pagination

type Bedeutung Parameter
none Ein Request, alle Zeilen —
offset Limit/Offset limitParam, offsetParam, defaultLimit
page Seitennummer (1-basiert) limitParam, pageParam, defaultLimit
nextLink Opaque URL aus der Antwort limitParam, nextLinkPath, defaultLimit

defaultLimit ist die Seitengröße (Anzahl Zeilen pro Request). Bei offset und page holt der Treiber automatisch weitere Seiten, bis die API eine leere Liste liefert oder weniger Zeilen als defaultLimit zurückkommen. Bei nextLink folgt der Treiber der URL aus nextLinkPath (Standard: /@odata.nextLink), bis kein Link mehr geliefert wird.

Ohne Paginierung (none)

Die API liefert alle Datensätze in einem Request:

"pagination": { "type": "none" }
SELECT id, name FROM users

Das ergibt einen Aufruf: GET /users.

Offset-Paginierung

So sieht die Spec aus, wenn die API limit und offset als Query-Parameter erwartet:

"pagination": {
  "type": "offset",
  "limitParam": "limit",
  "offsetParam": "offset",
  "defaultLimit": 2
}
SELECT id, name FROM users

Beispiel: 5 Datensätze in der API, Seitengröße 2 — der Treiber führt nacheinander aus:

# HTTP-Request Antwort (Auszug)
1 GET /users?limit=2&offset=0 [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
2 GET /users?limit=2&offset=2 [{"id":3,"name":"Carol"},{"id":4,"name":"Dave"}]
3 GET /users?limit=2&offset=4 [{"id":5,"name":"Eve"}]

Nach Request 3 endet das Nachladen (nur 1 Zeile, weniger als defaultLimit). Das ResultSet enthält alle 5 Zeilen — für die Anwendung wirkt es wie ein einzelnes SELECT.

Seiten-Paginierung

So sieht die Spec aus, wenn die API limit und page ab Seite 1 erwartet:

"pagination": {
  "type": "page",
  "limitParam": "limit",
  "pageParam": "page",
  "defaultLimit": 2
}
SELECT id, name FROM users

Für dieselben 5 Datensätze und die Seitengröße 2 sieht die Folge so aus:

# HTTP-Request Antwort (Auszug)
1 GET /users?limit=2&page=1 [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
2 GET /users?limit=2&page=2 [{"id":3,"name":"Carol"},{"id":4,"name":"Dave"}]
3 GET /users?limit=2&page=3 [{"id":5,"name":"Eve"}]

Momentus Enterprise nutzt denselben Typ mit anderen Parameternamen. page bleibt 1-basiert. filterParam schickt den FILTER-Text als Query-Parameter search:

"filterParam": "search",
"pagination": {
  "type": "page",
  "limitParam": "pageSize",
  "pageParam": "page",
  "defaultLimit": 100
}

Pfade und Spaltennamen stehen in der API-Hilfe der Installation. Liegt die Liste in einem Objekt, setzt man dataPath (zum Beispiel /Results). Die Authentifizierung für diese API ist JWT.

nextLink-Paginierung (OData)

Für APIs wie Microsoft Graph, die @odata.nextLink in der JSON-Antwort liefern:

"dataPath": "/value",
"pagination": {
  "type": "nextLink",
  "limitParam": "$top",
  "nextLinkPath": "/@odata.nextLink",
  "defaultLimit": 100
}

Der erste Request nutzt $top plus die übrigen Spec-Query-Optionen ($select, $expand, $filter, …). Folgeseiten werden über die komplette nextLink-URL abgerufen (ohne diese Parameter neu zu bauen).

Beispiel JSONPlaceholder

Siehe JSONPlaceholder — dort u. a. Paginierung mit _limit/_page und die vollständige Spec.

Rate Limits

Manche APIs begrenzen die Anzahl der Requests pro Zeiteinheit. Der Treiber unterstützt proaktive Drosselung und automatische Wiederholung bei HTTP 429:

Property Beschreibung Standard
requestintervalms Mindestabstand zwischen HTTP-Request-Starts in Millisekunden (0 = aus) 0
retryon429 Bei HTTP 429 automatisch erneut versuchen true
maxretries Maximale Wiederholungen bei HTTP 429 5

Bei requestintervalms=1000 wartet der Treiber nicht pauschal 1 Sekunde nach jedem Aufruf. Er merkt sich den Startzeitpunkt des letzten Requests und wartet nur die verbleibende Zeit bis zum Intervall. Das ist besonders bei Paginierung effizient: war der letzte Request schnell und liegt er schon länger als 1 Sekunde zurück, folgt der nächste sofort.

Bei HTTP 429 wird der Request automatisch wiederholt. Die Wartezeit kommt aus dem Header Retry-After (Sekunden oder HTTP-Datum) oder — falls nicht gesetzt — aus requestintervalms (mindestens 1000 ms).

props.setProperty("requestintervalms", "1000");
props.setProperty("retryon429", "true");

Siehe auch: Webmetic-Beispiel.

HTTP-Redirects

Der Treiber folgt 301, 302, 303, 307 und 308 (maximal 5 Hops). 303 wechselt auf GET. Ein Redirect von HTTPS nach HTTP wird abgelehnt.

Im Gegensatz zum Java-HttpClient (der Authorization bei einem anderen Host streicht) sendet der Treiber die ursprünglichen Request-Header auch an das Redirect-Ziel. Das ist nötig, wenn eine API den Client auf einen anderen Cluster schickt.

Power BI: Mit dem Query-Parameter preferClientRouting=true antwortet die API bei falschem Cluster mit 307 Temporary Redirect und Location auf wabi-…-redirect.analysis.windows.net. Ohne den Parameter leitet Power BI intern weiter (200 mit Daten); mit Parameter muss der Client folgen — sonst kommt ein leerer Body und keine Zeilen. Den Parameter als Spalte mit binding: "query" modellieren, nicht dauerhaft in path packen (siehe FILTER-Klausel).

Mit stdoutlog=http werden Redirects als 307 redirect <url> -> <url> protokolliert.

Weitergehende SQL-Features

Dieses Kapitel beschreibt FILTER, Sortierung, die Spaltenauswahl über selectParam, verwandte Ressourcen über expandParam und Pfad-Parameter in der Spec.

FILTER-Klausel

Die FILTER-Klausel leitet Filter an die REST-API weiter — serverseitig, nicht lokal wie WHERE. Das Verhalten hängt von der Spec ab:

Standard: param=wert

Ohne filterParam in der Spec gilt die Syntax param=wert:

SELECT id, name FROM users FILTER id=2

SELECT id, title FROM posts FILTER userId=1

DELETE FROM posts FILTER id=1

Das erzeugt z.B. GET /users?id=2, GET /posts?userId=1, DELETE /posts/1.

Der Name links vom = ist der HTTP-Query-Parameter bzw. Platzhalter im DELETE-/UPDATE-Pfad ({id}). Er muss nicht mit einer SELECT-Spalte übereinstimmen.

API mit generischem Suchparameter (z. B. q):

SELECT id, name FROM users FILTER q=status = 'active'

→ GET /users?q=status+%3D+%27active%27

Mehrere Parameter mit AND

Ohne filterParam können mehrere Query-Parameter in einer FILTER-Klausel kombiniert werden:

SELECT company_id, company_name
FROM wem_company
FILTER company_id='12345'

→ GET /company?company_id=12345

AND wird unabhängig von der Großschreibung erkannt. String-Werte setzt man in einfache Anführungszeichen. Siehe Webmetic-Beispiel.

Mit filterParam: nativer API-Ausdruck

Ist in der Spec filterParam gesetzt (z. B. "$filter" für OData/Microsoft Graph), wird der restliche FILTER-Inhalt (nach Pfad- und Query-Bindings) unverändert an diesen Query-Parameter übergeben — analog zu Salesforce (SOQL) oder Eloqua (OData):

"filterParam": "$filter"
SELECT id, "displayName" FROM users
  FILTER startswith(displayName,'A') AND accountEnabled eq true

→ GET /users?$filter=startswith(displayName,'A')+AND+accountEnabled+eq+true

Pfad-Platzhalter ({userId} in path) werden weiterhin aus führenden param=wert-Klauseln genommen und nicht als $filter gesendet. Der Rest geht an filterParam.

Für UPDATE und DELETE gilt weiterhin FILTER param=wert zur Identifikation der Ressource im Pfad.

Hinweis: WHERE filtert lokal auf bereits geladene Zeilen. Für REST-APIs solltest du FILTER verwenden.

Bei ungültiger FILTER-Syntax meldet der Treiber den empfangenen FILTER-Text und einen konkreten Hinweis (z. B. fehlendes =, OR statt AND). Die HTTP-URL wird in diesem Fall noch nicht erzeugt — bei erfolgreichen Requests hilft stdoutlog=http.

Pfad- und Query-Bindings (binding)

Viele APIs mischen URI-Segmente, eigene Query-Parameter und einen Filterausdruck in einem Request — z. B. /orders?from=2025-01-01&to=2025-12-31, /reports?year=2025 oder Graph calendarView. Das wird an der Spalte deklariert, unabhängig von Graph:

{ "name": "customerId", "type": "VARCHAR", "binding": "path" }
{ "name": "from", "type": "VARCHAR", "binding": "query", "required": true }
{ "name": "to", "type": "VARCHAR", "binding": "query" }
{ "name": "status", "type": "VARCHAR" }
SELECT id, status FROM orders
FILTER customerId='c-1' AND from='2025-01-01' AND to='2025-12-31' AND status='open'
binding Wirkung
path Füllt {customerId} in path (Platzhalter in der Pfad-Vorlage gelten auch ohne dieses Feld)
query Eigener Query-Parameter; nicht als $filter gesendet
weggelassen Restlicher FILTER: filterParam, falls gesetzt, sonst generische param=wert-Query-Parameter

parameterType wird als Alias für binding akzeptiert. Mit required: true bei einem Query-Binding ist ein fehlender FILTER-Wert ein Fehler.

Feste Flags wie Power BI preferClientRouting=true gehören als Query-Binding in die Spec, nicht in den REST-Pfad:

{ "name": "preferClientRouting", "type": "BOOLEAN", "binding": "query" }
SELECT id, name FROM groups FILTER preferClientRouting=true

Gebundene name=wert-Klauseln müssen vorne stehen (mit AND). Danach darf ein nativer Ausdruck folgen (eq, startswith, …), wenn filterParam gesetzt ist:

"path": "/users/{mailbox}/calendarView",
"filterParam": "$filter",
"columns": [
  { "name": "mailbox", "binding": "path", "readOnly": true },
  { "name": "startDateTime", "binding": "query", "required": true, "readOnly": true },
  { "name": "endDateTime", "binding": "query", "required": true, "readOnly": true },
  { "name": "subject", "type": "VARCHAR" }
]
SELECT id, subject FROM calendar_view
FILTER mailbox='user@example.com'
  AND startDateTime='2020-01-01T00:00:00Z'
  AND endDateTime='2020-12-31T23:59:59Z'
  AND subject eq 'Meeting'

→ GET /users/user@example.com/calendarView?startDateTime=…&endDateTime=…&$filter=subject eq 'Meeting'

Pfad- und Query-Spalten gehören nicht zu $select. Im Resultset werden sie aus den FILTER-Werten des Requests gefüllt (in der JSON-Antwort fehlen sie in der Regel). Siehe Microsoft Graph — Kalenderansicht.

ORDERBY-Klausel

Wenn orderByParam in der Spec gesetzt ist, kann man es in der Query verwenden:

SELECT id, name
FROM users
ORDERBY name ASC

Hinweis: Es heißt ORDERBY (datasqill-Dialekt), nicht ORDER BY.

Spaltenauswahl (selectParam)

Ist selectParam in der Spec gesetzt (z. B. "$select" für OData), sendet der Treiber nur die im SELECT referenzierten Spalten an die API. Die Feldnamen stammen aus der Spec:

  • Spaltenname, wenn kein jsonPointer gesetzt ist
  • selectName, falls angegeben
  • sonst erstes Segment von jsonPointer (z. B. /from/emailAddress/address → from)
  • Spalten mit jsonPointer: "" tragen nicht zu $select bei; steht nur eine solche Spalte im SELECT, entfällt $select (volles Objekt)
  • Spalten mit binding path oder query (und Pfad-Platzhalter) werden nicht in $select aufgenommen
"selectParam": "$select"
SELECT id, "displayName", mail FROM users

→ GET /users?$select=id,displayName,mail

Ohne selectParam werden alle in der Spec definierten Spalten angefordert (bisheriges Verhalten).

Verwandte Ressourcen (expandParam)

Ist expandParam gesetzt (z. B. "$expand" für OData), kann der Treiber beim SELECT verschachtelte bzw. verwandte Ressourcen anfordern. Die Werte stammen aus:

  • Entity-Feld expand — wird immer gesendet (String oder Liste)
  • Spaltenfeld expand — wird gesendet, wenn die Spalte in der SELECT-Liste steht
  • SELECT * / keine Spaltenliste — alle Spalten-expand-Werte plus der Entity-Default

Mehrere Fragmente werden per Komma zusammengeführt. Kommas innerhalb von OData-Klammern (z. B. $filter=…) gelten nicht als Trenner. Dubletten entfallen.

"expandParam": "$expand",
"expand": "singleValueExtendedProperties($filter=id eq 'String {guid} Name extra')"

Mehrere Werte gibt man als Liste an:

"expandParam": "$expand",
"expand": ["calendar", "attachments"]

Ein Expand auf Spaltenebene wird nur gesendet, wenn die Spalte im SELECT steht:

{ "name": "attachments", "type": "JSON", "jsonPointer": "/attachments", "expand": "attachments" }
SELECT id, subject, attachments FROM events

→ GET /events?$select=id,subject,attachments&$expand=attachments

$expand lässt sich mit $select kombinieren. Die Paginierung über @odata.nextLink bleibt unverändert: nur der erste Request wird aus der Spec gebaut; Folgeseiten nutzen die nextLink-URL der API.

Ohne expandParam wird kein Expand-Query-Parameter gesendet.

Pfad-Parameter in der Spec

Enthält der Entity-path Platzhalter wie {userId}, werden diese aus dem FILTER oder INSERT gebunden:

"path": "/users/{userId}/events",
"update": { "path": "/users/{userId}/events/{id}", "method": "PATCH" }
SELECT id, subject FROM user_calendar_events FILTER userId='GUID' AND startswith(subject,'Team')

INSERT INTO user_calendar_events ("userId", subject, "start_dateTime", "end_dateTime")
VALUES ('GUID', 'Meeting', '2026-07-01T10:00:00', '2026-07-01T11:00:00')

UPDATE user_calendar_events SET subject = 'Neu' FILTER userId='GUID' AND id=EVENT-ID

Pfad-Parameter (userId=…) stehen am Anfang des FILTER, getrennt durch AND vom OData-Teil ($filter) oder weiteren Pfad-Parametern (id=…).

Ohne filterParam werden restliche FILTER-Klauseln nach den Pfad-Parametern zu normalen Query-Parametern. Graph calendarView nutzt das für die Pflichtparameter startDateTime und endDateTime.

Weitergehende Spec-Features

Dieses Kapitel beschreibt gemeinsame Strukturen, tabellarische Zeilen und das Überschreiben von INSERT, UPDATE und DELETE.

Defaults und gemeinsame Strukturen

APIs mit vielen gleichartigen Tabellen (z. B. Lookup-Entities mit denselben Spalten) müssen Spaltenlisten und gemeinsame Einstellungen nicht in jeder Entity wiederholen.

  • defaults setzt Entity-Felder für alle Tabellen. Ein Eintrag in der Entity überschreibt den Default (ganzes Objekt, kein Merge verschachtelter Felder).
  • structures definiert benannte Spaltenlisten. Eine Entity verweist mit structure darauf, statt columns anzugeben. structure kann auch in defaults stehen, wenn fast alle Tabellen dieselbe Struktur haben.

Nach dem Laden hat jede Entity eine vollständige Spaltenliste. structures sind keine Tabellen und erscheinen nicht in system.table_list.

Regeln:

  • Eine Entity nennt entweder structure oder columns, nicht beides.
  • Unbekannter Strukturname oder fehlende Spalten führen zu einem Fehler beim Laden der Spec.
  • Bestehende Specs ohne defaults/structures bleiben gültig.

Vollständige Beispiele: spec-defaults-example.json, spec-defaults-example.yaml.

{
  "defaults": {
    "write": false,
    "pagination": { "type": "none" }
  },
  "structures": {
    "codeDescription": [
      { "name": "code", "type": "VARCHAR" },
      { "name": "description", "type": "VARCHAR" }
    ]
  },
  "entities": [
    {
      "name": "department",
      "description": "Department",
      "path": "/lookup/department",
      "structure": "codeDescription"
    },
    {
      "name": "currency",
      "description": "Currency",
      "path": "/lookup/currency",
      "structure": "codeDescription"
    },
    {
      "name": "budget",
      "description": "Budget",
      "path": "/lookup/budget",
      "write": true,
      "columns": [
        { "name": "id", "type": "VARCHAR", "primaryKey": true },
        { "name": "amount", "type": "DECIMAL" }
      ]
    }
  ]
}

department und currency erben write: false und die Spalten aus codeDescription. budget überschreibt write und definiert eigene columns.

Dieselbe Spec sieht in YAML so aus:

defaults:
  write: false
  pagination:
    type: none
structures:
  codeDescription:
    - name: code
      type: VARCHAR
    - name: description
      type: VARCHAR
entities:
  - name: department
    description: Department
    path: /lookup/department
    structure: codeDescription
  - name: currency
    description: Currency
    path: /lookup/currency
    structure: codeDescription

Tabellarische Zeilen (rowFormat)

Manche APIs liefern keine Objekte, sondern einen Spaltenkopf und Wertezeilen. Typisch ist eine Kennzahlentabelle: jede Abfrage hat eigene Dimensionen und eine eigene Reihenfolge, die letzte Spalte heißt je Abfrage anders, und alle Werte kommen als Strings. Eine Entity definiert die möglichen SQL-Spalten fest. Der Treiber liest pro Antwort columns und ordnet über den Namen zu. Dieselbe Entity bedient alle Abfragen über einen Pfad mit Platzhalter, zum Beispiel path: "/reports/{report}" und FILTER report='…'.

Eine Antwort sieht zum Beispiel so aus:

{
  "columns": ["Year", "Region", "Product", "Revenue"],
  "data": [
    ["2024", "North", "Widget", "1200"],
    ["2024", "South", "Widget", "800"]
  ]
}

columns ist der Spaltenkopf. Jedes Element von data ist eine Zeile in derselben Reihenfolge und mit derselben Länge. Der letzte Name ist die Kennzahl und wechselt je Abfrage. Bei Objekt-Zeilen stünde dort stattdessen {"Year": "2024", "Region": "North"}.

Ohne rowFormat, oder mit rowFormat: "object", bleibt das Lesen von Objekt-Zeilen über jsonPointer, readPath oder den Feldnamen. columnsPath, unknownColumns, sourceName, sourcePosition und sourceMeta sind dann ein Fehler beim Laden der Spec.

Beispiel

{
  "name": "metrics",
  "path": "/reports/{report}",
  "rowFormat": "tabular",
  "columnsPath": "/columns",
  "dataPath": "/data",
  "unknownColumns": "error",
  "pagination": { "type": "none" },
  "columns": [
    { "name": "report", "type": "VARCHAR(200)", "binding": "path", "required": true },
    { "name": "year", "type": "VARCHAR(4)", "sourceName": "Year" },
    { "name": "region", "type": "VARCHAR(50)", "sourceName": "Region" },
    { "name": "product", "type": "VARCHAR(50)", "sourceName": "Product" },
    { "name": "channel", "type": "VARCHAR(50)", "sourceName": "Channel" },
    { "name": "segment", "type": "VARCHAR(50)", "sourceName": "Segment" },
    { "name": "amount", "type": "VARCHAR(100)", "sourcePosition": -1 },
    { "name": "amount_name", "type": "VARCHAR(200)", "sourcePosition": -1, "sourceMeta": "columnName" }
  ]
}

Die Abfrage sales_by_region und die passende Antwort sehen zum Beispiel so aus:

SELECT report, year, region, product, channel, segment, amount, amount_name
FROM metrics
FILTER report='sales_by_region'
{
  "columns": ["Year", "Region", "Product", "Channel", "Segment", "Revenue"],
  "data": [
    ["2024", "North", "Widget", "Web", "Retail", "1200"]
  ]
}
report year region product channel segment amount amount_name
sales_by_region 2024 North Widget Web Retail 1200 Revenue

Segment steht im Kopf dieser Abfrage. Die Zuordnung läuft über den exakten Namen, nicht über die Position unter den Dimensionen. Fehlt einer dieser Namen in der Spec, bricht unknownColumns: "error" das SELECT ab, auch bei SELECT *. report kommt aus dem FILTER, nicht aus der Wertezeile. SELECT * liefert nur die Spalten der Spec.

Eine zweite Abfrage hat andere Dimensionen und eine andere Reihenfolge. Region kann fehlen, Product kann vor Channel stehen, die letzte Spalte heißt anders:

SELECT report, year, region, product, channel, segment, amount, amount_name
FROM metrics
FILTER report='units_by_product'
{
  "columns": ["Year", "Product", "Channel", "Units"],
  "data": [
    ["2024", "Widget", "Web", "40"]
  ]
}

Hier ist region NULL, ebenso segment, weil der Kopf sie nicht enthält. amount ist 40 und amount_name ist Units.

Der Platzhalter {query} wird pro Pfadsegment kodiert: Leerzeichen als %20, Umlaute als UTF-8. Bei pagination mit Typ none (das ist auch der Standard) führt ein SELECT genau ein GET aus.

Zuordnung

Feld Bedeutung
rowFormat object (Standard) oder tabular
columnsPath JSON Pointer auf ein String-Array. Pflicht bei tabular, muss mit / beginnen
dataPath JSON Pointer auf das Array der Wertezeilen, im Beispiel /data
unknownColumns error (Standard), warn oder ignore
sourceName Exakter Name im Kopf. Ohne Angabe gilt name. Schließt sourcePosition aus
sourcePosition Index im Kopf. 0 ist die erste Spalte, -1 die letzte
sourceMeta Nur columnName: jede Zeile bekommt den Kopfnamen an der Position, nicht den Zellwert

sourceName ist ein anderer Begriff als selectName. selectName geht bei gesetztem selectParam als $select an den Server. jsonPointer und readPath sind bei rowFormat: "tabular" verboten. Eine Spalte mit binding: "path" oder binding: "query" hat keine Position im Kopf; sourceName, sourcePosition und sourceMeta sind dort ein Spec-Fehler. Der gebundene Wert wird nach dem Lesen aus dem FILTER in die Zeile geschrieben, wie bei Pfad- und Query-Bindings.

Ablauf pro SELECT:

  1. Pfad-Platzhalter aus FILTER auflösen und ein GET ausführen.
  2. Den Kopf unter columnsPath lesen. Er muss ein Array von Strings sein. Doppelte Namen sind ein Fehler. Der Vergleich ist exakt, einschließlich Groß- und Kleinschreibung. Der Antwortkopf wird nicht normalisiert.
  3. Die Position bestimmen: sourcePosition, sonst sourceName, sonst name. Fehlt der Name im Kopf, ist der Spaltenwert in allen Zeilen NULL. Ein sourcePosition außerhalb des Kopfs ist ein Fehler.
  4. Jede Kopfposition behandeln, auf die keine Spec-Spalte zeigt (unknownColumns, siehe unten).
  5. dataPath lesen. Der Knoten muss ein Array sein. Ein fehlender oder falscher Pfad ist ein Fehler. Ein leeres Array ist ein leeres Ergebnis. Jede Zeile muss ein Array derselben Länge wie der Kopf sein. Die Meldung nennt die Zeilennummer ab 1.

Zwei Spec-Spalten dürfen dieselbe Position belegen. amount und amount_name nutzen beide -1: amount ist der Zellwert, amount_name der Name dieser Spalte im Kopf. Eine Kopfposition gilt als belegt, sobald mindestens eine Spec-Spalte darauf zeigt. Die Prüfung unbekannter Spalten verwendet alle Spalten der Entity, nicht die SELECT-Liste.

Unbekannte Spalten

unknownColumns Wirkung
error SQLException. Die Meldung nennt Entity, Bindungswerte und die unbekannten Spaltennamen
warn Dieselbe Meldung einmal auf stdout, Level warn. Sichtbar bei stdoutlog=warn oder stdoutlog=all
ignore Die Spalte wird übergangen

error ist der Standard, auch wenn das Attribut fehlt.

Werte

Ein JSON-Wert null wird als SQL-NULL zurückgegeben. Ein leerer String wird beim Übernehmen in die JDBC-Zeile ebenfalls zu NULL. Das gilt für Objektzeilen und für tabellarische Zeilen gleichermaßen. Steht in einer Zelle ein verschachteltes Objekt oder ein Array, bricht der Treiber mit einem Fehler ab. Zahlen und Booleans werden so in den JDBC-Wert umgewandelt wie bei Objektzeilen. Ein VARCHAR bleibt Text, auch bei Werten wie 1.5E-12. Die übrigen Spec-Typen verwenden dieselbe Umwandlung. Schlägt sie fehl, nennt die Fehlermeldung die Spalte und die Ursache.

Metadaten und ein Request pro Statement

SQL-Namen und JDBC-Metadaten kommen aus der Spec. Der Treiber legt keine Spalten aus den Überschriften der Antwort an. DatabaseMetaData.getColumns und SELECT … FROM system.column_list lesen nur die Spec und senden keinen Request an /reports/…. Ein OAuth-Token-Refresh ist ein eigener Aufruf am Auth-Server.

Ein Statement bleibt ein GET. Eine Schleife über mehrere Abfragen gehört nicht in den Treiber. Aktionsmodule mit Loop Query (Insert-Template, Remote, CallWebService) ersetzen ${…} und führen das Statement pro Zeile erneut aus. 45 Abfragen sind 45 Requests pro Lauf.

Nur lesbar

Eine Entity mit rowFormat: "tabular" unterstützt kein INSERT, UPDATE oder DELETE, auch wenn write nicht auf false steht.

Schreiben – Defaults und Overrides

Jede Entity braucht einen expliziten path in der Spec (REST-Endpunkt für GET {baseUrl}{path}). Der Treiber leitet ihn nicht aus dem SQL-Tabellennamen (name) ab — name und path können unterschiedlich sein (z. B. "name": "orders", "path": "/api/v2/order").

Derselbe path gilt auch für Schreiben. INSERT, UPDATE und DELETE sind standardmäßig eingeschaltet und der Treiber bildet sie so ab:

SQL Default HTTP
INSERT INTO … POST {path}
UPDATE … FILTER id=1 PUT {path}/{id}
DELETE … FILTER id=1 DELETE {path}/{id}

Im Standard ist Schreiben erlaubt. Die HTTP-Methode und das URL-Muster leiten sich aus dem Pfad ab: {path} für INSERT, {path}/{id} für UPDATE und DELETE.

{id} im Pfad wird durch den Wert aus FILTER param=wert ersetzt (z. B. FILTER id=1 → /posts/1).

Override bei abweichender API

Der Pfad und die Methode können zum Beispiel beim UPDATE überschrieben werden:

{
  "name": "posts",
  "path": "/posts",
  "update": { "path": "/v2/orders/{id}", "method": "PATCH" }
}

Schreiben deaktivieren

Das Schreiben kann so deaktiviert werden:

{
  "name": "users",
  "path": "/users",
  "write": false
}

Einzelne Operationen kann man getrennt abschalten: "insert": false, "update": false oder "delete": false.

Eine Entity mit rowFormat: "tabular" ist nur lesbar. INSERT, UPDATE und DELETE werden abgelehnt, auch wenn write nicht auf false steht. Siehe Tabellarische Zeilen.

System-Tabellen

Über das Schema system stehen virtuelle Tabellen zur Verfügung:

Tabelle Inhalt
table_list Alle Entities aus der Spec
column_list Spalten aller Entities
pk_list Primary Keys aus der Spec

table_list, column_list, pk_list und DatabaseMetaData.getColumns lesen nur die Spec. Sie senden keinen HTTP-Request an die API. Ein OAuth-Token-Refresh geht an den Auth-Server, nicht an den Datenpfad der Entity.

SELECT table_name, table_type, remarks
FROM system.table_list
WHERE table_name = 'users'

SELECT column_name, type_name, column_size, decimal_digits
FROM system.column_list
WHERE table_name = 'users'

SELECT table_name, column_name, key_seq
FROM system.pk_list
WHERE table_name = 'users'

Fehlersuche

Zum Debuggen von Verbindungs- und Abfrageproblemen schreibt man die gewünschten Stufen in die Connection Property stdoutlog, zum Beispiel:

stdoutlog=cursor+http+rows
Level Ausgabe
cursor Geöffnete Tabelle und FILTER-Text
http HTTP-Methode und vollständige URL inkl. Query-Parameter; Redirects als 307 redirect … -> …
rows Anzahl empfangener Zeilen pro HTTP-Antwort und Gesamtzahl beim Schließen des Cursors
warn Warnungen, einmal pro Antwort. Unbekannte Kopfspalten bei unknownColumns: "warn"
all alle Level

Die folgenden Meldungen treten häufig auf:

Meldung Ursache / Lösung
FILTER could not be mapped to query parameters FILTER-Syntax prüfen: param=wert mit AND, Strings in '…'
has both "structure" and "columns" Pro Entity nur structure oder columns angeben
unknown structure / neither "columns" nor "structure" Strukturnamen prüfen bzw. columns oder structure setzen
HTTP 401 / HTTP 403 Auth prüfen (z. B. Webmetic: auth=apikey, Header Authorization ohne Bearer)
HTTP 429 requestintervalms setzen (Webmetic: 1000), siehe Rate Limits
leeres Resultset trotz gültiger URL oft unbehandelter Redirect; mit stdoutlog=http prüfen. Power BI preferClientRouting=true löst 307 aus — der Treiber folgt dem (siehe HTTP-Redirects)
Too many HTTP redirects Redirect-Schleife oder mehr als 5 Hops
Property wird nicht erkannt Property-Namen sind case-insensitive; Werte prüfen
Unknown columns for entity Der Antwortkopf enthält einen Namen, den keine Spec-Spalte belegt. Spalte ergänzen oder unknownColumns auf warn bzw. ignore setzen. Siehe Tabellarische Zeilen
INSERT is not supported / UPDATE is not supported / DELETE is not supported Die Entity ist nicht schreibbar, zum Beispiel bei rowFormat: "tabular" oder write: false

Kompatible REST-APIs

Übersicht bekannter APIs, die sich per Spec-Datei anbinden lassen:

Beispiele

Schritt-für-Schritt-Anleitungen für konkrete REST-APIs:

  • JSONPlaceholder — öffentliche Test-API ohne Authentifizierung
  • GitHub — GitHub-API mit Bearer-Token
  • REST Countries — Länderdaten (nur Lesen, Paginierung, jsonPointer)
  • OpenWeatherMap — Wettervorhersage mit API-Schlüssel und FILTER q=…
  • Microsoft Graph — Microsoft 365 (OAuth2, OData-FILTER, nextLink, $select, $expand, Kalenderansicht)
  • Webmetic — B2B-Firmenstammdaten (wem_company), Paginierung und Rate Limiting (requestintervalms=1000)

Anmerkungen

Verschachteltes JSON

Felder wie "address": { "city": "Berlin" } werden nicht automatisch zu Spalte address.city. Nur Top-Level-Felder jedes Array-Elements sind Spalten. Ohne weiteren Eintrag landet address als JSON-Text in einer Spalte.

Mit jsonPointer wird ein verschachteltes Feld zur eigenen Spalte. Der Pointer beginnt am Zeilenobjekt:

{ "name": "city", "type": "VARCHAR", "jsonPointer": "/address/city" }

SELECT city FROM users liest dann Berlin. Weitere verschachtelte Felder stehen im Kapitel jsonPointer.

Fehlerbehandlung

Alle HTTP-Fehler (4xx, 5xx) führen zu einer SQLException mit diesem Statuscode und dem Response-Body.

UPDATE und PUT

Bei UPDATE enthält der JSON-Body nur die Spalten aus SET. Das gilt für PUT und PATCH. FILTER füllt die URL. PUT ist der Standard, weil viele APIs Änderungen nur über dieses Verb anbieten und nicht genannte Felder stehen lassen.

Erwartet der Server bei PUT das vollständige Objekt und prüft die Pflichtfelder, lehnt er den Aufruf ab. Der Treiber gibt Status und Response-Body weiter. Für UPDATE users SET email = 'ada@example.com' FILTER id=1 kann die SQLException so aussehen:

HTTP 400 for PUT https://api.example.com/users/1: {"message":"name is required"}

Der Text nach dem Doppelpunkt stammt von der API. Ein anderer Server antwortet mit 200 und ersetzt die Ressource durch das Teilobjekt. Name und weitere Felder sind danach leer. Das zeigt sich erst beim nächsten SELECT.

Führt die API Teiländerungen über PATCH, setzt man in der Spec "update": { "method": "PATCH" }. Der Body bleibt derselbe.