Der RestJDBC-Treiber ermöglicht das Lesen und Schreiben von Daten aus bzw. in REST-APIs über JDBC mit SQL.
Es gelten folgende Konventionen:
jdbc:rest: verwendetjdbc:rest:https://…)baseUrl in der Spec als Fallback gesetzt werden, wenn die JDBC-URL keine HTTP-URL enthältpublic verwendet (Standard)users, orders)jsonPointer)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 | |
|---|---|---|
| 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.
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:
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
@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: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.
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(...).
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
SELECT id, name, email
FROM users
Diese Abfrage führt der Treiber als GET {baseUrl}/users aus.
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 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.
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.
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).
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.
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.
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" }
]
}
]
}
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
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:
Alle weiteren Einträge sind optionale Ergänzungen.
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.
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.
varchar(255) = VARCHAR(255)).DECIMAL(p) oder NUMERIC(p) die Scale, gilt 0. Die Scale darf die Precision nicht überschreiten.type oder ein unbekannter Typ ohne Klammern wird als VARCHAR geladen.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)" }
]
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.
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".
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").
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.
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 → basictoken gesetzt → bearerapikey gesetzt → apikeykeystore gesetzt → clientcertnoneoauth2 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.
props.setProperty("auth", "none");
Alternativ setzt man keine Auth-Properties. Dann erkennt der Treiber das Verfahren none.
props.setProperty("auth", "bearer");
props.setProperty("token", "eyJhbGciOiJIUzI1NiIs...");
Der Treiber sendet den Header Authorization: Bearer <token>.
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.
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.
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.
props.setProperty("auth", "basic");
props.setProperty("user", "api-user");
props.setProperty("password", "geheim");
Der Treiber sendet den Header Authorization: Basic <base64(user:password)>.
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).
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.
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
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.
| 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.
none)Die API liefert alle Datensätze in einem Request:
"pagination": { "type": "none" }
SELECT id, name FROM users
Das ergibt einen Aufruf: GET /users.
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.
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.
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).
Siehe JSONPlaceholder — dort u. a. Paginierung mit _limit/_page und die vollständige Spec.
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.
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.
Dieses Kapitel beschreibt FILTER, Sortierung, die Spaltenauswahl über selectParam, verwandte Ressourcen über expandParam und Pfad-Parameter in der Spec.
Die FILTER-Klausel leitet Filter an die REST-API weiter — serverseitig, nicht lokal wie WHERE. Das Verhalten hängt von der Spec ab:
param=wertOhne 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
ANDOhne 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.
filterParam: nativer API-AusdruckIst 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.
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.
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.
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:
jsonPointer gesetzt istselectName, falls angegebenjsonPointer (z. B. /from/emailAddress/address → from)jsonPointer: "" tragen nicht zu $select bei; steht nur eine solche Spalte im SELECT, entfällt $select (volles Objekt)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).
expandParam)Ist expandParam gesetzt (z. B. "$expand" für OData), kann der Treiber beim SELECT verschachtelte bzw. verwandte Ressourcen anfordern. Die Werte stammen aus:
expand — wird immer gesendet (String oder Liste)expand — wird gesendet, wenn die Spalte in der SELECT-Liste stehtSELECT * / keine Spaltenliste — alle Spalten-expand-Werte plus der Entity-DefaultMehrere 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.
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.
Dieses Kapitel beschreibt gemeinsame Strukturen, tabellarische Zeilen und das Überschreiben von INSERT, UPDATE und DELETE.
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:
structure oder columns, nicht beides.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
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.
{
"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.
| 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:
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.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.unknownColumns, siehe unten).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.
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.
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.
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.
Eine Entity mit rowFormat: "tabular" unterstützt kein INSERT, UPDATE oder DELETE, auch wenn write nicht auf false steht.
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).
Der Pfad und die Methode können zum Beispiel beim UPDATE überschrieben werden:
{
"name": "posts",
"path": "/posts",
"update": { "path": "/v2/orders/{id}", "method": "PATCH" }
}
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.
Ü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'
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 |
Übersicht bekannter APIs, die sich per Spec-Datei anbinden lassen:
Schritt-für-Schritt-Anleitungen für konkrete REST-APIs:
jsonPointer)FILTER q=…FILTER, nextLink, $select, $expand, Kalenderansicht)wem_company), Paginierung und Rate Limiting (requestintervalms=1000)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.
Alle HTTP-Fehler (4xx, 5xx) führen zu einer SQLException mit diesem Statuscode und dem Response-Body.
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.