HTTP: eigener Vertrag (Referenz)
Der eigene Vertrag des Dienstes unter /api/v1/ – lesen, schreiben, Prozeduren aufrufen, eigene
Anweisungen ausführen – für Programme, die nicht OData sprechen. Clients, die OData v4 sprechen
(Access über den Treiber, Excel, Power BI), verwenden die OData-Oberfläche.
Syntax
| Methode | Pfad | Bedeutung |
|---|---|---|
GET | /api/v1/cf/{alias}/{art}/{name}.json | lesen; Art: tables, views oder functions |
POST | /api/v1/cf/{alias}/procedures/{name}.json | JSON-Rumpf mit benannten Argumenten |
POST | /api/v1/cf/{alias}/sql.json | Rumpf: eigene Anweisung (freizuschalten) |
GET | /api/v1/cf/{alias}/tables/{name}.json?{schluessel}={wert} | einen Satz lesen |
POST | /api/v1/cf/{alias}/tables/{name}.json | anlegen; Rumpf: Felder; Antwort 201 + Satz |
PATCH | /api/v1/cf/{alias}/tables/{name}.json?{schluessel}={wert} | ändern; Rumpf: Felder; Antwort 200 + Satz |
DELETE | /api/v1/cf/{alias}/tables/{name}.json?{schluessel}={wert} | löschen; Antwort 204 |
Ohne Anmeldung, für Überwachung und Verteilung:
| Methode | Pfad | Bedeutung |
|---|---|---|
GET | /health/live | Prozess lebt (200, Text „Healthy“) |
GET | /health/ready | 200; „Healthy“ mit mindestens einem Alias, sonst „Degraded“ (auch dann 200) |
GET | /api/v1/health | {"status":"ok","configuredAliases":<Zahl>} |
GET | /download/ | Installationspakete (siehe unten) |
Mit Windows-Anmeldung, für den WKL Bootstrapper:
| Methode | Pfad | Bedeutung |
|---|---|---|
GET | /schema-state/{alias}/{produkt} | Schemastand einer versionierten Datenbank (siehe unten) |
Parameter
Die Endung entscheidet die Darstellung: .json oder .xml, an jeder dieser Adressen und für
Erfolg wie Fehler. Siehe Darstellungen.
Angemeldet wird mit jedem Verfahren, das die Konfiguration freischaltet: Dienstschlüssel in der
Kopfzeile X-Api-Key, persönlicher Schlüssel (in derselben Kopfzeile oder als Kennwort von HTTP Basic),
Windows-Konto (Negotiate) oder SQL-Anmeldung (HTTP Basic). Alle Verfahren, ihre Antworten und der
zweite Faktor stehen in HTTP: Anmeldung; für diesen Vertrag gelten sie unverändert.
Schreiben in Tabellen
Schreiben ist so dynamisch wie Lesen: Methode, Tabelle, Schlüssel und Felder kommen aus der Anfrage, das SQL erzeugt der Dienst. Der Aufrufer schickt dabei kein SQL. Er bestimmt, welcher Satz (über den Schlüssel) und welche Werte (über die Felder), nie eine Anweisung, eine Bedingung oder eine Spalte, die der Katalog nicht kennt.
PATCH /api/v1/cf/fachdaten/tables/APPARAT.json?nummer=4711
X-Api-Key: …
If-Match: "AAAAAAAQgpE="
Content-Type: application/json
{ "benennung": "Kühler Nord", "preis": 12.50 }
Der Rumpf einer ändernden Anfrage trägt Content-Type: application/json, eine hochgeladene Datei
application/octet-stream. Einen Rumpf ohne diese Angabe oder mit einem Typ, den eine Webseite ohne Rückfrage
senden darf (text/plain, application/x-www-form-urlencoded, multipart/form-data), beantwortet der Dienst mit
400 und dem Code BAD_REQUEST, bevor er ihn liest. Ausgenommen sind die Abfrageoptionen eines Lesezugriffs in
$query, die als text/plain kommen.
Grundsätze und Begründung: Schreiben und Schreibkonflikte.
| Regel | Warum |
|---|---|
PATCH setzt nur die genannten Felder | Die übrigen behalten ihren Wert; niemand setzt fünfzig Felder auf den Stand einer älteren Kopie zurück |
PUT wird nicht angeboten (405) | „Satz ersetzen“ müsste jede nicht genannte Spalte leeren |
| Der Schlüssel ist nicht änderbar | Der Satz wanderte unter seiner eigenen Adresse weg |
| Identitäts-, berechnete und Versionsspalten sind nicht schreibbar | Die setzt die Datenbank |
| Unbekannte oder doppelt geschriebene Felder werden abgewiesen | Welches von zwei Schreibweisen gewönne, hinge an der Reihenfolge im Rumpf |
| Beim Anlegen müssen alle Pflichtfelder und der Schlüssel dastehen | Außer ein Schlüssel, den die Datenbank vergibt – als Identität oder per Vorgabe (NEWSEQUENTIALID(), NEWID(), Sequenz); den vergebenen Wert liest die Anweisung beim Einfügen mit (OUTPUT … INTO, auch bei Tabellen mit Triggern), der gespeicherte Satz wird über ihn zurückgelesen |
Wie Werte gebunden werden: Zeitpunkte, Zahlen und Namen.
| Antwort | Bedeutung |
|---|---|
201 / 200 + Satz | angelegt / geändert; der Satz ist der gespeicherte Stand |
204 | gelöscht |
400 | Schlüssel unvollständig, Feld unbekannt oder nicht schreibbar, Wert passt nicht zum Typ |
404 | kein Satz mit diesem Schlüssel |
409 | Schlüssel beim Anlegen schon vergeben |
412 | Zeilenversion veraltet |
422 | von der Datenbank abgelehnt (Pflichtwert, Fremdschlüssel, Prüfbedingung); die Meldungen von SQL Server stehen in details (siehe „Meldungen von SQL Server“ unter Fehlerform); löst ein Trigger eine eigene Meldung aus, steht ihr Text in message (siehe „Ausnahme: Meldungen der Anwendung“) |
Zum Schreiben braucht ein Aufrufer eine Regel mit der Ressourcenart tables und den Verben
POST, PATCH, DELETE. Das Nur-Lese-Kennzeichen sperrt alle drei.
Abfrageoptionen beim Lesen: $select, $filter, $orderby, $top, $skip, $count.
Alles davon wird zu SQL übersetzt und von der Datenbank ausgewertet.
Was $filter annimmt
| Bestandteil | Angenommene Schreibweisen |
|---|---|
| Vergleiche | eq ne gt ge lt le, Prüfung auf fehlenden Wert mit eq null / ne null |
| Feld gegen Feld | ParentId eq Id, Stufe lt Grenze: rechts ein Feldname statt eines Werts; beide Felder müssen zur Menge gehören, ein leeres Feld trifft weder den Vergleich noch seine Verneinung. Text gegen Zahl lehnt SQL Server ab (422) – seit Vertrag 0.4.0 (GatewayFilter.FieldComparison) |
| Verknüpfungen | and, or, not, Klammern |
| Textsuche | startswith(feld,'x'), endswith(feld,'x'), contains(feld,'x'), substringof('x',feld) |
| Zahlen | 5, 12.5, mit Breitenangabe 5L, 12.5m, 12.5f, 12.5d |
| Zeitpunkte | datetime'2023-01-01T00:00:00', datetimeoffset'…' sowie ohne Typnamen 2023-01-01T00:00:00Z |
Kennungen (uniqueidentifier) | guid'6f9619ff-8b86-d011-b42d-00cf4fc964ff' sowie ohne Typnamen 6f9619ff-8b86-d011-b42d-00cf4fc964ff (so schreibt sie der ODBC-Treiber) |
| Weitere Typen | time'…', binary'…' |
Beide Schreibweisen des Protokolls werden angenommen, weil die Verbraucher nicht einer
Generation angehören: substringof und datetime'…' stammen aus der älteren Version,
contains, der Zeitpunkt und die Kennung ohne Typnamen aus der neueren. Welche davon ankommt, entscheidet
der Client und nicht die Anfrage.
Ein abgewiesenes Literal ist teurer als es aussieht. Ein Tabellenkalkulations- oder Berichtsclient meldet die
400nicht weiter – er lässt die Bedingung fallen und liest stattdessen das ganze Objekt. Aus einer gezielten Abfrage wird so ein Durchzug über die ganze Tabelle, der bei großen Tabellen abbrechen kann. Deshalb sind die Literale des Protokolls Teil des Vertrags und keine Bequemlichkeit.
Welche Abfrageoptionen bedient werden
Bedient: $select, $filter, $orderby, $top, $skip, $count, $inlinecount,
$format (nur json). Die Gesamtzahl wird in beiden Schreibweisen erfragt – $count=true und
$inlinecount=allpages –, weil die ältere und die neuere Version des Protokolls sie
verschieden nennen und der Client entscheidet, welche ankommt.
Abgewiesen mit 400:
| Abgewiesen | Meldung |
|---|---|
$expand, $apply, $search, $compute, $skiptoken, $levels, $ref, $value, $id | „wird von diesem Dienst nicht ausgeliefert“ ($skiptoken und $apply nimmt nur die OData-Oberfläche an, siehe Folgeseiten und Gruppieren dort) |
jede andere Angabe mit $ | „ist keine bekannte Abfrageoption“ |
ein Wert, der weder true noch false ist ($count=yes) | „erwartet 'true' oder 'false'“ |
eine andere Darstellung als JSON ($format=xml) | „wird nicht ausgeliefert“ |
Eine übergangene Option ist schlimmer als eine abgewiesene. Eine Option ist eine Angabe darüber, welche Zeilen oder Spalten gewollt sind; sie fallen zu lassen erzeugt keinen Fehler, sondern ein anderes Ergebnis, das wie eine Antwort aussieht. Ohne diese Prüfung würde
$fillter=Typ eq 'S'– ein Buchstabe zu viel – mit200und dem ganzen Objekt beantwortet statt mit den gesuchten Zeilen, und nichts in der Antwort sagte das.
Die Unterscheidung zwischen „nicht ausgeliefert“ und „unbekannt“ ist gewollt: Wer $expand
schickt, hat etwas Richtiges geschrieben, das hier nicht bedient wird; wer $fillter
schickt, hat sich vertippt. Beide brauchen eine andere Korrektur, und eine gemeinsame
Meldung schickte den ersten auf die Suche nach einem Tippfehler, den es nicht gibt.
Antwortform
{ "data": [ … ], "rowCount": 100, "count": 100, "window": 100, "totalCount": 250, "nextLink": "/api/v1/cf/fachdaten/views/liste.json?$skip=100&wkl-page=…", "executionTimeMs": 0 }
| Feld | Bedeutung |
|---|---|
rowCount | Zeilen in dieser Antwort. Immer vorhanden, nie negativ |
count | Spiegel von rowCount. Ältere Schreibweise des Vorgängerdienstes; sie bleibt, weil bestehende Aufrufer sie lesen |
window | Fenstergröße, mit der tatsächlich gelesen wurde. Bei einem Prozeduraufruf nicht vorhanden |
totalCount | Zeilen, die derselbe Filter insgesamt trifft. Nur bei $count=true vorhanden |
affectedRowCount | Zeilen, die ein Prozeduraufruf geändert hat – nur wenn er kein Ergebnis lieferte |
truncated | Nur vorhanden, wenn die Zeilenobergrenze griff |
nextLink | Verweis auf das nächste Fenster, pfadabsolut. Nur vorhanden, wenn der Dienst das Fenster gewählt und gefüllt hat – am Ende fehlt das Feld |
windowundnextLinksind die Ergänzungen gegenüber den älteren Versionen. Ohnewindowkann ein Aufrufer ein volles Fenster nicht von einem kurzen unterscheiden – und genau daran erkennt ein Aufrufer ohne$count, dass er am Ende angekommen ist. Die gewünschte Größe genügt dafür nicht: sie wird auf die konfigurierte Obergrenze begrenzt, und wer keine nennt, bekommt die konfigurierte Voreinstellung, die er nicht kennt. Ein Verbraucher, der das Feld nicht kennt, übergeht es.
nextLinksagt dasselbe in der Form, auf die ein Werkzeug ohne Kenntnis dieses Vertrags von selbst reagiert: einen Verweis, dem es folgt, bis er fehlt.windowist eine Abmachung, die nur ein eigens dafür geschriebener Aufrufer auswertet – jeder andere hört nach dem ersten Fenster auf und hält es für das ganze Ergebnis.
Wann ein Folgeverweis angeboten wird
| Anfrage | Antwort |
|---|---|
kein $top, Fenster voll | Verweis mit $skip + Fenstergröße, ohne $top |
$top größer als MaxTop, Fenster voll | Verweis mit dem Rest als $top – wer 25 000 verlangt, bekommt genau 25 000, verteilt auf mehrere Fenster |
$top nicht größer als MaxTop | kein Verweis – der Aufrufer hat bekommen, was er verlangt hat |
| Fenster kurz oder leer | kein Verweis – das Ergebnis ist zu Ende |
Alle anderen Optionen – Filter, Projektion, Sortierung, Funktionsargumente – übernimmt der
Verweis unverändert, damit jedes Fenster ein Fenster desselben Ergebnisses ist. Nur die
Zählung ($count, $inlinecount) entfällt: sie ist für alle Fenster eines Lesevorgangs
dieselbe, und sie je Fenster zu wiederholen wiederholt den teuersten Teil für eine bekannte
Antwort. Ein volles Fenster beweist nicht, dass weitere Zeilen existieren; endet das Ergebnis
genau an seiner Kante, führt der Verweis auf ein leeres Fenster. Das ist eine richtige Antwort
und billiger als eine Zählung bei jedem Lesen.
Der Verweis ist pfadabsolut (/api/v1/cf/…?…) und nennt keinen Host: hinter einem
vorgeschalteten Proxy ist der Host, den dieser Prozess sieht, nicht der, den der Aufrufer
benutzt hat.
totalCountfehlt, wenn niemand gezählt hat. Das ist nicht dasselbe wie0. Wer die Unterscheidung zu einer Null vereinfacht, sagt einer Liste, es gebe nichts zu laden – über nachweislich vorhandenen Daten und ohne Fehlermeldung an irgendeiner Stelle.
Fehlerform
Angaben „seit Vertrag 0.x“ nennen die Version des Vertragspakets Wkl.Data.Gateway.Contracts, ab der
eine Fehlerart oder ein Feld erkannt wird; ohne Angabe gilt sie seit 0.1.0. Ein Programm, das eine
ältere Version festhält, liest die neueren Arten mit ihrem Statuscode, wie unten je Art beschrieben.
{ "error": { "code": "NOT_FOUND", "message": "…", "correlationId": "…" } }
Codes: BAD_REQUEST (400), UNAUTHENTICATED (401), FORBIDDEN (403), NOT_FOUND (404),
CONFLICT (409), PRECONDITION_FAILED (412), LIMIT_EXCEEDED (429), DATABASE_REJECTED
(422), UNAVAILABLE (503), DATABASE_UNREADABLE (503), TIMEOUT (504), INTERNAL (500).
UNAVAILABLE steht nicht nur für eine unerreichbare Datenbank, sondern für jeden unerwarteten
Fehler im Dienst. Nach einer Schreibanfrage (POST, PATCH, DELETE, Abschluss einer
Transaktion) heißt 503 deshalb: Ob geschrieben wurde, ist unbekannt. Eine Antwort 5xx wird für
Idempotency-Key nicht gespeichert; eine Wiederholung schreibt dann womöglich ein zweites Mal. Den
Satz vorher neu lesen.
LIMIT_EXCEEDED steht für jede Grenze des Dienstes: Anfragen je Zeitraum, Fehlversuche, Anzahl
gebundener Werte, Größe des Rumpfs (Limits:MaxBodyBytes), offene Transaktionen. Für einen zu großen
Rumpf weicht der Vertrag damit von RFC 9110, Abschnitt 15.5.14
ab, der 413 Content Too Large vorsieht: Ein Aufrufer soll alle Grenzen an einer Art erkennen. Ob
Warten hilft (Ratenbegrenzung) oder nur eine kleinere Anfrage (Rumpf, Werte), sagt message.
TIMEOUT (seit Vertrag 0.3.0; vorher UNAVAILABLE) heißt: Die Anweisung lief und überschritt Limits:QueryTimeoutSeconds. Die Datenbank
war erreichbar; ein Datenbanktreiber meldet das als Zeitüberschreitung (SQLSTATE HYT00), nicht
als verlorene Verbindung. TIMEOUT und UNAVAILABLE gelten als vorübergehend (IsTransient).
DATABASE_UNREADABLE (seit Vertrag 0.6.0; vorher UNAVAILABLE) heißt: SQL Server war erreichbar, kann die Datenbank des Alias aber nicht lesen.
Die Dateien der Datenbank lassen sich nicht lesen (823, 824, 605), oder die Datenbank ist
offline (942), als verdächtig markiert (926), ihre Dateien sind nicht zu öffnen (945), sie wird
noch wiederhergestellt (922) oder ihr Protokoll fehlt (9001). message nennt den Alias, etwa
„Die Datenbank der Datenquelle 'lager' ist auf dem Server derzeit nicht lesbar. Bitte den Betreuer
verstaendigen.“; Dateinamen und Nummer stehen nur im Protokoll, details fehlt. Sie gilt nicht als vorübergehend:
Wiederholen hilft erst, wenn jemand am Server handelt. Ein Aufrufer, der den Code nicht kennt, liest
den Statuscode 503 und behandelt sie wie UNAVAILABLE. Der .NET-Client Wkl.Data.Gateway.Client
meldet sie ab Version 0.2.0 als GatewayProblemKind.DatabaseUnreadable (nicht vorübergehend); die
Version 0.1.0 kennt den Code nicht und meldet GatewayProblemKind.Unavailable, also „vorübergehend“. Eine 503 bedeutet in keinem Fall „keine Daten“. Umgekehrt belegt das Fehlen des
Codes keine lesbare Datenbank: Er steht nur für die oben genannten Fehlernummern; ein Lesefehler aus
einem anderen Grund kommt als UNAVAILABLE.
Nach einem kurzen Verlust des Laufwerks unter den Datenbankdateien bleibt das so, bis der Dienst „SQL Server“ auf dem Datenbankserver neu gestartet wird; Anweisungen, deren Seiten noch im Speicher von SQL Server liegen, gelingen in dieser Zeit weiter.
Eine unbekannte Ressource und eine für den Aufrufer nicht lesbare Ressource antworten
beide mit NOT_FOUND. Ausnahmen: Ein lesbares Objekt, das der Aufrufer so nicht ändern darf,
antwortet FORBIDDEN; eine geprüfte Anweisung antwortet für unbekannte wie für verborgene Objekte
gleich mit FORBIDDEN. Die Unterscheidung würde den Dienst zum Verzeichnis machen: ein
Aufrufer könnte durch Vergleich der Antworten erfahren, welche Objekte existieren.
Was eine Fehlerantwort nie enthält: Datenbankname, Verbindungszeichenfolge,
Anweisungstext, den Namen eines Objekts, das der Aufrufer nicht sehen darf. Diese Angaben stehen
im Protokoll, auffindbar über die correlationId.
Meldungen von SQL Server (details, seit Vertrag 0.5.0). Lehnt die Datenbank eine Anweisung ab, trägt die Antwort
die Meldungen von SQL Server, in der Reihenfolge, in der SQL Server sie meldet –
wie eine direkte Verbindung über den Treiber von SQL Server, damit Code auf die Nummer verzweigen kann
(etwa 2627 „Schlüssel vergeben“ gegen 547 „Bezug verletzt“):
{ "error": { "code": "DATABASE_REJECTED", "message": "Der Vorgang wurde von der Datenbank abgelehnt.",
"correlationId": "…",
"details": [ { "code": "547", "sqlState": "23000", "severity": 16, "released": true,
"message": "Die INSERT-Anweisung steht in Konflikt mit der CHECK-Einschränkung …" } ] } }
| Feld | Inhalt |
|---|---|
code | Meldungsnummer von SQL Server, als Text (OData-Form von details) |
sqlState | SQLSTATE, den der Treiber von SQL Server für diese Nummer meldet: 23000 Einschränkung, Schlüssel, Pflichtwert; 22001 Wert zu lang; 22018 Umwandlung; 22007/22008 Datum oder Zeit; 22003 Überlauf; 22012 Division durch null; 42S02/42S22 Objekt bzw. Spalte unbekannt; 25000 Transaktionszustand; 40001 Deadlock; 01000 Hinweis; sonst 42000 |
severity | Schwere: bis 10 ein Hinweis, der die Ablehnung begleitet, ab 11 ein Fehler |
released | true: message ist der Text von SQL Server. false: der Text nennt ein Objekt, das der Aufrufer nicht sehen darf; message ist dann ein Text des Dienstes, Nummer und SQLSTATE stimmen trotzdem |
message | Text, wie oben beschrieben |
details trägt höchstens zehn Meldungen. Die Nummer erhält der Aufrufer für jede davon. Den Text nur, wenn er keinen Namen enthält, den der Aufrufer
nicht sehen darf: keine Tabelle, Sicht, Routine, Einschränkung, keinen Trigger und keinen Index eines
Objekts, das seine Regeln nicht erreichen, und weder die Anmeldung des Alias noch den Servernamen.
Geprüft wird der Name als ganzes Wort, unabhängig von Anführungszeichen; so greift die Prüfung auch
in deutschen Meldungen, die Namen ohne Anführung nennen („in der lager-Datenbank“). Der Name der
Datenbank wird durch den Alias ersetzt. Ein verborgener Name, der zufällig ein gewöhnliches Wort ist,
hält einen harmlosen Text zurück; die Nummer kommt trotzdem an.
Meldungen mit einem Wert. Umwandlungsfehler (245, 8114, Datumswerte 241 und 242), ein
doppelter Schlüssel (2627, 2601) und ein abgeschnittener Wert (2628, 8152) nennen einen Wert („… beim
Konvertieren des Werts 'X' …“, „Der doppelte Schlüsselwert ist (X)“). Ob X die Eingabe des Aufrufers
war oder aus den Daten gelesen wurde, ist von außen nicht zu unterscheiden: Der Filter des Aufrufers,
eine eigene Anweisung (INSERT … SELECT, UPDATE … FROM, MERGE), eine Prozedur oder ein Trigger
kann einen gelesenen Wert gezielt in eine eindeutige oder zu kurze Spalte bringen. Läuft die
Anweisung als Anmeldung des Alias, hält der Dienst den Text dieser Meldungen deshalb immer zurück
(released false); Nummer, SQLSTATE und Statuscode (409 beim vergebenen Schlüssel) kommen an.
Grund: Blendet die Datenbank Zeilen je Person aus (Sicherheit auf Zeilenebene über den
SESSION_CONTEXT, den der Dienst setzt), kann SQL Server solche Ausdrücke vor oder neben der
Sicherheitsbedingung auswerten, und der Text verriete einen Wert aus einer ausgeblendeten Zeile;
unter der Anmeldung des Alias ist die Prüfung des Dienstes die einzige Sicherung. Läuft die Anweisung
als die Person (Identitätswechsel oder eigene SQL-Anmeldung), entscheidet SQL Server selbst, was sie
lesen darf, und der Text kommt wie über eine direkte Verbindung. Die Regel hängt an der Identität,
nicht an der Form der Anweisung.
details fehlt bei einer verlorenen Verbindung (503), einer nicht lesbaren Datenbank (503
DATABASE_UNREADABLE) und einer Zeitüberschreitung (504): Das
sind Aussagen über die Verbindung, nicht Antworten der Datenbank auf die Anweisung. message und
Statuscode bleiben unverändert.
Ausnahme: Meldungen der Anwendung. Löst die Anwendung in der Datenbank selbst eine Meldung aus –
THROW mit eigener Nummer oder RAISERROR mit einem Text, also Fehlernummer ab 50000 und Schwere
11 bis 16 –, steht deren Text als message in der Antwort (422 DATABASE_REJECTED), auf jedem Weg:
Prozeduraufruf, Schreiben in Tabellen (auch aus einem Trigger), OData und Durchreiche. Eine solche
Meldung ist für den Menschen geschrieben, der den Vorgang ausgelöst hat, und eine direkte Verbindung
zeigte sie ihm ebenso. Was sie nennt, entscheidet, wer sie schreibt. Meldungen von SQL Server selbst
(Nummer unter 50000) stehen nur in details; message bleibt dann der Text des Dienstes.
Prozeduren aufrufen
Eine gespeicherte Prozedur wird mit POST auf procedures/{name} aufgerufen; der Rumpf ist ein
JSON-Objekt mit den Argumenten nach Namen.
POST /api/v1/cf/fachdaten/procedures/auftrag_anlegen.json
X-Api-Key: …
Idempotency-Key: 6c1e0c7a-…
Content-Type: application/json
{ "kunde_id": 4711, "betreff": "Kühler Nord", "stand": "2026-10-10T17:00:00.123+02:00", "notiz": null }
Der Dienst führt daraus SET XACT_ABORT ON; EXEC [schema].[auftrag_anlegen] @kunde_id = @p0, …
aus.
| Regel | Warum |
|---|---|
Argumentnamen ohne @; ein Name mit @ wird als ungültiger Bezeichner abgewiesen (400) | Das Präfix setzt der Dienst. Ein mitgeschicktes gäbe @@name, ein Name nur aus @ einen namenlosen Parameter – beides scheiterte erst in der Datenbank mit einer Meldung über die Prozedur statt über die Anfrage |
| Aufruf nach Namen; ein weggelassenes Argument nimmt den Vorgabewert der Prozedur | Ein Aufruf nach Position kann einen optionalen Parameter nicht überspringen |
JSON-null wird NULL | Ein fehlender Wert muss sich von einem leeren Text unterscheiden; als Abfrageargument käme ein optionaler Text als '' an und überschriebe einen Wert, der bleiben sollte |
| Eine Prozedur ohne Parameter wird ohne Rumpf aufgerufen | Das ist der gewöhnliche Aufruf, kein Fehler |
Ein Objekt oder eine Liste als Wert wird abgewiesen (400) | Kein Prozedurparameter nimmt es an; als Text eingeebnet bekäme die Prozedur etwas, das sie nie deklariert hat |
Keine Transaktion des Dienstes um den Aufruf; SET XACT_ABORT ON davor | Die Prozedur führt ihre eigene. Eine äußere Transaktion würde verschachteln: ein inneres ROLLBACK verwürfe auch die äußere, ein inneres COMMIT zählte nur herunter. XACT_ABORT rollt bei einem Laufzeitfehler zurück, statt eine offene Transaktion mit ihren Sperren in den Verbindungspool zu geben |
Ein Aufruf ist genau ein EXEC | Was mehrere Schritte braucht, ist eine Prozedur mit mehreren Schritten in einer Transaktion; zwei Aufrufe hintereinander ließen bei einem Fehler dazwischen einen halben Stand zurück |
Typabbildung der Argumente. Gebunden wird ohne Typangabe; SQL Server wandelt den Wert in den Typ des Parameters.
| JSON-Wert | Gebunden als |
|---|---|
| Text | nvarchar |
Ganzzahl (4711) | bigint |
Dezimalzahl ohne Exponent (12.50) | decimal, exakt |
Zahl mit Exponent (1E-300) | float |
true / false | bit |
null | NULL |
Folge für Zeitpunkte: Es gibt kein JSON-Datum. Ein Zeitpunkt geht als Text, etwa ISO 8601
(2026-10-10T17:00:00.123+02:00), und SQL Server wandelt ihn in den Typ des Parameters –
auch in einen varchar-Parameter, den die Prozedur selbst umwandelt (CONVERT(…, 127)).
Antwortform.
{ "data": [ { "ergebnis": "angelegt", "auftrag_id": 815, "stand": "2026-10-10T17:00:00.123+02:00" } ], "rowCount": 1, "count": 1, "executionTimeMs": 12 }
| Feld | Bedeutung |
|---|---|
data | Die erste Ergebnismenge der Prozedur, als Liste von Objekten (Spaltenname → Wert). Weitere Ergebnismengen werden nicht gelesen. Ohne Ergebnismenge eine leere Liste |
rowCount, count | Zeilen in data |
affectedRowCount | Nur wenn die Prozedur keine Ergebnismenge lieferte: die Zahl der geänderten Zeilen; data ist dann eine leere Liste |
truncated | Die Zeilenobergrenze des Alias hat gegriffen |
executionTimeMs | Laufzeit im Dienst |
Es gibt kein Fenster und keinen nextLink: Eine Prozedur liefert die Ergebnismenge, für die sie
geschrieben ist, und der Aufrufer kann keinen Teil davon verlangen. Werte der Ergebnismenge
stehen in derselben Form wie beim Lesen; ein datetimeoffset mit seinem Versatz.
Keine OUTPUT-Parameter, kein RETURN-Wert auf diesem Weg. Diese Adresse liest weder das eine
noch das andere, und sie liest nur die erste Ergebnismenge. Was der Aufrufer erfahren soll – die
vergebene Kennung, der neue Stand, ein Ergebniswort –, gibt die Prozedur hier als Ergebnismenge zurück
(SELECT … AS ergebnis, … AS id). Wer Ausgabeparameter, den Rückgabewert oder alle Ergebnismengen
braucht, ruft die Prozedur als Anweisung auf (EXEC @rueckgabe = …), siehe
Ausgabeparameter, mehrere Ergebnismengen, Beschreiben.
Fehler.
| Ursache in der Prozedur | Antwort |
|---|---|
THROW mit eigener Nummer, RAISERROR mit Text, ab Schwere 11 | 422 DATABASE_REJECTED, message ist der Text der Meldung |
| Ein Fehler von SQL Server selbst (etwa Division durch null, Fremdschlüssel) | 422 DATABASE_REJECTED, Text „Der Vorgang wurde von der Datenbank abgelehnt.“ |
RAISERROR bis Schwere 10 (Hinweis) | Kein Fehler: 200 mit dem Ergebnis; der Hinweistext geht verloren |
Primärschlüssel oder eindeutiger Index verletzt (2627, 2601) | 409 CONFLICT |
Zeitlimit des Dienstes überschritten (Limits:QueryTimeoutSeconds) | 504 TIMEOUT |
| Datenbank nicht erreichbar | 503 UNAVAILABLE |
Datenbank auf dem Server nicht lesbar (823, offline, verdächtig) | 503 DATABASE_UNREADABLE |
Fehlende Berechtigung (229, 230, 262, 297, 300, 916) | 403 FORBIDDEN, wenn die Anweisung als Person läuft (Identitätswechsel oder SQL-Anmeldung); sonst 422 – dann fehlt die Berechtigung der Anmeldung des Alias, und das ist eine Lücke in der Einrichtung, nicht beim Aufrufer |
Seit Vertrag 0.5.0 stehen Nummer, SQLSTATE und Schwere jeder Meldung in details, der Text nur, wenn
der Dienst ihn freigibt (siehe Fehlerform); vorher stand nur der Text der
Anwendungsmeldung in message. Ein Aufrufer, der je nach Grund verschieden handeln soll, bekommt den
Grund trotzdem besser als Ergebniszeile (ergebnis = 'gesperrt'): Eine Nummer der Anwendung ab 50000
ist für jeden Grund frei wählbar, aber nur eine Abmachung zwischen Prozedur und Aufrufer. Und eine Meldung unter
Schwere 11 ist ein Hinweis, kein Fehler: Sie bricht nichts ab und erscheint nirgends in der Antwort.
Rechte. Ein Aufrufer braucht eine Regel mit der Ressourcenart procedures und dem Verb POST;
das Nur-Lese-Kennzeichen sperrt den Aufruf, denn der Dienst kann nicht wissen, ob eine Prozedur
schreibt. Eine Prozedur, die der Katalog des Alias nicht kennt oder der Aufrufer nicht aufrufen
darf, antwortet mit 404. Wiederholungen nach einem Verbindungsabbruch: Der Idempotency-Key
gilt auch hier, siehe Wiederholte Schreibanfragen.
Lange Abfragen: Optionen im Rumpf
Die Abfrageoptionen einer Leseanfrage stehen in der Adresse, und deren Länge begrenzt der Host vor
dem Dienst: IIS nimmt in der Vorgabe höchstens 2 048 Bytes Abfragezeichenfolge an
(requestFiltering/requestLimits/@maxQueryString, Microsoft:
Request Limits),
Kestrel 8 KB Anforderungszeile (KestrelServerLimits.MaxRequestLineSize). Die IIS-Vorlage des
Installationspakets hebt die Grenze auf 8 192 Zeichen an. Eine Leseanfrage darf ihre Optionen deshalb auch im Rumpf tragen – in zwei
Formen, auf beiden Oberflächen:
POST /odata/v4/fachdaten/dbo/Artikel/$query Content-Type: text/plain POST /odata/v4/fachdaten/dbo/Artikel/$count/$query POST /api/v1/cf/fachdaten/tables/artikel.json/$query QUERY /odata/v4/fachdaten/dbo/Artikel (ebenso an den übrigen Adressen) $filter=Nummer%20eq%201%20or%20Nummer%20eq%202&$select=Nummer,Bezeichnung&$count=true
Der Rumpf ist genau der Text, der sonst hinter dem ? stünde. POST …/$query ist die Form, die
OData 4.01 dafür vorsieht (OData Version 4.01 Part 1: Protocol,
Abschnitt „Passing Query Options in the Request Body“), und kommt durch jeden Proxy, der POST
durchlässt. QUERY ist die Methode für sichere Leseanfragen mit Rumpf aus dem Entwurf der IETF
„The HTTP QUERY Method“ (draft-ietf-httpbis-safe-method-w-body); als Entwurf nimmt der Dienst sie
zusätzlich an, ohne sich auf seine endgültige Form festzulegen. Einen Rumpf an GET nimmt der Dienst
bewusst nicht an: Nach RFC 9110, Abschnitt 9.3.1
hat er dort keine festgelegte Bedeutung, Proxys verwerfen ihn oder weisen die Anfrage ab, und ein
Zwischenspeicher, der nach der Adresse speichert, gäbe die Antwort eines Filters für einen anderen
aus.
| Regel | Grund |
|---|---|
| Geprüft, begrenzt und protokolliert wird als Lesen | Ein Aufrufer mit reinem Leserecht darf es; beurteilt nach der Methode, bräuchte er das Recht zu schreiben |
Der Rumpf wird erst nach Anmeldung und Freigabe gelesen, begrenzt durch MaxBodyBytes | Einen Rumpf zu lesen ist Arbeit; eine unberechtigte Anfrage soll sie nicht auslösen |
Optionen in Adresse und Rumpf: 400 | Welcher von zwei $filter gälte, könnte niemand beim Lesen der Anfrage sagen |
Nur text/plain (oder application/x-www-form-urlencoded) | JSON ist eine andere Form der Anfrage; ihn als Optionen zu lesen, hieße ihn falsch zu lesen |
Die Folgeseite zeigt auf die Ressource, nicht auf $query | Ein Client folgt ihr mit GET; ist sie selbst zu lang, schickt er sie wieder als $query |
Ab der Übernahme sind beide Formen derselbe Lesevorgang: derselbe Übersetzer, dieselben Grenzen, dieselbe Seitenbildung. Wie viele Werte eine Bedingung nennen darf, hängt deshalb nicht am Weg, sondern an der Form der Bedingung; siehe Lange Wertelisten.
Lange Wertelisten
Eine Bedingung wie „eine dieser Nummern“ kommt als Kette von Gleichheiten an:
Nummer eq 1 or Nummer eq 2 or …. Die Filtersprache von OData 4.0 kennt kein in (erst 4.01 führt es
ein, URL Conventions 4.01);
der ODBC-Treiber schreibt ein IN (…) aus Access deshalb so, und der typisierte Client schreibt
eine Mengenprüfung ebenso. Einzeln gebunden wäre jeder Wert ein eigener Parameter, und davon
nimmt SQL Server in einer Anweisung höchstens 2 098 an: Ein Aufruf trägt 2 100 Parameter, zwei
davon belegt sp_executesql selbst.
Der typisierte Client lässt in einer Mengenprüfung höchstens 20 000 Werte zu (seit Vertrag 0.2.0, vorher 1 000); die Grenze prüft das Vertragspaket, bevor die Anfrage den Prozess verlässt.
Der Dienst fasst eine solche Kette deshalb zusammen. Mehr als 100 Gleichheiten auf derselben
Spalte innerhalb eines or belegen einen einzigen Parameter, gleich wie viele es sind: Sie
reisen als ein XML-Dokument. Die Anweisung liest das Dokument zuerst in eine Variable und
vergleicht die Spalte mit den Werten daraus, jeden gelesen in dem Typ, den sein Parameter gehabt
hätte.
| Regel | Grund |
|---|---|
| Bis 100 Werte bleibt jeder Wert ein eigener Parameter | Der Optimierer sieht dann die Werte selbst, und die kurzen Schlüssellisten, mit denen eine Anwendung ihre Sätze nachliest, bleiben unverändert (Access fragt je zehn Schlüssel ab) |
| XML, nicht JSON und keine Tabellenwertübergabe | XML liest die Engine auf jeder Kompatibilitätsstufe. OPENJSON und STRING_SPLIT verlangen Stufe 130, eine Tabellenwertübergabe einen Typ, der erst in der Datenbank angelegt werden müsste |
| Das Dokument wird einmal in eine Variable gelesen | In der Bedingung selbst wandelte die Engine die Liste für jede geprüfte Zeile neu um; bei langen Listen wüchse die Laufzeit mit der Zahl der Werte mal der Zahl der Zeilen |
| Die Liste trifft genau die Sätze, die dieselben Werte als Parameter träfen | Jeder Wert steht in einer Form im Dokument, die die Engine verlustfrei zurückliest (Tabelle unten) |
| Kann eine Liste das nicht leisten, wird Wert für Wert gebunden | Gemischte Arten von Werten, ein Zeitpunkt außerhalb des Bereichs von datetime für eine solche Spalte, eine Uhrzeit außerhalb eines Tages, Text mit Zeichen, die XML nicht tragen kann, Dezimalwerte, deren längster ganzzahliger Teil und größte Skala zusammen mehr als 38 Stellen ergeben. Dann gilt wieder die Grenze von 2 098 Werten |
| Die Liste wird ausdrücklich als frei von fehlenden Werten ausgewiesen | Bei NOT IN muss die Engine sonst für jede Zeile prüfen, ob die Liste einen fehlenden Wert enthält – dann träfe die Bedingung keine Zeile. Mit dem Hinweis entfällt diese Prüfung, und NOT IN über eine lange Liste bleibt schnell |
Gleichheiten unter einem and werden nicht zusammengefasst | (a eq 1 and b eq 2) or … – etwa die Paare eines zusammengesetzten Schlüssels – ist keine Liste einer Spalte |
Verlustfrei je Typ. Die Liste trifft in jeder Spalte dieselben Sätze wie die einzeln gebundenen Werte, auch bei Dubletten, leeren und nur aus Leerzeichen bestehenden Texten, Zeilenumbrüchen und Zeichen außerhalb der Basisebene von Unicode.
| Typ | Form im Dokument | Warum diese Form |
|---|---|---|
| Ganzzahlen | Ziffern, gelesen als bigint | – |
decimal, numeric | Ziffern, gelesen als decimal(38, s) mit der größten Skala der Liste | Beide Vergleiche sind exakt |
float, real | ganzzahlige Mantisse und Zweierpotenz | Als Text liest die Engine manche Werte als Nachbarwert (subnormale und große ganzzahlige Werte); in dieser Form weicht keiner ab |
datetime, smalldatetime | vorab auf 1/300 s gerundet, wie der Treiber einen Parameter rundet | Ungerundet träfen manche Werte nicht dieselben Sätze |
datetime2, date, time, datetimeoffset | mit sieben Nachkommastellen | – |
| Text | Zeilenenden und Tabulatoren als Zeichenreferenz; ein leerer Wert als Element ohne Text | Ein wörtlicher Wagenrücklauf würde beim Lesen des Dokuments zum Zeilenvorschub. Verglichen wird unter der Sortierfolge der Spalte, mit wie ohne Unterscheidung von Groß- und Kleinschreibung |
| Binärwerte | Base64 | – |
bit, uniqueidentifier | gewöhnliche Schreibweise | – |
Was die Länge einer Liste begrenzt: der Rumpf der Anfrage. Die Voreinstellung von
1 MB für MaxBodyBytes fasst als Richtwert rund 36 000 Werte bei kurzem Spaltennamen und kurzen
Zahlenwerten; längere Spaltennamen und Werte verkürzen die Liste entsprechend. Die Grenze von 2 098 Einzelwerten bleibt für alles, was
keine Liste ist – viele verschiedene Spalten, Vergleiche mit gt oder lt, Textsuche und
Ketten von höchstens 100 Werten je Spalte, die zusammen mehr ergeben. Eine solche Anfrage wird
mit 429 („Die Anfrage bindet zu viele Einzelwerte.“) abgewiesen, bevor sie die Datenbank
erreicht.
Darstellungen: JSON und XML
Jede Adresse des eigenen Vertrags endet auf .json oder .xml; die Endung entscheidet, keine
Accept-Kopfzeile. Das ist Absicht: Mehrere Tabellenkalkulations- und Berichtswerkzeuge
bieten keine Möglichkeit, eine Kopfzeile zu setzen, aber jedes kann auf eine Adresse zeigen.
Eine Endung, die keine der beiden Darstellungen nennt, wird mit 400 abgewiesen und nicht
stillschweigend als JSON beantwortet.
Erfolg und Fehler kommen in der angeforderten Darstellung. Ein Client, der XML anfragt und im Fehlerfall ein JSON-Objekt bekäme, kann genau die Antwort nicht lesen, die er am dringendsten braucht.
Die XML-Form folgt einer festen Zeilenkonvention, weil JSON und XML nicht symmetrisch sind: Eine Liste hat keinen eigenen Elementnamen, denn ihre Einträge tragen keinen Schlüssel.
<?xml version="1.0" encoding="utf-8"?> <results xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <data> <row> <nummer>4711</nummer> <GeändertAm>2026-09-22T21:16:00</GeändertAm> <bemerkung xsi:nil="true" /> <field name="Menge (kg)">12.5</field> </row> </data> <rowCount>1</rowCount> </results>
| Regel | Warum |
|---|---|
Jeder Eintrag einer Liste wird <row> | Ein Listeneintrag hat keinen Schlüssel, aus dem ein Elementname werden könnte |
| Ein Spaltenname wird zum Element | Auch mit Umlaut: gültige XML-Namen sind nicht auf ASCII beschränkt |
Ein Name, der kein XML-Name ist, wird <field name="…"> | Leerzeichen, Klammern, führende Ziffer. Weder umbenannt noch weggelassen – der Aufrufer muss die Spalte unter ihrem echten Namen finden. In einer Anfrage nennt er sie kodiert, siehe Zeitpunkte, Zahlen und Namen |
Ein fehlender Wert ist ein leeres Element mit xsi:nil="true" | Ein leeres Element allein ist von der leeren Zeichenkette nicht zu unterscheiden, und bei einer nullbaren Textspalte sind das zwei verschiedene Tatsachen |
| Werte sind kulturunabhängig und wie in JSON | Dezimaltrennzeichen Punkt, Zeitpunkt nach ISO 8601, Binärwert als Base64 |
| Zeichen, die XML verbietet (Steuerzeichen außer Tab und Zeilenwechsel), lassen sich nicht darstellen | Die Antwort scheitert dann mit 503; solche Daten über JSON lesen |
Eigene Anweisungen (Durchreiche)
Freizuschalten, siehe die eine Ausnahme.
Ein Aufrufer ohne das Recht sql, dessen Regeln den Alias nennen, erreicht dieselbe Adresse mit
geprüften Anweisungen; dann gilt zusätzlich der Abschnitt Geprüfte Anweisungen.
Der Rumpf trägt die Anweisung und, wo sie Platzhalter hat, deren Werte:
POST /api/v1/cf/fachdaten/sql.json
X-Api-Key: …
{
"sql": "SELECT b.name, COUNT(*) AS anzahl FROM apparat a JOIN betrieb b ON b.id = a.betrieb WHERE a.stand = @stand GROUP BY b.name",
"parameters": { "stand": 3 }
}
Werte reisen als Parameter, nie im Text. Das ist hier keine Schutzmaßnahme – der Text
gehört ohnehin dem Aufrufer –, aber es verhindert, dass ein Wert als Syntax gelesen wird, und
genau das macht aus einem Datum oder einer Dezimalzahl je nach Spracheinstellung der Sitzung
einen anderen Wert. Parameternamen sind gewöhnliche Bezeichner und werden ohne @ geschickt.
Die Antwort hat die gewohnte Erfolgsform, ergänzt um columns:
{
"columns": [
{ "name": "name", "type": "Edm.String", "storeType": "nvarchar", "nullable": true, "maxLength": 50 },
{ "name": "anzahl", "type": "Edm.Int32", "storeType": "int", "nullable": true }
],
"data": [ { "name": "Halle 1", "anzahl": 42 } ],
"rowCount": 1,
"executionTimeMs": 12
}
columns gibt es nur hier, und das ist kein Beiwerk: Über ein Objekt des Katalogs kann ein
Client die Form vorher im Schemadokument nachlesen, über eine selbst geschriebene Anweisung
entsteht sie erst beim Ausführen. Ein Client, der nur die Zeilen bekäme, müsste die Typen aus
den Werten erraten, die zufällig in der ersten Zeile stehen – und ein leeres Ergebnis hätte
dann gar keine Form. Deshalb steht columns auch dann in der Antwort, wenn die Anweisung
kein Ergebnis liefert: als leere Liste, die aussagt „keine Spalten“, statt eines fehlenden
Feldes, das nichts aussagt. Die Typnamen sind dieselben wie im Schemadokument der
OData-Oberfläche, mit zwei Unterschieden: Werte von datetime, datetime2 und smalldatetime stehen
als Wanduhrzeit ohne Z oder Versatz, obwohl der Typ Edm.DateTimeOffset heißt; eine Spalte
eines nicht angebotenen Typs trägt "type": null. storeType nennt den Typnamen der Datenbank,
wie Store.Type im Schemadokument: Am Protokolltyp allein erkennt ein Client nicht, dass ein
Edm.Binary eine geography enthält oder ein Edm.String einen sql_variant. Eine Spalte,
deren Wert die Datenbank vergibt, trägt zusätzlich "identity": true; SQL Server meldet das für
eine Identitätsspalte, die unverändert aus einer Tabelle stammt. Zeilen tragen ihre Werte unter dem
Namen der Spalte; eine Spalte ohne Namen oder mit einem schon vergebenen Namen bekommt deshalb einen
eigenen (Column2, Nummer1) und trägt den Namen der Datenbank in label: leer für einen Ausdruck
ohne Namen, den wiederholten Namen für die zweite Spalte dieses Namens. ODBC Driver 18 zeigt beide
so, wie die Datenbank sie meldet. Ein xml-Wert steht in der Form, in der ihn SqlClient schreibt –
ein leeres Element als <r /> statt <r/>; das Dokument ist dasselbe.
Weiteres zur Form:
| Feld | Bedeutung auf diesem Pfad |
|---|---|
data | Das erste Ergebnis mit Spalten. Zähler, die ein Auslöser vorher ausgibt, sind nicht die Antwort auf die gestellte Frage. Weitere Ergebnisse stehen in results, siehe unten |
affectedRowCount | Summe der geänderten Zeilen über alle Anweisungen des Textes; fehlt, wenn die Anweisung keinen solchen Zähler erzeugt |
truncated | Die Zeilenobergrenze des Alias hat gegriffen; die Antwort ist gekürzt |
Jede Spalte hat einen eigenen Namen. Eine Spalte ohne Namen (SELECT MAX(a), MIN(b)) heißt
Column mit ihrer Position, wie in einer DataTable; ein doppelter Name (SELECT a.id, b.id)
bekommt die kleinste Zahl, die ihn eindeutig macht (id1). Die Zeile trägt ihre Werte unter
dem Namen; ohne eindeutige Namen kämen mehrere Spalten als ein Wert an.
Zeit- und Zeilenobergrenze kommen aus Limits wie überall sonst. Der Schlüssel, gegen den
eine Regel unter Limits:Objects geprüft wird, lautet hier {alias}/sql/ – ein Muster wie
fachdaten/sql/* setzt also die Grenzen dieses Pfades, ohne die der Tabellen anzurühren.
Das ist die Stelle, an der eine lange Auswertung mehr Zeit bekommt als ein Datenblatt.
Geprüfte Anweisungen
Für einen Aufrufer ohne das Recht sql nimmt der Dienst an derselben Adresse eine Anweisung an,
wenn der Alias geprüfte Anweisungen zulässt (AllowCheckedStatements, Vorgabe an) und eine Regel
des Aufrufers den Alias nennt. Der Dienst zerlegt die Anweisung, prüft jedes genannte Objekt gegen
die Regeln des Aufrufers und führt eine neu geschriebene Fassung aus, in der jedes Objekt mit seinem
Schema genannt ist. Was zulässig ist, steht unter
Geprüfte Anweisungen.
Rumpf und Antwort sind dieselben wie oben; zusätzlich gilt:
| Fall | Antwort |
|---|---|
| Der Text ist kein T-SQL | 400 mit Zeile und Spalte der Fundstelle |
Die Trennzeile GO, eine Schemaänderung, dynamisches SQL, ein Name mit Datenbank oder Server, eine nicht zulässige Funktion oder Form | 403; die Meldung nennt die Form, bei mehreren Anweisungen auch die Nummer der betroffenen (Anweisung 2 von 3: …) |
| Ein Objekt ist unbekannt oder für das, was die Anweisung damit tut, nicht freigegeben | 403; die Meldung nennt das Objekt, wie es geschrieben wurde, und den Zugriff. Unbekannt und verboten werden gleich beantwortet |
Im Audit-Strom steht die ausgeführte, neu geschriebene Fassung.
Ausgabeparameter, mehrere Ergebnismengen, Beschreiben
Drei Zusätze an derselben Adresse decken ab, was eine ODBC-Anwendung bei Microsofts Treiber gewohnt
ist: Ausgabe- und Rückgabeparameter, SQLMoreResults und die Spalten einer vorbereiteten Anweisung
vor dem Ausführen. Sie gelten für eigene wie für geprüfte Anweisungen gleich; eine Datenquelle, deren
Provider sie nicht kann, antwortet mit 400, bevor sie die Datenbank anspricht.
Ausgabeparameter (outputs). Der Rumpf nennt je Ausgabeparameter seinen Edm-Typ; bei Text und
Binärdaten wahlweise mit Länge:
POST /api/v1/cf/fachdaten/sql.json
X-Api-Key: …
{
"sql": "EXEC @rueckgabe = dbo.auftrag_anlegen @kunde = @kunde, @nummer = @nummer OUTPUT",
"parameters": { "kunde": 4711 },
"outputs": { "rueckgabe": "Edm.Int32", "nummer": { "type": "Edm.String", "maxLength": 20 } }
}
| Regel | Warum |
|---|---|
Zulässige Typen: Edm.Boolean, Byte, Int16, Int32, Int64, Decimal, Single, Double, Date, TimeOfDay, DateTimeOffset, Guid, Binary, String; ein anderer Name → 400 | Ein Ausgabeparameter braucht seinen Typ, bevor die Anweisung läuft; SQL Server kann ihn nicht nachträglich melden |
Ohne Länge: Text als nvarchar(max), Binärdaten als varbinary(max); Edm.Decimal als decimal(38,10); Edm.DateTimeOffset als datetime2 | Weite Text- und Binärtypen schneiden nichts ab. Verluste: Ein Dezimalwert mit mehr als zehn Nachkommastellen wird gerundet, ein datetimeoffset verliert seinen Versatz |
Steht derselbe Name auch in parameters, ist der Parameter Ein- und Ausgabe | So bindet Microsofts Treiber einen INPUT_OUTPUT-Parameter |
Namen ohne @, gewöhnliche Bezeichner | Wie bei parameters |
Die Antwort trägt die Werte nach Namen, null für NULL:
{ "columns": [], "data": [], "rowCount": 0, "outputs": { "rueckgabe": 0, "nummer": "A-2026-0815" }, "executionTimeMs": 9 }
outputs steht nur in der Antwort, wenn die Anfrage outputs nannte. SQL Server schickt die Werte
nach der letzten Ergebnismenge; der Dienst liest deshalb alle Mengen zu Ende, bevor er sie einträgt.
Mehrere Ergebnismengen (results). Liefert eine Anweisung mehr als eine Ergebnismenge mit
Spalten, steht die erste wie bisher in columns/data, jede weitere in dieser Reihenfolge in
results:
{
"columns": [ { "name": "id", "type": "Edm.Int32", "storeType": "int", "nullable": false } ],
"data": [ { "id": 1 } ],
"rowCount": 1,
"results": [
{ "columns": [ { "name": "name", "type": "Edm.String", "storeType": "nvarchar", "nullable": true, "maxLength": 50 } ],
"data": [ { "name": "Halle 1" } ], "rowCount": 1 }
],
"executionTimeMs": 11
}
Ein Client, der nur eine Menge kennt, liest dieselbe Antwort wie vorher. Die Zeilenobergrenze gilt je
Menge; eine gekürzte Menge trägt "truncated": true. Ergebnisse ohne Spalten (Zähler einer Änderung)
erscheinen nicht als eigene Menge.
Beschreiben ohne Ausführen (describe). Mit "describe": true führt der Dienst nichts aus und
antwortet mit den Spalten der ersten Ergebnismenge:
{ "sql": "SELECT id, name FROM dbo.betrieb WHERE id = @id", "parameters": { "id": 1 }, "describe": true }
{ "columns": [ { "name": "id", "type": "Edm.Int32", "storeType": "int", "nullable": false }, { "name": "name", "type": "Edm.String", "storeType": "nvarchar", "nullable": true, "maxLength": 50 } ], "data": [], "rowCount": 0, "executionTimeMs": 3 }
Die Beschreibung läuft unter derselben Anmeldung und denselben Prüfungen wie die Ausführung; ein
Aufrufer erfährt also nichts über Objekte, die er nicht ausführen dürfte. Die Werte in parameters
bestimmen nur die Typen der Platzhalter. Eine Anweisung, die eine Zeile einfügt, fügt mit describe
keine ein.
Wiederholte Schreibanfragen (Idempotency-Key)
Ein Client auf einer schlechten Verbindung schickt eine Änderung, der Dienst speichert sie, und die
Antwort geht auf dem Rückweg verloren. Der Client kann das nicht von einer Anfrage unterscheiden,
die nie ankam. Wiederholt er sie, entstünde ein zweiter Satz – oder ein Schreibkonflikt (412)
für eine Änderung, die durchging, weil sich die Zeilenversion inzwischen bewegt hat.
Trägt eine Schreibanfrage (POST, PATCH, DELETE) die Kopfzeile Idempotency-Key (bis
128 sichtbare ASCII-Zeichen, vom Client zufällig gewählt), beantwortet der Dienst jede
Wiederholung mit der gespeicherten ersten Antwort und der Kopfzeile Idempotent-Replayed: true,
statt ein zweites Mal zu schreiben. Der ODBC-Treiber schickt sie bei jedem Schreiben mit. Die
Kopfzeile wirkt bei jeder Methode außer GET und HEAD und auf jeder Adresse, auch beim
Prozeduraufruf und bei eigenen Anweisungen.
| Fall | Antwort |
|---|---|
| Derselbe Schlüssel, dieselbe Methode, Adresse, Anmeldung und derselbe Rumpf | Gespeicherte Antwort (Status, Rumpf, ETag, Location) |
| Wiederholung, während die erste noch läuft | Wartet auf die erste |
| Derselbe Schlüssel mit anderem Rumpf | 422 |
Erste Antwort 401, 429 oder 5xx | Nicht gespeichert; die Wiederholung läuft wirklich – nach 5xx womöglich ein zweites Mal |
Gespeichert werden Erfolg sowie Ablehnungen aus Gründen der Anfrage (400, 403, 404, 409,
412, 422), 10 Minuten lang. Auch ein 422 aus einem THROW der Prozedur zählt dazu: Eine
Wiederholung mit demselben Schlüssel bekommt die gespeicherte Antwort (mit derselben
correlationId), auch wenn der Grund in der Datenbank inzwischen behoben ist; ein neuer Versuch
braucht einen neuen Schlüssel. Gespeichert wird nur im Speicher des Prozesses: Ein Neustart beendet
das Fenster. Grenzen: Antworten über 1 MiB werden nicht gespeichert, höchstens 10 000 Einträge (die ältesten
fallen heraus); der Rumpf einer Anfrage mit Idempotency-Key wird vor der Anmeldung ganz gelesen,
ohne die Grenze MaxBodyBytes; eigene Ablehnungen (400, 422) dieser Prüfung kommen als Text, nicht
in der Fehlerform. Die Anmeldung zählt über die Kopfzeile des Schlüssels (ApiKeyHeader,
Vorgabe X-Api-Key) und Authorization. Eine gespeicherte Antwort wird nicht erneut
berechtigt – ein inzwischen gesperrter Schlüssel erhält sie noch bis zum Ablauf der zehn Minuten.
Eine Windows-Anmeldung zählt über ihr Konto, nicht über ihren Handschlag, der auf jeder
neuen Verbindung anders lautet; die Adresse des Clients zählt nicht, weil sie sich mit der
abgebrochenen Verbindung ändern kann.
Installationspakete herunterladen
GET /download/ listet die Installationspakete aus dem Ordner Gateway:Downloads:Path (Vorgabe
downloads, relativ zum Inhaltsstamm; leer schaltet das
Herunterladen ab) mit Größe, SHA-256 und – nur bei .exe, aus deren Versionsangabe – der Version,
als JSON, für einen Browser als Seite. GET /download/{datei} liefert die Datei.
Angeboten werden nur Dateien direkt in diesem Ordner mit der Endung .msi oder .exe, ohne
Anmeldung, denn sie enthalten kein Geheimnis; jeder andere Name, ein Unterordner oder .. ergibt
404. Die Ratenbegrenzung gilt je Herkunft.
Schemastand für den WKL Bootstrapper
GET /schema-state/{alias}/{produkt} liefert den Schemastand eines Produkts in der Datenbank des
Alias, damit der WKL Bootstrapper nur die Anwendungsversionen anbietet, die zu ihr passen:
{ "format": 1, "product": "Demo", "schema": 13, "applications": { "min": "2.4.0", "max": "" }, "updated": "2026-10-22T10:05:00Z" }
schema ist die Schemaversion, applications der Bereich der Anwendungsversionen, die mit ihr
arbeiten (max leer: ohne Obergrenze). Der Dienst liest den Stand aus wkl.Schemastand mit der
Anmeldung der Datenquelle; diese Anmeldung muss Mitglied der Datenbankrolle wkl_leser sein.
Anwender brauchen keine Berechtigungen in der Datenbank.
| Antwort | Bedeutung |
|---|---|
200 | Der Stand als JSON, mit ETag |
304 | If-None-Match nennt den aktuellen ETag |
401 | Nicht mit Windows angemeldet, und Gateway:SchemaState:Anonymous ist aus |
404 | Alias unbekannt, sein Provider spielt keine Datenbank-Updates ein, oder das Produkt hat keinen Stand |
503 | Die Datenbank ist nicht lesbar |
Angemeldet wird nur mit Windows; jedes Konto, das Windows anmeldet, erhält die Antwort, denn sie enthält keine Daten der Datenbank. Ohne Anmeldung antwortet der Dienst nur, wenn `Gateway:SchemaState:Anonymous` eingeschaltet ist. Der Dienst hält eine Antwort 30 Sekunden vor; eine nicht lesbare Datenbank wird beim nächsten Aufruf erneut gefragt.
Anwendung und Version melden (X-Wkl-Application)
Eine Anwendung sendet in jeder Anfrage den Kopf X-Wkl-Application: <produkt>/<version>, etwa
Demo/2.4.0 (Produkt: Buchstaben, Ziffern, ., -, _, höchstens 64 Zeichen; Version: zwei bis
vier Zahlen). Der Dienst merkt sich je Alias, wann welche Version zuletzt angefragt hat. Ein
Datenbank-Update, das ältere Anwendungen bricht, wartet, solange eine ältere Version innerhalb des
Sitzungsfensters angefragt hat. Ohne gültigen Kopf gilt eine Anfrage als „unbekannt“; unbekannte
Anwendungen halten kein Update auf. Der Wert ist eine Angabe des Programms, kein
Sicherheitsmerkmal.
Bemerkungen
- Um einen Prozeduraufruf wird keine äußere Transaktion gelegt. Die Schreibprozeduren dieses Produkts enthalten je eine eigene; eine Schachtelung würde ihre Fehlerbehandlung stillschweigend verändern. Stattdessen läuft
SET XACT_ABORT ONvor dem Aufruf. Hat der Client selbst eine Transaktion geöffnet, läuft der Aufruf in ihr und ohneXACT_ABORT, wie über eine direkte Verbindung: EinROLLBACKin der Prozedur beendet dann auch die Transaktion des Clients. Um ein erzeugtes Schreiben dagegen schon: es bringt keine eigene mit, und ohne sie könnte der Wächter einen Vorgang, der die falsche Zahl Zeilen traf, nicht zurücknehmen. - Eine Tabelle mit einer Pflichtspalte, die sich nicht benennen lässt, nimmt kein Anlegen an. Das betrifft nur Namen, deren kodierte Form länger als 128 Zeichen wäre; Sonderzeichen allein machen einen Namen nicht unbenennbar. Ist eine solche Spalte Pflicht und hat keinen Vorgabewert, kann kein Aufrufer einen neuen Satz vollständig angeben; der Dienst meldet das vorab, statt die Ablehnung der Datenbank abzuwarten. Ändern und Löschen sind davon nicht betroffen.
- Ein Argument an einer Tabelle ist ein Schlüssel.
GET …/tables/X.json?Id=5liest genau diesen Satz; ein Argument, das nicht zum Schlüssel gehört, wird mit400abgewiesen, statt übergangen zu werden; übergangen, lieferte die Anfrage sonst das erste Fenster der ganzen Tabelle. - Gezählt wird in der Anweisung, nicht im Treiber. Der Treiber rechnet Zeilen mit, die Trigger ändern; eine Tabelle mit Historien-Triggern würde dann für jede Einzeländerung mehrere Zeilen melden.
@@ROWCOUNTdirekt nach der Anweisung ist davon unberührt.