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

Features

Die Festlegung der Details einer REST API erfolgt über 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.

Sechs Authentifizierungsverfahren stehen zur Verfügung:

  • none - keine Authentifizierung
  • basic - HTTP Basic-Auth mit User und Passwort
  • bearer - Authentifizierung mit Bearer Token
  • 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.

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);

Connection Properties

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

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

Authentifizierung

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

Die Property auth legt das Verfahren fest: none, bearer, basic, 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

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");

Oder einfach keine Auth-Properties setzen.

Bearer Token

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

Der Treiber sendet: Authorization: Bearer <token>

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");

clientsecret ist optional (Public Client). offline_access ermöglicht Token-Erneuerung per Refresh Token.

Siehe auch: Microsoft Graph-Beispiel.

HTTP Basic

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

Der Treiber sendet: 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(...)).

Kombiniert mit Bearer-Token:

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.

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.

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)

Beispieldatei: 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)

Beispieldatei: spec-example.yaml

Gleicher Inhalt wie oben — Inhalt und Feldnamen sind identisch:

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, 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
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

Es ist

  • 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.

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:

  • 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": "abteilung",
      "description": "Board Abteilung",
      "path": "/schema/Entities/Abteilung",
      "structure": "codeDescription"
    },
    {
      "name": "currency",
      "description": "Board Currency",
      "path": "/schema/Entities/Currency",
      "structure": "codeDescription"
    },
    {
      "name": "forecast",
      "description": "Board Forecast",
      "path": "/schema/Entities/Forecast",
      "write": true,
      "columns": [
        { "name": "id", "type": "VARCHAR", "primaryKey": true },
        { "name": "amount", "type": "DECIMAL" }
      ]
    }
  ]
}

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

Dasselbe in YAML:

defaults:
  write: false
  pagination:
    type: none
structures:
  codeDescription:
    - name: code
      type: VARCHAR
    - name: description
      type: VARCHAR
entities:
  - name: abteilung
    description: Board Abteilung
    path: /schema/Entities/Abteilung
    structure: codeDescription
  - name: currency
    description: Board Currency
    path: /schema/Entities/Currency
    structure: codeDescription

Spalten-Typen

Unterstützte Typen in der Spec: BIGINT, INTEGER, BOOLEAN, DOUBLE, TIMESTAMP, DATE, JSON sowie die unten beschriebenen Zeichen- und Numerik-Typen.

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.

SQL-ähnliche Längen- und Precision-Angaben

Für VARCHAR, CHAR, VARBINARY und DECIMAL kann die Länge bzw. Precision direkt im type-Feld in SQL-Notation angegeben werden. Die Angaben erscheinen in den JDBC-Metadaten (type_name, column_size, decimal_digits) und in system.column_list.

Typ Syntax Beispiel Metadaten
VARCHAR VARCHAR oder VARCHAR(n) "type": "VARCHAR(255)" column_size = n
CHAR CHAR oder CHAR(n) "type": "CHAR(10)" column_size = n
VARBINARY VARBINARY oder VARBINARY(n) "type": "VARBINARY(64)" column_size = n
DECIMAL DECIMAL, DECIMAL(p) oder DECIMAL(p,s) "type": "DECIMAL(10,2)" column_size = p, decimal_digits = s
NUMERIC Alias für DECIMAL "type": "NUMERIC(18)" wie DECIMAL

Hinweise:

  • Groß-/Kleinschreibung ist egal (varchar(255) = VARCHAR(255)).
  • DECIMAL ohne Klammern wird wie bisher als DOUBLE behandelt (Abwärtskompatibilität). Mit Klammern (DECIMAL(p) bzw. DECIMAL(p,s)) wird der JDBC-Typ DECIMAL verwendet.
  • Längen und Precision werden zur Laufzeit nicht erzwungen; sie dienen der Metadaten-Beschreibung für nachgelagerte Tools.
  • Ungültige Angaben (z. B. VARCHAR(0), DECIMAL(2,5), INTEGER(10)) führen beim Laden der Spec zu einem Fehler.

Beispiel:

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

Optional pro Spalte (für verschachteltes JSON):

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

Ohne jsonPointer gilt der Spaltenname als Top-Level-JSON-Feld (bisheriges Verhalten).

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. VARCHAR reicht; JSON ist hier nicht nötig.

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');

Request-Body:

{
  "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 (z. B. Teilnehmerlisten) Typ JSON und JSON-Literal in SQL:

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

Beim SELECT werden Werte von denselben Pfaden gelesen. Spaltennamen mit Unterstrich in SQL quoten ("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 – Antwort ist direkt ein JSON-Array (Standard, dataPath weglassen oder ""):

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

Verschachtelt unter data (dataPath: "/data"):

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

Verschachtelt unter items (dataPath: "/items"):

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

Es 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").

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;

→ ein Aufruf: GET /users

Offset-Paginierung

Spec, 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

Spec, wenn die API limit und page (ab Seite 1) erwartet:

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

Gleiche 5 Datensätze, Seitengröße 2:

# 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"}]

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.

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}

Wie der Treiber im Standard funktioniert: dass Schreiben ist erlaubt, die HTTP-Methode und das URL-Muster leiten sich aus dem Pfad ab ({path} für INSERT, {path}/{id} für UPDATE/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
}

Das geht auch einzeln: "insert": false, "update": false, "delete": false.

SQL-Syntax

Grundlegende SELECT-Abfrage

SELECT id, name, email
FROM users

Das entspricht: GET {baseUrl}/users.

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 ist case-insensitive. String-Werte in einfachen Anführungszeichen setzen. 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')"

Oder mehrere Werte:

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

Expand auf Spaltenebene (nur wenn die Spalte selektiert ist):

{ "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.

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).

INSERT

INSERT schreibt neue Datensätze über die REST-API. Im Standard führt dies zu einem POST auf den Entity-path. Dieser kann bei Bedarf überschrieben werden durch:

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

Entspricht: POST {baseUrl}/posts mit JSON-Body aus den Spalten/Werten.

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}. Dies kann in der Spec mit PATCH überschrieben werden:

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

Entspricht: PUT {baseUrl}/posts/1 (bzw. PATCH bei Override).

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}). Auch dieser Pfad kann in der Spec überschrieben werden:

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

Der Wert aus id=1 ersetzt {id} im Pfad → DELETE {baseUrl}/posts/1.

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.

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
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:

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
all alle Level

Typische Fehler:

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

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.

Fehlerbehandlung

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