Kuonen SQL

Handbuch

Syntax, Datentypen, Funktionen und Fehlernummern je Abfragesprache — jeder Abschnitt mit einem Beispiel aus dem Beispielkatalog und, wo die Sprache einen eigenen Sprachmeilenstein der Roadmap hat, dem Stand jedes einzelnen Punktes daraus.

Über Port 3306 (voreingestellt aus) spricht KuonenSQL das MariaDB-Client/Server-Protokoll und den MariaDB-SQL-Dialekt auf demselben Datenbestand wie T-SQL — der Dialekt-Übersetzer bildet ihn auf denselben Syntaxbaum ab, es gibt keinen zweiten Ausführer.

Punkt Beschreibung Stand
D17.1 Der Handschlag Port 3306 (einstellbar, voreingestellt aus), Paketrahmen mit Folgenummer und 16-MB-Aufteilung, Handshake v10 samt Fähigkeitsbits und Versionsangabe (`11.x-MariaDB`), TLS über `SSLRequest`, `COM_QUIT`/`COM_PING`/`COM_INIT_DB`/`COM_RESET_CONNECTION` ✅
D17.2 Die Anmeldung `mysql_native_password`, `client_ed25519` (Ed25519 im eigenen Code) und `caching_sha2_password`, `COM_CHANGE_USER`, Auth-Switch; gegen dieselben Anmeldungen wie TDS, ohne zweite Benutzertabelle ✅
D17.3 Textabfragen und Ergebnismengen `COM_QUERY` mit Mehrfachanweisungen, Spaltendefinitionen, Textzeilen, `OK`/`ERR`/`EOF` samt `affected_rows`, `last_insert_id`, Warnungszahl und SQLSTATE; die MariaDB-Fehlernummern (1062, 1146, 1064 …) statt der SQL-Server-Nummern ✅
D17.4 Vorbereitete Anweisungen `COM_STMT_PREPARE`/`EXECUTE`/`FETCH`/`CLOSE`/`RESET`/`SEND_LONG_DATA`, das binäre Zeilenformat, Parameter mit `?`, Cursor; MariaDBs `COM_STMT_BULK_EXECUTE` für Stapel (am echten `mariadb:11` vermessen, 30.09.2026: erweiterte Fähigkeit `STMT_BULK_OPERATIONS` im Gruss und in der Anmeldeantwort, 1295 ohne sie, Hinweise NULL/DEFAULT/IGNORE, Typen des letzten Aufrufs, alles oder nichts, 1210/1835) ✅
D17.4b Bulk: Einzelergebnisse **Erledigt, mit einem Rohclient am echten `mariadb:11` (11.8.9) vermessen:** `BULK_UNIT_RESULTS` — Kennzeichen 0x40 an `COM_STMT_BULK_EXECUTE` liefert statt des OK eine Ergebnismenge `Id`/`Affected_rows` (beide BIGINT, Binärzeilen) mit einer Zeile je Parametersatz; `Id` ist der Zählerwert, den genau dieser Satz erzeugt hat (sonst 0, auch bei einem UPDATE nach einem früheren INSERT der Sitzung), der Gruss bietet jetzt `0x24` (`STMT_BULK_OPERATIONS` und `BULK_UNIT_RESULTS`). Der echte Server richtet sich allein nach dem Kennzeichen, nicht nach der Fähigkeit — hier ebenso. 3 neue Tests. ✅
D17.4c PROGRESS, EXTENDED_METADATA, CACHE_METADATA **Erledigt, jedes Byte am echten `mariadb:11` (11.8.9) mit einem Rohclient gemessen:** der Gruss bietet wie der Server `0x3D`; **nur für einen Client, der sie anfordert**, gilt: `EXTENDED_METADATA` (hinter dem Originalnamen jeder Spalten- und Platzhalterdefinition ein Text, leer `00`, sonst `uuid`/`json` als Typname bzw. Format) und `CACHE_METADATA` (hinter der Spaltenzahl ein Kennbyte: 1 Definitionen folgen, 0 nicht — bei vorbereiteten Anweisungen merkt sich der Server die zuletzt geschickten und wiederholt sie erst nach einer Änderung); `PROGRESS` wird ausgehandelt, gemeldet wird nichts (keine Anweisung hat einen Fortschritt). Ein Client ohne die Bits sieht das Format von vorher. 7 neue Tests. Rest in D17.4d ✅
D17.4d Metadaten und Bulk im Einzelnen **Erledigt (M17), Zahlen am echten `mariadb:11` gemessen (`MariaDbEinfuegeKennungTests`, `MariaDbLetzteKennungTests`):** `Id` und `LAST_INSERT_ID()` nennen bei einem `INSERT` mit mehreren Zeilen (auch im Bulk-Satz und bei `INSERT … SELECT`) den **ersten** erzeugten Wert statt des letzten; ein eigener Wert erzeugt nichts. Das `insert_id` des OK-Pakets nennt nur, was **diese** Anweisung erzeugt hat (ein `UPDATE`/`DELETE` nach einem `INSERT` meldete den Wert von vorhin, jetzt 0: `ExecutionEngine.AnweisungsAutowert`); **`LAST_INSERT_ID(ausdruck)`** setzt den Wert der Sitzung und das `insert_id` der Anweisung (`ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`, `UPDATE … SET n = LAST_INSERT_ID(n + 1)`; war ein Durchreicher), `LAST_INSERT_ID(NULL)` setzt 0; `LAST_INSERT_ID()` ist eine Ganzzahl (war `decimal`) und ohne erzeugten Wert 0 (war NULL). `ON DUPLICATE KEY UPDATE` und `REPLACE` mit mehreren Zeilen laufen seit D17.5c Zeile für Zeile und folgen derselben Regel. ✅
D17.4e Rest: Zähler-Lücken, Fortschritt, erweiterte Metadaten **Erledigt (M17), am echten `mariadb:11` (11.8.9) gemessen (`MariaDbAutowertvorratTests`):** der `AUTO_INCREMENT`-Vorrat der Anweisung (`n` Wertezeilen reservieren `n` Werte, `INSERT … SELECT` Stapel 1, 2, 4 …, eine nur ändernde/auslassende Zeile verbraucht bei einer Zeile trotzdem einen Wert, bei mehreren gibt sie ihn dem Vorrat zurück, der Rest verfällt) und `REPLACE` als Löschen jeder Zeile mit gleichem Schlüssel plus Einfügen mit neuem Wert (Fremdschlüsselaktion, Trigger, betroffene Zeilen = gelöschte + eingefügte). ✅
D17.4f Erweiterte Metadaten und Fortschrittspakete **Erledigt (M17, Runde 11), jedes Byte aus dem Mitschnitt gegen den echten `mariadb:11` (11.8.9) (`orakel/mitschnitt-antworten.txt`, `MariaDbMetadatenTypenTests`):** mit `EXTENDED_METADATA` steht hinter dem Originalnamen das Tupel aus Kennbyte und Text: `inet4`/`inet6`/`point`/`linestring` und die weiteren Geometrienamen (Kennbyte 0), `uuid` (0), `json` als Format (1); `GEOMETRY` hat keines. Die Felder fester Länge: `INET4`/`INET6` als Zeichenkette 0xfe, Länge 60 und 156, Zeichensatz 224, Flags 0xa0; `UUID` ebenso mit Länge 144; Geometrie 0xff, Zeichensatz 63; `JSON` 0xfc mit der Höchstlänge, Zeichensatz 224. Die Marke reist als Typmarke (`ma:t:`, `ma:g:`) vom Katalog (Typbedingung, gemerkte Schreibweise `point`) über den Wert in die Ergebnisspalte. Fortschrittspakete (`ff ffff 01`, Stufe, Stufenzahl, Fortschritt in drei Byte, Name): braucht `ALTER TABLE … ALGORITHM=COPY` mindestens `progress_report_time` Sekunden und hat der Client `PROGRESS` verlangt, folgen vor dem OK die vier Stufen `copy to tmp table`, `Enabling keys` (zweimal) und `After apply log event`; `ALGORITHM` und `LOCK` in einem `ALTER` werden gelesen. Der Fortschrittswert der ersten Stufe steht bei 0 (der Server nennt den Stand zur Meldezeit). ✅
D17.4g LAST_INSERT_ID in Triggern **Erledigt (M17, Runde 11), am echten `mariadb:11` (11.8.9) gemessen (T1 bis T14, `MariaDbLetzteKennungTriggerTests`):** schreibt der Trigger eines `INSERT` in eine andere Tabelle mit `AUTO_INCREMENT`, bleiben `LAST_INSERT_ID()` und `insert_id` die der Anweisung (die erste der eigenen Tabelle, auch bei mehreren Zeilen); ein `BEFORE`-Trigger sieht die Kennung der Sitzung von vorher, danach gilt die der Anweisung. ✅
D17.5 Der MariaDB-Parser Eigener Lexer und Parser auf **denselben** Syntaxbaum: Backticks, `'` und `"` je nach `ANSI_QUOTES`, `#`- und `-- `-Kommentare, `/*! … */`, `LIMIT n OFFSET m` und `LIMIT m, n`, `:=`, `<=>`, `DIV`/`MOD`, `REGEXP`, `\`-Escapes, Benutzervariablen `@x` und Systemvariablen `@@x` ✅
D17.5b Die volle Dialekt-Weiche im gemeinsamen Baum **Schrittweise (M17), je Konstrukt eine Scheibe.** **Erledigt: `LIMIT n`, `LIMIT n OFFSET m`, `LIMIT m, n`** sind Grammatik im gemeinsamen `PrattParser` (`IstLimitKlausel`, nur unter `Dialekt.LimitKlausel` (D17.13); derselbe Baum wie `OFFSET m ROWS FETCH NEXT n ROWS ONLY`, ohne `ORDER BY` mit derselben verdeckten Sortierung); `MariaDbLimitUebersetzer` ist gelöscht. **Scheibe 2: `WITH RECURSIVE`** liest der Parser selbst (`ParseWithCteStatement`; ein CTE-Name `recursive` bleibt ein Name, in T-SQL bleibt es Fehler 102). **Scheibe 3: `CAST(x AS UNSIGNED|SIGNED [INTEGER])` und `CONVERT(x, UNSIGNED|SIGNED [INTEGER])`** (`ParseCastExpression`/`ParseConvertExpression`; für CONVERT liest eine Vorschau im Quelltext das letzte Argument, weil MariaDB und T-SQL die Reihenfolge vertauschen). Neu möglich, was der Text-Übersetzer nicht konnte: `LIMIT` in einer abgeleiteten Tabelle. **Scheibe 4 und 5: `IF(b, a, c)` als Ausdruck und `DATE_ADD|DATE_SUB(d, INTERVAL n EINHEIT)`** liest der Parser selbst (`IF` vor `(` wird zu `IIF`, `INTERVAL` wird `DATEADD`; `MariaDbIfDatumParserTests`); `ErsetzeIf` und `ErsetzeDateAddSub` sind gelöscht (`MariaDbIfDatumParserTests`). Damit der Wiederanlauf einen Stapel mit `LIMIT` versteht, trägt eine Protokollfolge das Merkmal der MariaDB-Sitzung (Bit 1048576 der Transaktionsoptionen) und wird mit demselben Dialekt abgespielt (`MariaDbLimitWiederanlaufTests`). **Scheibe 6: `GROUP_CONCAT([DISTINCT] a [, b …] [ORDER BY …] [SEPARATOR s])`** (`ParseGroupConcat`, `Dialekt.GroupConcat`; derselbe Baum wie `STRING_AGG … WITHIN GROUP`), neu: mehrere Ausdrücke (wie `CONCAT`, eine Zeile mit NULL fällt weg); `MariaDbFunktionsUebersetzer` ist gelöscht. **Scheibe 7: `UPDATE/DELETE … LIMIT n`** ohne `ORDER BY` (`LiesDmlLimit`, derselbe Baum wie `TOP (n)`) und **`RETURNING`** an `INSERT`/`UPDATE`/`DELETE` (`LiesReturning`, `Dialekt.ReturningKlausel`; derselbe Baum wie `OUTPUT INSERTED.…`/`DELETED.…`; `RETURNING` ist dann kein Alias); die zwei Textfunktionen in `MariaDbDmlUebersetzer` sind gelöscht. Rest siehe D17.5c ✅
D17.5c Rest: die übrigen Text-Übersetzer im Parser **Scheibe 1 erledigt: `UPDATE/DELETE … ORDER BY … LIMIT n`** liest der Parser selbst (`LiesDmlLimit`/`MitDmlTop`, gleiche CTE-Form `SELECT TOP (n) * … ORDER BY …` im Baum; ein `ORDER BY` ohne `LIMIT` entfällt wie bei MariaDB an einer Einzeltabelle; `MariaDbDmlGeordnetParserTests`); nebenbei ein Engine-Fehler behoben (Fehler 207, wenn eine aktualisierbare CTE einen `ORDER BY`-Ausdruck mit `TOP` trägt: die verdeckte Sortierspalte zählte als Zielspalte). **Scheibe 2 erledigt: `INSERT IGNORE`, `REPLACE [INTO]`, `INSERT … ON DUPLICATE KEY UPDATE` (samt `VALUES(spalte)`) und `INSERT … SET`** liest der Parser (`PrattParser.Upsert.cs`, `Dialekt.UpsertKlauseln`, `InsertStatement.Upsert`); die Engine führt es Zeile für Zeile als `MERGE` über den Schlüssel aus dem Katalog aus (`ExecutionEngine.Upsert.cs`), `MariaDbUpsertUebersetzer` ist gelöscht (der Klammer-Teiler für `sp_executesql` steht jetzt als `Parser/Textteiler`, die Ausnahme im Architekturtest ist weg). Am echten `mariadb:11` mit denselben Anweisungen gemessen (Zeilen **und** betroffene Zeilen: geändert 2, unverändert 0, ersetzt 2, neu 1; `MariaDbUpsertParserTests`, 13) - neu gegenüber dem Text-Übersetzer: Schlüssel auch über `UNIQUE`, `INSERT … SELECT`, ohne Spaltenliste, `INSERT … SET`, zwei Zeilen mit demselben Schlüssel in einer Anweisung sehen einander, `REPLACE` setzt nicht genannte Spalten auf ihren Vorgabewert zurück, `INSERT IGNORE` nimmt für NULL in einer `NOT NULL`-Spalte den Vorgabewert des Typs und lässt Zeilen aus, die einen anderen Schlüssel/Fremdschlüssel/`CHECK` verletzen, eine unbekannte Spalte in `ON DUPLICATE KEY UPDATE` scheitert auch ohne Treffer. Hinter `WITH … INSERT IGNORE` lehnt die Engine mit 1235 ab statt still wegzulassen. Ohne Schlüssel in der Spaltenliste ist es ein gewöhnliches `INSERT`. Nebenbei geschlossen: `OUTPUT INSERTED.*`/`DELETED.*` kannte die Engine nur bei `MERGE` (damit ging auch `RETURNING *` nicht) - der Stern wird jetzt bei `INSERT`, `UPDATE` und `DELETE` zu den sichtbaren Spalten (`ExecutionEngine.AusgabeSterne`), und `RETURNING` liefert auch bei `REPLACE`, `ON DUPLICATE KEY UPDATE` und `INSERT IGNORE` die neuen Zeilen. **Scheibe 3 erledigt: `MATCH (spalten) AGAINST (anfrage [IN NATURAL LANGUAGE MODE | IN BOOLEAN MODE])`** liest der Parser (`PrattParser.Volltext.cs`, `Dialekt.MatchAgainst`) als Aufruf `__MDB_MATCH(bezug, modus, anfrage, spalten…)`; welcher `FULLTEXT`-Index gemeint ist, bestimmt die Auswertung aus den `FROM`-/`JOIN`-Tabellen der umgebenden Abfrage (`VolltextBezug` bekommt sie, wenn die Abfrage gelesen ist; auch über eine abgeleitete Tabelle), ein `MATCH` hinter `WHERE`/`AND`/`OR`/`NOT`/`HAVING`/`ON` ist wahr bei Relevanz über 0. `MariaDbVolltextUebersetzer` ist gelöscht; `MariaDbVolltextSuche` (die Rechnung) und die Verwaltung der `FULLTEXT`-DDL (`MariaDbVolltext`) bleiben. Am echten `mariadb:11` gemessen (`MariaDbMatchParserTests`, 5; ein Nachspiel von 15 Abfragen stimmt bis auf `WITH QUERY EXPANSION`, das wie vorher 1235 meldet). **Scheibe 4 erledigt: `LOAD DATA [LOCAL] INFILE` liest der Parser** (`PrattParser.LadeDaten.cs`, `Dialekt.LadeDaten`, `LadeDatenStatement`: alle Klauseln, `FIELDS`/`LINES`, `IGNORE n LINES`, Spaltenliste mit `@var`, `SET`); die Regex-Übersetzung `MariaDbLoadDataUebersetzer` ist weg. Die **Datei** liest weiter der Anschluss (`MariaDbLoadData`, ohne Textumformung der Syntax) und schickt der Engine ein `INSERT` mit den Daten: das Transaktionsprotokoll hält den Text der Anweisung, ein `LOAD DATA`, das nur auf eine Datei zeigt, überstünde den Neustart nicht (bei `LOCAL` gäbe es die Datei gar nicht mehr). `MariaDbLoadData*Tests` (41 mit dem Parser-Test, 4 neu) unverändert grün. **Scheibe 5 erledigt: die Spaltentypen von MariaDB** (`UNSIGNED`, `ZEROFILL`, Anzeigebreite, `ENUM`, `SET`, `YEAR`, `BIT(n)`, `UUID`, `INET4`/`INET6`, `DATETIME(p)`/`TIMESTAMP(p)`, `BOOL`, `DOUBLE [PRECISION]`, `TEXT`-/`BLOB`-Familie, Geometrietypen, `CHARACTER SET`/`COLLATE` an der Spalte) liest der Parser in `CREATE TABLE` und `ALTER TABLE … ADD|MODIFY|CHANGE` (`PrattParser.Spaltentypen.cs`, `Dialekt.MySqlSpaltentypen`): der Typname im Stil von T-SQL und die `CHECK`-Bedingungen des Typs kommen aus derselben Grammatik (in `ALTER` als eigene `ADD CONSTRAINT`-Schritte wie vorher), `MariaDbTypRueckuebersetzung` liest sie unverändert zurück; `MariaDbTypUebersetzer` ist gelöscht. `MariaDbSpaltentypenParserTests` (7) und die bestehenden Typ-Tests (`MariaDbTypenTests`, `MariaDbSpaltentypErweiterungTests`, `MariaDbZahldarstellungTests`) unverändert grün. Nebenbei: der Optionen-Ausdruck des DDL-Übersetzers fraß bei `… VARCHAR(50) COLLATE x)` die schliessende Klammer. **Scheibe 6 erledigt: `ALTER TABLE` mit mehreren Änderungen, `RENAME TABLE` mit Paaren, `DROP TABLE` mit Liste** (`PrattParser.AlterListe.cs`, `Dialekt.AlterMitAenderungsliste`): `ADD [COLUMN]` (auch `(spalte typ, …)`, `INDEX`/`KEY`/`UNIQUE KEY`, `PRIMARY KEY`, `FOREIGN KEY`, `CHECK`, `CONSTRAINT`), `MODIFY`, `CHANGE`, `ALTER COLUMN … [SET DEFAULT x | DROP DEFAULT]`, `DROP COLUMN|INDEX|KEY|FOREIGN KEY|CONSTRAINT|PRIMARY KEY`, `RENAME COLUMN|INDEX|TO`, `AUTO_INCREMENT = n`, `ENGINE`/`COMMENT`/Zeichensatz (ohne Wirkung), `FIRST`/`AFTER x` (gelesen, die Engine legt die Lage nicht fest) - als Block der vorhandenen Anweisungen; **was die Grammatik nicht kennt, ist ein Syntaxfehler statt eines stillen Weglassens.** Beim Nachspiel gegen `mariadb:11` (`alt_diff`) zeigte sich: nach der ersten Änderung las der T-SQL-Zweig den Rest der Anweisung bis zum Semikolon und warf ihn weg (`ADD COLUMN c INT, ADD COLUMN e INT NOT NULL DEFAULT 5` legte `e` nie an), `RENAME TABLE a2 TO a3, a3 TO a4` legte eine Tabelle namens `a3,` an, `DROP TABLE a, b` und `RENAME COLUMN` waren Syntaxfehler, `AUTO_INCREMENT=n` als Tabellenoption fiel weg, ein zweiter Primärschlüssel wurde nicht abgelehnt (1068), `DROP COLUMN`/`MODIFY` einer unbekannten Spalte meldeten 1105. Die Übersetzer für `RENAME TABLE`, `ADD COLUMN`, `MODIFY`, `CHANGE`, `AFTER`/`FIRST` und die Tabellenoptionen im `MariaDbDdlUebersetzer` sind gelöscht; `DROP TABLE a, b` gilt seit dem auch in T-SQL. Neu in der Engine: `AlterTableAktion.StandardSetzen` und `PrimaerschluesselEntfernen`, die Ablehnung eines zweiten Primärschlüssels. `MariaDbAlterListeParserTests` (9) plus zwei Leitungs-Tests, Nachspiel gegen `mariadb:11` stimmt bis auf `ON UPDATE CURRENT_TIMESTAMP` in einer Änderungsliste (bleibt ein Syntaxfehler, siehe D17.6e). **Noch Textumformung und damit offen,** je mit Grund: Mehrtabellen-`UPDATE`/`DELETE` (`MariaDbDmlUebersetzer`, `MariaDbMehrfachzielUebersetzer`: mehrere Anweisungen samt Zwischentabelle), DDL im MariaDB-Stil (`MariaDbDdlUebersetzer`: `CREATE TABLE` mit `AUTO_INCREMENT`, Tabellenoptionen und Inline-`KEY`, `IF NOT EXISTS`, `OR REPLACE`, `LIKE`, `AS SELECT`, Transaktions- und `KILL`-Anweisungen; `MariaDbOnUpdateAlterUebersetzer`: `ON UPDATE CURRENT_TIMESTAMP` als Trigger), Routinen und Trigger (`MariaDbRoutineUebersetzer`: Kontrollfluss mit `BEGIN…END`, Handler, Cursor-Nachbildung). Jede Scheibe muss dieselben Bäume liefern, die der Text heute ergibt, und wiederanlauffest sein (Kennung im Protokoll, D17.13). **Scheibe 7 erledigt (Runde 8): `UPDATE t1 JOIN t2 … SET …`, `UPDATE t1, t2 SET …` und `DELETE FROM t1 USING …` mit einem Ziel liest der Parser selbst** (`PrattParser.MehrtabellenDml.cs`, `Dialekt.UpdateMitTabellenliste`: die Tabellenliste mit `LiesQuellen(ohneFrom: true)`, das Ziel ist die Tabelle oder der Alias vor den Spalten in `SET`, ohne Bezug die erste Tabelle; derselbe Baum wie die T-SQL-Form `UPDATE ziel SET … FROM …`). `MariaDbDmlUebersetzer` (125 Zeilen) ist gelöscht; `MariaDbMehrtabellenParserTests` (8, Zeilen und betroffene Zeilen je am echten `mariadb:11` gemessen, ohne die Text-Übersetzer des Anschlusses). **Scheibe 8 erledigt (Runde 8): die Transaktionsanweisungen** (`START TRANSACTION` mit Merkmalen, `BEGIN [WORK]`, `COMMIT`/`ROLLBACK` mit `WORK`/`AND NO CHAIN`/`RELEASE` als stilles OK ohne offene Transaktion, `SAVEPOINT`, `ROLLBACK [WORK] TO [SAVEPOINT]`, `RELEASE SAVEPOINT`, `KILL CONNECTION` oder `KILL QUERY`) liest der Parser (`PrattParser.MariaTransaktion.cs`, `Dialekt.MariaTransaktionen`; `MariaDbTransaktionParserTests`, 12); **Scheibe 9: `CREATE OR REPLACE` für Sicht, Prozedur, Funktion und Trigger** ist im Parser `CREATE OR ALTER` (`Dialekt.CreateOrReplace`), **Scheibe 10: `AUTO_INCREMENT` an der Spalte** ist `IDENTITY` im Parser (`MariaDbCreateOrReplaceParserTests`, 3); die tote Rückfall-Regel `CREATE TABLE … LIKE` ist gelöscht. Entfallen ist ausserdem `MariaDbOnUpdateAlterUebersetzer` (157 Zeilen: `ON UPDATE CURRENT_TIMESTAMP` liest der Parser als Spalteneigenschaft). **Scheibe 11 erledigt: `CREATE TABLE` im MariaDB-Stil liest der Parser** (`PrattParser.CreateTableMaria.cs`, `Dialekt.CreateTableMaria`): Inline-`KEY`/`INDEX`/`UNIQUE KEY`/`FULLTEXT KEY` in der Spaltenliste (einfacher `KEY` ist ein `CREATE INDEX` hinter der Tabelle, namenlos `IX_tabelle_n`, `UNIQUE KEY` eine Einschränkung, Präfixlängen und `DESC` werden gelesen), Tabellenoptionen (`AUTO_INCREMENT = n` setzt den Zähler per `DBCC CHECKIDENT`, `ENGINE`, Zeichensatz, `COMMENT` ohne Wirkung), `IF NOT EXISTS` (Hinweis 1050 statt Anlegen), `OR REPLACE TABLE` (`DROP TABLE IF EXISTS` und `CREATE TABLE`), `AS SELECT` (`SELECT … INTO`) und bei Sichten `ALGORITHM`/`DEFINER`/`SQL SECURITY` sowie `CREATE VIEW IF NOT EXISTS`; die gespeicherte Definition einer Sicht trägt den Kopf nicht. **`MariaDbDdlUebersetzer` ist gelöscht;** übrig bleibt eine Textstufe, `MariaDbSpaltenzeichensatz.TabellenstandardAnTabellen` (der Zeichensatz der Tabelle muss vor dem Parsen an die Textspalten). Neu gegenüber dem Text: Index-Optionen (`USING BTREE`, `COMMENT`) hinter `KEY (…)`, `IF NOT EXISTS` auch mit `AS SELECT`, ein Kopf ohne `VIEW` ist ein Syntaxfehler. `MariaDbCreateTableParserTests` (12; die Sabotage-Probe `KEY` wird nicht nachgezogen machte 2 rot) und alle 1426 Tests der Klassen MariaDb, Parser, Dialekt und Architektur unverändert grün. **Noch Text, je eine eigene mehrtägige Scheibe ohne neues Verhalten für den Client:** **Scheibe 12 erledigt: Mehrtabellen-`UPDATE`/`DELETE` mit mehreren Zielen** (`UPDATE t1 JOIN t2 … SET t1.a = …, t2.b = …`, `UPDATE t1, t2 SET …`, `DELETE t1, t2 FROM …`, `DELETE FROM t1, t2 USING …`, auch `t1.*` und `LOW_PRIORITY`/`QUICK`/`IGNORE`) liest der Parser als neues Baumobjekt `MehrfachzielStatement` (Ziele, je Zuweisung ihr Ziel, Quellen, `WHERE`; `PrattParser.MehrtabellenDml.cs`). Die Schlüsselspalten der Ziele löst die Ausführung über den Katalog auf (`ExecutionEngine.ExecuteMehrfachziel`: eine Anweisung schreibt Schlüssel und alte Werte in `#__kmt`, danach ändert je Ziel eine Anweisung; die Anweisung ist als Ganzes atomar über `GanzOderGarNicht`, die Summe der betroffenen Zeilen ist das Ergebnis). `MariaDbMehrfachzielUebersetzer` (370 Zeilen) samt `NimmSumme` und der Hilfsvariable `@__kmt_zeilen` sind gelöscht; die 8 Leitungs-Tests `MariaDbMehrfachzielTests` unverändert grün, dazu `MariaDbMehrfachzielParserTests` (7: Baumform, beide Schreibweisen, Rollback auch in offener Transaktion, 102 ohne Primärschlüssel oder bei unbekanntem Ziel und bei `LIMIT`). **Scheibe 13 ✅ (in sechs Unterscheiben, je eigener Commit): die Routinen und Trigger liest der Parser, `MariaDbRoutineUebersetzer` (acht Dateien, rund 1900 Zeilen) ist gelöscht.** **13a:** `CALL name[(argumente)]` (`PrattParser.MariaCall.cs`, `Dialekt.MariaCall`): derselbe Baum wie `EXEC`; ob ein `@variable`-Argument `OUTPUT` ist, entscheidet die Ausführung an der Deklaration der Prozedur (`ExecuteProcedureStatement.AusgabeNachDeklaration`) statt des Katalogs beim Übersetzen; ein Vergleich `a = 1` als Argument ist kein benannter Parameter mehr. **13b:** der Kopf von `CREATE PROCEDURE`/`FUNCTION`/`TRIGGER` (`PrattParser.MariaRoutine.cs`, `Dialekt.MariaRoutinen`): Parameter mit `IN`/`OUT`/`INOUT` (`OUT` nimmt den Wert des Aufrufers nicht an), `RETURNS`, Merkmale, `DEFINER`, `OR REPLACE`, `IF NOT EXISTS` (`WennNichtVorhanden` an der Anweisung, die Ausführung prüft den Katalog erst beim Ausführen), beim Trigger Zeitpunkt, Ereignis, Tabelle, `FOR EACH ROW`, `FOLLOWS`/`PRECEDES`. **Die Definition im Katalog ist der Wortlaut des Clients** (vorher der übersetzte T-SQL-Text); ein Neustart liest sie mit dem Dialekt der Sitzung, den das Transaktionsprotokoll mitträgt (`MariaDbRoutineKopfParserTests`: Prozedur mit `OUT`, Funktion und Trigger überstehen einen Neustart und laufen danach). **13c:** der Rumpf (`PrattParser.MariaRumpf.cs`): `BEGIN … END` auch mit Marke, `DECLARE` von Variablen und Cursorn, `IF`/`ELSEIF`/`ELSE`, `WHILE`, `LOOP`, `REPEAT … UNTIL`, `LEAVE`/`ITERATE` (`BREAK`/`CONTINUE` oder `GOTO` auf `__ende_n`/`__weiter_n`; ein `ITERATE` in `REPEAT` springt vor die `UNTIL`-Prüfung), alles Übrige liest der Parser unverändert. Lokale Namen und Sitzungsvariablen sind kein Textersatz mehr, sondern ein Filter auf den Token (`MariaRumpfToken`: eine Variable oder ein Parameter wird `@name`, `@x` wird `@__u_x`, hinter einem Punkt bleibt es eine Spalte - der Text-Umschreiber machte daraus `t.@name`); die Zeilenmarken `/*L5*/` sind durch `Herkunftstext`/`Herkunftsbeginn` an jeder Anweisung ersetzt (der Text der Routine ab `CREATE`; `Routinenzeilen.ZeileBei`); die Spaltennamen eines `SELECT` im Rumpf vergibt der Parser (`Dialekt.AuswahlSpaltennamen`). Neu gegenüber dem Text: `CALL` im Rumpf kennt `OUTPUT`. **13d:** Handler, `SIGNAL`/`RESIGNAL`, `GET DIAGNOSTICS`, `FETCH … INTO`, `SELECT … INTO` (`PrattParser.MariaHandler.cs`, `MariaDbHandlerBedingung`): `EXIT` umschließt den Rest des Blocks mit `TRY … CATCH`, `CONTINUE` jede folgende Anweisung, `NOT FOUND`/`SQLWARNING` als Aktion hinter dem leeren Lesen bzw. als Fehler 02000 im `EXIT`; die Bedingung (`SQLEXCEPTION`, Fehlernummer, `SQLSTATE`) bildet `Dialekt.HandlerBedingungAlsTSql` auf die Nummern der Engine ab; `SELECT … INTO` an der Liste oder am Ende wird `SELECT @v = …` samt `@@ROWCOUNT`-Prüfung (kein Treffer = `NOT FOUND`, mehr als eine Zeile = 1172); feste Bausteine entstehen aus kleinen T-SQL-Fragmenten mit der Herkunft der Anweisung des Clients. **13e:** der Rumpf von `CREATE TRIGGER … FOR EACH ROW` (`PrattParser.MariaTrigger.cs`): `NEW.spalte`/`OLD.spalte` werden Variablen eines Cursors auf `inserted`/`deleted` (bei `UPDATE` über den Primärschlüssel verknüpft, ohne einen Fehler 1785), ein Rumpf ohne `NEW`/`OLD` läuft ohne Cursor, `BEFORE` ist `INSTEAD OF` und schreibt die Zeile hinter dem Rumpf mit den `NEW`-Werten selbst; `SET NEW.x = …` und `NEW.[x]` gehen, ein unbekanntes Feld ist 1054. **13f:** `MariaDbRoutineUebersetzer` samt `Dialekt.RoutinenrumpfAlsTSql`/`TriggerrumpfAlsTSql`, den Zeilenmarken und `FindeEndeDesBegin`/`EntferneZeilenkommentare` gelöscht; `MariaDbLexer.UebersetzeNormalisiert` kennt keinen Routinenschritt mehr. **Bleibt als Text:** die Normalisierung des Lexers, der Anweisungs-Teiler `MariaDbTextWerkzeuge.Anweisungen` (er hält `BEGIN … END` und `IF … END IF` beim Zerlegen eines Stapels zusammen - der Client schickt `;` innerhalb einer Routine) und die katalogabhängigen Vorstufen (`MariaDbTabelleLike`, `MariaDbVolltext`, `MariaDbSpaltenzeichensatz`, `MariaDbLoadData`): sie bauen keinen Baum, sondern bereiten den Text für ihn vor. **Belegt:** `MariaDbCallParserTests` (7), `MariaDbRoutineKopfParserTests` (9), `MariaDbRoutineRumpfParserTests` (8), `MariaDbRoutineHandlerParserTests` (11), `MariaDbRoutineTriggerParserTests` (7), je mit Sabotage-Probe (z. B. `OUTPUT` nicht gesetzt, `IF NOT EXISTS` abgeschaltet, `ITERATE` in `REPEAT` als `CONTINUE`, `EXIT … NOT FOUND` ohne Zähler, `UPDATE` des `BEFORE`-Triggers mit alten Werten, Zeilenzählung um eins verschoben); dazu die 1593 Tests der Klassen MariaDb, Dialekt, Architektur, Wal, Prozedur, Trigger und Parser unverändert grün, darunter die Anschlusstests über die Leitung (`MariaDbRoutinen*`, `MariaDbHandler*`, `MariaDbSelectIntoTests`, `MariaDbDiagnoseTests`, am echten `mariadb:11` gemessen). Ehrlich: Orakel und volle Testsuite wurden in diesem Lauf nicht gefahren (Vorgabe: nur betroffene Klassen). ✅
D17.6 DDL im MariaDB-Stil `CREATE TABLE` mit `AUTO_INCREMENT`, `ENGINE=`, `DEFAULT CHARSET`/`COLLATE`, Inline-`KEY`/`UNIQUE KEY`/`FULLTEXT`, `ON UPDATE CURRENT_TIMESTAMP` (als erzeugter AFTER-UPDATE-Trigger, am echten `mariadb:11` gemessen, 30.09.2026), `IF [NOT] EXISTS`, `CREATE TABLE … LIKE/AS SELECT`, `ALTER TABLE … MODIFY/CHANGE/ADD … AFTER/FIRST`, `RENAME TABLE`, `TRUNCATE`, Sequenzen, `CREATE OR REPLACE` ✅
D17.6b ON UPDATE CURRENT_TIMESTAMP in ALTER **Erledigt, gegen `mariadb:11` gemessen:** `ALTER TABLE t {ADD | MODIFY | CHANGE} [COLUMN] spalte … ON UPDATE CURRENT_TIMESTAMP` (`MariaDbOnUpdateAlterUebersetzer`): Primärschlüssel, übrige Spalten und die schon nachziehenden Zeitspalten kommen aus dem Katalog (die Zeitspalten aus dem Text des vorhandenen Triggers `__onupdate_t`), der Trigger wird bei jedem solchen `ALTER` neu gebaut; ein `MODIFY`/`CHANGE` derselben Spalte **ohne** die Klausel schaltet das Nachziehen ab, mit der Klausel wieder ein; ohne Primärschlüssel Fehler 1785 statt stillem Überlesen. Dazu ein Fund: `ALTER TABLE t ADD COLUMN c …` legte eine Spalte namens `COLUMN` an (T-SQL kennt das Wort dort nicht) — behoben. 6 neue Tests. ✅
D17.6c ON UPDATE sichtbar und dauerhaft richtig **Erledigt, Sollformen am echten `mariadb:11` gemessen:** `DEFAULT current_timestamp()` und `ON UPDATE current_timestamp()` stehen in `SHOW CREATE TABLE`, `SHOW COLUMNS` (Default/Extra `on update current_timestamp()`) und `information_schema.COLUMNS` (`COLUMN_DEFAULT`/`EXTRA`) — vorher schrieb `SHOW CREATE TABLE` das Standardwort als Text in Anführungszeichen (`DEFAULT 'current_timestamp'`), und eine Sicherung verlor die Klausel. Der nachziehende Trigger veraltet nicht mehr: hat die Tabelle einen, baut ihn jedes einzelne `ALTER TABLE … ADD|DROP|MODIFY|CHANGE` neu (mit den Spalten von jetzt); eine gestrichene Zeitspalte zieht nicht mehr nach. 6 neue Tests. ✅
D17.6d `FULLTEXT` und `MATCH … AGAINST` **Erledigt, jeder Sollwert am echten `mariadb:11` (InnoDB) gemessen:** `FULLTEXT [KEY|INDEX] [name] (spalten)` im `CREATE TABLE`, `CREATE FULLTEXT INDEX`, `ALTER TABLE … ADD FULLTEXT`/`DROP INDEX` und `DROP INDEX … ON` werden im Katalog vermerkt (`MariaVolltextIndex`, überlebt den Neustart; ein leerer Ersatz statt eines Baums — gesucht wird direkt im Text). `MATCH (spalten) AGAINST (…)` in Natural Language Mode und Boolean Mode (`+ - * "…"`) mit der Relevanz von InnoDB: Summe von `tf * idf²` in **einfacher Genauigkeit**, `idf = log10(N / df)` (N = Zeilen der Tabelle, nach `DELETE`/`INSERT` neu gezählt), `log10(1,0001)` wenn alle Zeilen Treffer sind; Wörter aus Buchstaben, Ziffern und Unterstrich (der Apostroph trennt), 3 bis 84 Zeichen, ohne die 18 Stoppwörter; ohne passenden Index Fehler 1191. Als Bedingung (`WHERE`/`AND`/`OR`/`NOT`/`HAVING`/`ON`) heißt es „Relevanz > 0“. 12 Tests (`MariaDbVolltextTests`); Sabotage: Bedingung → 3 rot, Stoppwörter → 1, df = N → 1 ✅
D17.6e FULLTEXT, ALTER mit mehreren Änderungen, TIMESTAMP, Nulldatum, Zeichensätze **Erledigt (M17, Runden 7 und 8), jeder Sollwert am echten `mariadb:11` (11.8.9) gemessen:** `WITH QUERY EXPANSION`, Akzent- und Sortierungsgleichheit, der `FULLTEXT`-Index einer Tabelle `db.t` im Katalog von `db`, `MATCH` über `FROM db.t` einer anderen Datenbank (`MariaDbVolltextFremdeDatenbankTests`); **Runde 8: der Boolean Mode mit `>` `<` `~` und Klammern beliebiger Tiefe, Präfixe jeder Länge** (`MariaDbVolltextOperatorenTests`: 65 Einzelanfragen, 260 Zufallsanfragen, 8 Syntaxfehler 1064 wie am Server) — die „nicht erklärte winzige Rangfolge“ der kurzen Präfixe war `df` als Zahl der Einträge (Wort, Zeile) mit negativem Logarithmus bei mehr Einträgen als Zeilen, die Regeln stehen in `docs/umsetzung.md`; ein alter Test erwartete für `fox*` die Zahlen von `fox` und ist berichtigt. `ALTER` mit mehreren Änderungen samt `ON UPDATE` und `RENAME COLUMN`, `CREATE TABLE … LIKE`, `IF NOT EXISTS`-Fehler, Zeichensätze von Tabellen und Spalten samt 1366, `SET time_zone` und TIMESTAMP als UTC. **Runde 8, ebenfalls gemessen:** das Nulldatum `0000-00-00` und der 1. Januar 0001 sind zwei Werte (`MariaDbNulldatumUnterscheidungTests`; vorher las die Engine `0001-01-01` sogar als `1900-01-01`); `ON UPDATE CURRENT_TIMESTAMP` ist eine Spalteneigenschaft im `UPDATE`-Pfad statt eines erzeugten Triggers und gilt damit auch an `TEMPORARY`-Tabellen, an Tabellen einer anderen Datenbank und ohne Primärschlüssel (`MariaDbOnUpdateDirektTests`; `MariaDbOnUpdateAlterUebersetzer` ist gelöscht); `ALTER TABLE … CONVERT TO CHARACTER SET` wandelt Daten, Sortierung und die `TEXT`-Stufe wirklich um (`MariaDbConvertZeichensatzTests`). **Rest als eigene Zeile D17.6f.** ✅
D17.6f Typumwandlung nach `binary` **Erledigt (M17, Runde 9), am echten `mariadb:11` (11.8.9) gemessen (`MariaDbConvertZeichensatzTests`):** `ALTER … CONVERT TO CHARACTER SET binary` macht aus `VARCHAR(n)` `VARBINARY(n)`, aus `TEXT`/`MEDIUMTEXT` `BLOB`/`MEDIUMBLOB`, aus `CHAR(n)` `BINARY(n)` mit Nullbytes aufgefuellt (`6A6B000000`, `LENGTH` 5), die Daten werden zu ihren Bytes (`latin1` ein Byte, sonst UTF-8), die Tabelle steht auf `DEFAULT CHARSET=binary`, und die Spalten bleiben danach Binaerspalten, wenn zurueck nach `utf8mb4` umgestellt wird. Nebenbei: `LENGTH` eines Binaerwertes zaehlte die Zeichen der Hex-Schreibweise, jetzt die Bytes. **Rest als eigene Zeile D17.6g.** ✅
D17.6g Rest: FULLTEXT-Formen mit abweichender Reihenfolge **Erledigt (M17, Runde 10), als Black-Box am echten `mariadb:11` (11.8.9) gemessen (138 Anfragen, `MariaDbVolltextGruppenTests`), kein Quelltext gelesen:** die Formen, die `MATCH` bisher mit 1235 abwies, rechnen jetzt wie der Server, `MariaDbVolltextSuche` kennt keine Ablehnung mehr. Die Regeln: (1) kommt ein Wort mehrfach in der Anfrage vor (auch als Präfix, in einer Phrase oder in einer Gruppe), zählt seine Häufigkeit `df` so oft, wie es vorkommt (`alpha alpha` bei zehn Zeilen und `df` 5 ist `df` 10, also der Sonderfall `log10(1.0001)` mit 1.9e-9), seine Relevanz aber je Zeile nur einmal, beim ersten Teil davor (ausser `~`), der die Zeile trifft; (2) die Teile einer Liste wirken der Reihe nach auf die Menge der Zeilen: ohne `+` fügt jeder Teil Zeilen hinzu und ein `-` nimmt die bis dahin enthaltenen heraus (samt Relevanz, `alpha -delta (beta gamma)`), mit `+` beginnt die Menge beim ersten `+`, jedes weitere schneidet, eine Gruppe ohne Operator dahinter fügt hinzu (`+alpha (beta gamma) +delta` ist Zeilen mit `delta` und mit `alpha` oder der Gruppe), ein `-` vor dem ersten `+` hält die Zeile aus der Menge; (3) nach einem `-` holt ein Wort die Zeile nicht zurück, eine Gruppe schon; (4) der Überschuss von `>` und `<` einer Gruppe zählt nur als Vorzeichen (`>(<alpha <beta)` wertet nicht ab); (5) ein verlangtes Stoppwort bleibt als Teil stehen, den keine Zeile erfüllt, eine Gruppe dahinter fügt Zeilen hinzu und nach ihr auch ein Wort (`+the (alpha beta) gamma`). **Erledigt in Runde 9 (aus dieser Zeile):** `TINYTEXT`/`TINYBLOB`, `SET`, `BIT(n)`, `BOOL`, `FLOAT` zeigen sich in `SHOW COLUMNS`, `SHOW CREATE TABLE` und `information_schema.COLUMNS` wie am Server (`MariaDbSpaltenartTests`). ✅
D17.6h Rest: FULLTEXT, ein Wort nach einer Gruppe bei echtem ersten `+`, Präfixe über zwei Spalten **Erledigt (M17, Runde 10), am echten `mariadb:11` (11.8.9) gemessen (K1 bis K12, `MariaDbVolltextRunde5Tests`):** nach einer Gruppe holen Wörter Zeilen in die Menge (auch bei einem echten ersten `+`: `+alpha (beta) gamma` nimmt die Zeile mit nur `gamma`), eine Gruppe hebt die Sperre eines vorangehenden `-` auf, ein Wort holt aber keine Zeile zurück, die ein `-` nach der Gruppe getroffen hat; bei einem Index über zwei Spalten addieren sich die Häufigkeiten der Spalten (`alp*` in `alpha alpha delta` und `alpha zeta eta` ist 3). ✅
D17.6i FULLTEXT: Präfix mit mehreren passenden Wörtern in mehreren Spalten **Erledigt (M17, Runde 10), am echten `mariadb:11` (11.8.9) gemessen (M3 bis M12, `MariaDbVolltextRunde6Tests`):** bei einem Index über mehrere Spalten zählt das letzte passende Wort der ganzen Zeile in der Ordnung des Index (kürzeres Wort zuerst), und seine Vorkommen in allen Spalten werden gezählt (`foxy*`, `foxe*`, `fo*`); kommt das Präfix in allen Zeilen vor (der Sonderfall `df = N` mit `log10(1,0001)`), zählt bei mehreren Spalten nur die Zahl der Spalten mit einem Treffer (an `fox*` in einer Tabelle mit fünf Zeilen gemessen). ✅
D17.7 DML im MariaDB-Stil `INSERT … ON DUPLICATE KEY UPDATE` samt `VALUES()`, `INSERT IGNORE`, `REPLACE`, mehrzeiliges `INSERT`, `UPDATE`/`DELETE` mit `ORDER BY … LIMIT` und über mehrere Tabellen (`UPDATE t1 JOIN t2 … SET`, `UPDATE t1, t2 SET`, `DELETE FROM t1 USING …`, `DELETE t1 FROM … JOIN`; am echten `mariadb:11` gemessen, 30.09.2026), `RETURNING`, `LOAD DATA [LOCAL] INFILE`, `SELECT … FOR UPDATE`/`LOCK IN SHARE MODE`, `WITH RECURSIVE`, Fensterfunktionen ✅
D17.7b Sperrlesen und LOAD DATA **Erledigt:** `SELECT … FOR UPDATE`, `FOR SHARE` und `LOCK IN SHARE MODE` (nur im MariaDB-Dialekt; `Lesehinweis.Aktualisierung`/`Teilen`): in einer Transaktion bleiben die gelesenen Zeilen bis zu ihrem Ende für Schreiber gesperrt (`Sperrverwaltung.Erwerbe(…, nurSperre: true)` bzw. geteilte Sperre), gewöhnliche Leser werden wie bei InnoDB nicht aufgehalten, ohne Transaktion bleibt nichts zu halten. Dazu `START TRANSACTION` (fehlte). `LOAD DATA INFILE` auf den vorhandenen `BULK INSERT` (FIELDS TERMINATED/ENCLOSED, LINES TERMINATED, IGNORE n LINES; Datei durch dieselbe Freigabe wie jeder Dateizugriff; unlesbarer Wert → 1366/22007 mit zurückgenommener Anweisung, fehlende Datei → Fehler 13, `LOCAL` → 1148), gegen `mariadb:11` gemessen. 13 neue Tests. ✅
D17.7c Sperrlesen-Optionen und LOAD DATA im Einzelnen **Erledigt, jeder Sollwert am echten `mariadb:11` gemessen (zwei Sitzungen bzw. dieselben Dateien und Befehle):** `FOR UPDATE|FOR SHARE|LOCK IN SHARE MODE` mit `NOWAIT` (scheitert sofort mit 1205, dem Fehler des Zeitlimits), `WAIT n` (wartet n Sekunden, dann 1205, kommt durch, wenn die Sperre vorher endet) und `SKIP LOCKED` (gesperrte Zeilen fehlen, der Rest wird gesperrt; zwei neue `Lesehinweis`-Werte, `SelectStatement.SperrFrist`, Fehler 1222 heißt jetzt 1205). `LOAD DATA` liest die Datei jetzt selbst (`MariaDbLoadDataLeser`, rein, fuzzgeprüft) statt über `BULK INSERT` und erzeugt EIN mehrzeiliges `INSERT [IGNORE]|REPLACE`: Rückstrich-Fluchtfolgen und `\N` als NULL, angeführte Felder mit Trennern und Zeilenumbrüchen, das Wort `NULL` bei Anführung, `STARTING BY`, jedes `ESCAPED BY`, Spaltenliste mit `@var` und `SET spalte = ausdruck`, `REPLACE`/`IGNORE`, Feldzahl zu klein/groß im strengen Modus 1261/1262, und **`LOCAL INFILE`** (Paket 0xFB, die Fähigkeit `LOCAL_FILES` steht im Gruß; ohne sie des Clients weiter 1148; mit der echten `mariadb`-CLI und `--local-infile` geprüft). Konvertierungsfehler beim Einfügen jetzt 1366/1292 (22007) statt 1105. rund 40 neue Tests. ✅
D17.7e `LOAD DATA … CHARACTER SET` **Erledigt, am echten `mariadb:11` gemessen:** die Dateibytes werden nach der Angabe gelesen — `latin1`/`ascii`/`binary` byteweise (0xE4 ergibt ä), `utf8`/`utf8mb3`/`utf8mb4` und keine Angabe als UTF-8, ein unbekannter Name meldet 1115. Bei `LOCAL` gilt die Angabe nicht (der Client liefert Text). Drei Tests (`MariaDbLoadDataOptionenTests`); Sabotage (latin1 als UTF-8 gelesen) → genau der latin1-Test rot ✅
D17.7f Rest: Sperrlesen, LOAD DATA, nicht strenger Modus **Erledigt (M17, Runde 8), jeder Sollwert am echten `mariadb:11` (11.8.9) gemessen:** **der nicht strenge Modus** (`sql_mode = '` und jedes `INSERT IGNORE`) wandelt unpassende Werte um statt sie abzuweisen — zu langer Text gekürzt (1265), ein Text ohne Zahl ist 0 (1366), mit einer Zahl am Anfang deren Wert (1265), Zahlen ausser Bereich der größte/kleinste Wert (1264), ein Datum, das es nicht gibt, ist das Nulldatum (1265), ein Wert ausser Bereich von `UNSIGNED`, `TINYINT`, `MEDIUMINT`, `BOOL`, `DECIMAL UNSIGNED` ist die nächste Grenze des Typs, bei `YEAR` 0000, mit 1264 (elf gemessene Fälle), `NULL` in `NOT NULL` ist bei mehreren Wertezeilen und im `UPDATE` der Nullwert mit 1048, eine weggelassene Spalte ohne Vorgabe der Nullwert mit 1364, jede Warnung mit `at row n` (`MariaDbNichtStrengTests`, 18 Fälle); `LOAD DATA` meldet 1261/1262 je Zeilenform und 1263 für `\N` in `NOT NULL`; **`LOAD DATA LOCAL` wirkt wie `IGNORE`** (nur Warnungen, auch im strengen Modus; der alte Test, der nach `zwei,falsch` einen Fehler erwartete, hielt Abweichendes fest) und ein **fehlendes Feld ist der Nullwert des Typs** (`'`, 0, `0000-00-00`), nur eine Spalte mit der Vorgabe NULL bekommt NULL, ein Feld zu viel entfällt (`MariaDbLoadDataLokalIgnoriertTests`); Festbreite-Felder (Breite = Anzeigebreite des Spaltentyps in Bytes, `@var` meldet 1409, Rest-Leerzeichen gekürzt) und die Fehlermeldung bei 1366/1264/1406 mit Spalte, Wert und Zeile (D17.7g) waren schon da. **Rest als eigene Zeile D17.7h.** ✅
D17.7h Typbedingungen ohne strengen Modus: ENUM, SET, YEAR, BIT **Erledigt (M17, Runde 9), jede Meldung am echten `mariadb:11` (11.8.9) gemessen (`MariaDbNichtStrengAufzaehlungTests`):** ein ungültiger `ENUM`-Wert ist ohne strengen Modus der Leerwert mit 1265 (`Data truncated for column ...`, auch im `UPDATE`), ein `SET` behält seine gültigen Teile (`x,q` ergibt `x`, `q` den Leerwert), im strengen Modus ist es 1265 (nicht mehr 1406 `Data too long`, weil der Wert länger als jeder erlaubte ist); `YEAR` nimmt zweistellige Jahre an (1 bis 69 sind 2001 bis 2069, 70 bis 99 sind 1970 bis 1999, in jedem Modus), ausser Bereich ist 0 mit 1264 und `abc` 0 mit 1366; `BIT(4)` über 15 ist 15 mit 1264; eine Spalte, die ein `UPDATE` nicht anrührt, wird nicht neu geprüft (der Leerwert eines `ENUM` bleibt auch im strengen Modus bei jedem anderen `UPDATE` stehen). **Rest als eigene Zeile D17.7i.** ✅
D17.7i Sperrlesen unter SNAPSHOT, ENUM/SET-Nummern, Upsert über jeden eindeutigen Schlüssel **Erledigt (M17, Runde 9):** `FOR UPDATE`/`LOCK IN SHARE MODE` halten die Zeilen jetzt in jeder Isolationsstufe, auch unter `SNAPSHOT` und `READ_COMMITTED_SNAPSHOT` (`HalteZeilen`, `MariaDbSperrlesenTests`); eine Zahl in einer `ENUM`-Spalte ist die Nummer des Mitglieds, in einer `SET`-Spalte die Menge der Bits, ein `SET` steht in der Reihenfolge der Vereinbarung (`INSERT … VALUES (4, 2, 5)` ergibt `b` und `x,z`, am Server gemessen), eine ungültige Nummer (0, zu groß, `''`) ist im strengen Modus 1265, bei `YEAR` 100 1264; `INSERT … ON DUPLICATE KEY UPDATE` erkennt eine Doppelung in jedem eindeutigen Schlüssel (Primärschlüssel zuerst, dann die `UNIQUE`-Indizes) und ändert die erste Zeile, die einen davon teilt (`MariaDbUpsertSchluesselTests`); `INSERT IGNORE`, `REPLACE` und damit `LOAD DATA … IGNORE|REPLACE` erkannten schon jeden eindeutigen Index (der alte Vermerk „nur über den PRIMARY KEY“ stimmte nicht mehr). **Rest als eigene Zeile D17.7j.** ✅
D17.7j ENUM/SET/YEAR als Wert **Erledigt (M17, Runde 10), am echten `mariadb:11` (11.8.9) gemessen (Frage H3, `MariaDbWertetypTests`):** beim Lesen einer Spalte im MariaDB-Dialekt trägt der Wert eine Marke (`MariaWertmarken`: `ma:e:`/`ma:s:` mit den Mitgliedern, `ma:y`, im Feld `Xmlsammlung` des Wert-Typs, nur im Ausdruck, nicht in der Ablage), an der Rechnen, Vergleichen und Anzeige erkennen, was er ist: `n + 0` an einem `ENUM` ist die Nummer des Mitglieds, `s + 0` an einem `SET` die Menge der Bits, `n = 1` und `n = 2 OR n = 'c'` vergleichen die Nummer, ein `SET` wird mit Text in der Reihenfolge der Vereinbarung verglichen (`s = 'x,z'`), `FIND_IN_SET('z', s)` nennt die Nummer des Mitglieds (3, nicht die Stelle im Text 2), ein `YEAR` mit dem Wert 0 steht als `0000` (auch `CAST(y AS CHAR)`, während `HEX(y)` und `y + 0` 0 bleiben); ein `INSERT` ohne Spaltenliste nimmt eine Zahl ebenfalls als Nummer bzw. Bits. Der Weg lief über eine Marke am Wert statt über einen neuen Datentyp, damit Ablage, Katalog und die Leitung der anderen Dialekte unberührt bleiben. ✅
D17.7k ENUM/SET/YEAR in IN, BETWEEN, SUM, ORDER BY und weiteren Ausdrücken **Erledigt (M17, Runde 10), am echten `mariadb:11` (11.8.9) gemessen (J30 bis J93, `MariaDbWertetypRestTests`, 64 Anfragen):** `IN` und `BETWEEN` vergleichen je Eintrag die Nummer, wenn er eine Zahl ist, sonst den Text (`n IN (1,'b')`), Vergleiche mit Zahlen die Nummer (`n > 1`), mit Text den Text (`n < 'b'`, `c` ist grösser als `b` trotz Nummer 1); `SUM`/`AVG`, `ABS`/`FLOOR`, unäres Minus, `CAST … AS UNSIGNED|SIGNED` und ein einfaches `CASE n WHEN 1` rechnen mit der Nummer bzw. den Bits, `MIN`/`MAX`/`GREATEST`/`LEAST`/`UNION` mit dem Text, `ORDER BY`, `GROUP BY` und `DISTINCT` ordnen nach der Nummer, ein `YEAR` bleibt als `MIN(y)`, `COALESCE(y, 1)` und `y + 1` eine Zahl (0 statt 0000), steht aber in `CONCAT`, `LENGTH` und `LEFT` als `0000`; dazu `AVG` über ganze Zahlen als Dezimalwert mit vier Stellen (`2.0000`) und `GREATEST`/`LEAST` mit einem NULL als NULL. Die Datei von `LOAD DATA` wird ganz in einen Befehl gelesen (bis 128 MB; die Wahl, damit das Transaktionsprotokoll den Befehl hält). ✅
D17.7g Datenfehler mit Spalte, Wert und Zeile; Typbedingungen **Erledigt (M17), jede Meldung am echten `mariadb:11` (11.8.9) gemessen (`MariaDbDatenfehlerTests`, 18 Fälle; Nachspiel `np_fehler`, `np_typ`, `np_alter_typ` mit `--text`: 0 Unterschiede):** eine Negativzahl in einer `UNSIGNED`-Spalte (und jeder Wert ausser Bereich von `TINYINT`/`SMALLINT`/`MEDIUMINT`/`INT`/`BIGINT UNSIGNED`, `DECIMAL`/`FLOAT UNSIGNED`, `YEAR`) meldet **1264** `Out of range value for column 'v' at row 1` (vorher 4025 einer `CHECK`-Bedingung), `ENUM`/`SET` 1265, `BIT(n)` 1406, `INET4`/`INET6` 1292 mit Wert und Spalte (`Incorrect inet4 value: 'x' for column `db`.`t`.`c` at row 1`); die Bedingungen, die die Typübersetzung erzeugt, tragen jetzt einen eigenen Namen (`TY<Art>__tabelle__spalte`) und sind von der `CHECK`-Bedingung des Anwenders (4025, `CONSTRAINT `t.v` failed for `db`.`t``, `CONSTRAINT_n`) unterscheidbar. Umwandlungs-, Bereichs- und Längenfehler nennen Spalte, Wert und die **Zeilennummer** (`at row 2`; beim `UPDATE` die wievielte Zeile gelesen wurde), ein ausdrückliches NULL ist 1048, eine weggelassene Spalte ohne Vorgabe 1364, `INSERT … VALUES (…, 'nodate')` 1292, `'zz'` in `DECIMAL` 1366 (vorher 1105 bzw. ohne Wert). **`ALTER TABLE … MODIFY … UNSIGNED` und `ADD CHECK` prüfen den Bestand** (scheitert mit 1264 bzw. 4025 samt Zeilennummer; meldet die Zeilenzahl wie der Server), die Bedingungen des alten Typs entfallen mit der Änderung, eine unbenannte `ADD CHECK` bekommt `CONSTRAINT_n`. `BOOL` ist `TINYINT(1)` (jeder Wert von -128 bis 127; vorher nur 0/1), `SET` prüft seine Werte, `INET4` lässt NULL durch. Drei bestehende Tests hatten 4025/„nur 0 und 1“ als gegeben genommen und folgen jetzt dem Server. ✅
D17.7l ALTER … MODIFY ENUM/SET, das nur Mitglieder anhängt **Erledigt (M17, Runde 10), am Server gemessen:** hängt `MODIFY e ENUM(…)` oder `SET(…)` nur Mitglieder ans Ende, baut der Server die Tabelle nicht um und meldet 0 betroffene Zeilen (`MariaDbAlterAufzaehlungTests`); jede andere Änderung der Liste und jede Typänderung meldet die Zeilenzahl. ✅
D17.7m UUID prüft das Format **Erledigt (M17, Runde 10), am echten `mariadb:11` (11.8.9) gemessen (N2 bis N12, `MariaDbUuidTests`):** zweiunddreißig Hexziffern in beliebiger Schreibung, Bindestriche an beliebigen Stellen zwischen den Ziffern (auch `123e4567e89b-12d3-…` und `123e-4567-…`), die Ausgabe klein mit 8-4-4-4-12; nicht angenommen (1292 mit Wert und Spalte): geschweifte Klammern, ein Bindestrich am Anfang, 31 oder 33 Ziffern, andere Zeichen, ein Leerzeichen am Ende. ✅
D17.7n UUID: Bindestriche am Ende, doppelt und vor der letzten Ziffer **Erledigt (M17, Runde 11), am echten `mariadb:11` (11.8.9) gemessen (P2 bis P5, `MariaDbUuidTests`):** ein Bindestrich am Ende ist 1292, zwei hintereinander und einer vor der letzten Ziffer werden angenommen, eine Schreibweise mit einem Bindestrich nach jeder Ziffer (31) wird abgelehnt; der Nachbau nimmt bis zu fünf Bindestriche an. ✅
D17.7o UUID: Bindestriche bis dreißig **Erledigt (M17, Runde 11), am echten `mariadb:11` (11.8.9) gemessen (S2 bis S8 und U2 bis U11, `MariaDbUuidTests`):** mit 5 bis 30 Bindestrichen nimmt der Server die Schreibweise an, mit 31 (einer nach jeder Ziffer) lehnt er sie ab; der Nachbau nimmt bis zu dreißig an. ✅
D17.7d Mehrfachziele in UPDATE/DELETE **Erledigt, jeder Sollwert (Zeilen und betroffene Zeilen) am echten `mariadb:11` mit denselben Daten gemessen:** `UPDATE t1 JOIN t2 … SET t1.a = …, t2.b = …`, `UPDATE t1, t2 SET …`, `DELETE t1, t2 FROM …` und `DELETE FROM t1, t2 USING …` (`MariaDbMehrfachzielUebersetzer`). Zuerst schreibt eine Anweisung die **Schlüssel der Treffer** (bei UPDATE dazu die neuen Werte, vor jeder Änderung berechnet — `mzb.x = mza.v + 100` nimmt wie am Orakel das *alte* `mza.v`) in die Sitzungstabelle `#__kmt`, danach ändert je Zieltabelle eine Anweisung über diese Schlüssel; das Ganze steht in einer eigenen Transaktion bzw. einem Sicherungspunkt (scheitert das zweite Ziel, ist das erste zurückgenommen — Test mit Fremdschlüssel), und die **Summe** der betroffenen Zeilen aller Ziele kommt als OK-Zähler zurück (`@__kmt_zeilen`, über Text- und vorbereiteten Weg). LEFT JOIN ohne Gegenstück löscht nur links. Voraussetzung: jede Zieltabelle hat einen Primärschlüssel im Katalog — sonst bleibt es ein ehrlicher Syntaxfehler (1064). Dabei ein Fund: der Platzhalter-Zähler der vorbereiteten Anweisungen hielt jedes `#` für einen Kommentar (die Normalisierung hat sie längst entfernt) und übersah alle `?` hinter einer Sitzungstabelle. 8 neue Tests; zwei alte, die den Syntaxfehler festschrieben, sind umgedreht. ✅
D17.8 Die Typen und die Funktionsbibliothek Alle MariaDB-Typen samt `UNSIGNED`, `ENUM`, `SET`, `YEAR`, `BIT`, `JSON`, `UUID`, `INET4/6`; die Funktionen `CONCAT_WS`, `IFNULL`, `IF`, `GROUP_CONCAT`, `DATE_FORMAT`, `STR_TO_DATE`, `NOW`, `UNIX_TIMESTAMP`, `JSON_*`, `REGEXP_*` und die übrigen, jede gegen das Orakel gemessen ✅
D17.8b Die Restfunktionen der Bibliothek (Bestandsaufnahme docs/umsetzung.md#d178) `CAST/CONVERT AS UNSIGNED|SIGNED [INTEGER]` (rundet numerische Quellen, schneidet stringförmige, HEX/BIN/OCT je mit eigener dritter Regel — alle gegen mariadb:11 gemessen), `HEX`/`UNHEX`/`BIN`/`OCT`/`CONV`/`CRC32`, `ELT`/`FIELD`, `DECIMAL/FLOAT/DOUBLE UNSIGNED` als Spaltentyp, `CHARACTER SET`/`COLLATE` je Spalte, `SHA2(x,224)` (eigenes SHA-224 nach FIPS 180-4, .NET hat keins), `PASSWORD()`/`AES_ENCRYPT`/`AES_DECRYPT` ✅
D17.8c `ENCRYPT()` **Erledigt, jeder Sollwert vom echten `mariadb:11` (glibc-`crypt`) einmalig gemessen und als Bekannt-Antwort festgehalten:** `MariaDbDesCrypt` ist das klassische Unix-`crypt(3)` mit DES, im eigenen Code (S-Boxen, Schlüsselplan und die vom Salz abgewandelte Expansion — .NETs `DES` kann das Salz nicht); `ENCRYPT(str, salz)` liefert Salz + 11 Zeichen (21 Vektoren, darunter Salz `./`, `zZ`, ein Kennwort über 8 Zeichen — auf 8 Byte gekürzt —, Umlaute als UTF-8), `NULL` bei NULL und bei einem Salz unter zwei Zeichen, `*0` bei einem Zeichen außerhalb des Alphabets, leere Zeichenkette bei leerem `str`, ohne Salz ein zufälliges. Eine Sabotage in einer S-Box macht 23 von 33 Tests rot. 33 neue Tests. ✅
D17.8d Geometrie-Grundlagen und `ENCRYPT` mit `$1$`/`$5$`/`$6$` **Erledigt, jeder Sollwert am echten `mariadb:11` gemessen:** (1) `ENCRYPT(str, '$1$salz$')` als MD5-crypt und `$5$`/`$6$` als SHA-crypt (`MariaDbHashCrypt`: Salz auf 8 bzw. 16 Zeichen gekürzt, `rounds=N$` von 1000 bis 999999999 — außerhalb, bei Nichtzahl und bei unbekannter Kennung `$n$` antwortet es `*0`, die Standardzahl 5000 fehlt in der Ausgabe; 19 Fälle, alle beim ersten Lauf gleich dem Server). (2) Geometrie auf dem vorhandenen Geo-Modell: die Spaltentypen `POINT`/`LINESTRING`/`POLYGON`/`MULTI…`/`GEOMETRYCOLLECTION`/`GEOMETRY` (als `geometry`), `ST_GeomFromText`/`ST_GeomFromWKB` (Aliase `ST_PointFromText` & Co. ohne Artprüfung wie bei MariaDB; ungültiger Text → `NULL`), `ST_AsText` in der Schreibweise von MariaDB (`POINT(1 2)`, Zahlen fest von 1e-15 bis unter 1e15, sonst `1e-16`/`1.2345678901234568e17`), `ST_AsBinary`, `POINT()`, `ST_X`/`ST_Y`, `ST_GeometryType`, `ST_SRID`, `ST_Distance`/`ST_Area`/`ST_Length`/`ST_NumPoints`/`ST_IsEmpty`, `ST_Contains`/`ST_Within`/`ST_Intersects`/`ST_Disjoint`/`ST_Equals`; auf der Leitung als `MYSQL_TYPE_GEOMETRY` mit vier Byte SRID vor dem WKB (`HEX(POINT(1,2))` gleich dem Server). Nebenbei behoben: ein `DOUBLE` stand im MariaDB-Port als `1.500000000000000e+000` — jetzt kürzeste Form wie beim Server (`SELECT SQRT(2)` gleich). 62 Tests (`MariaDbGeometrieTests`, `MariaDbEncryptTests`); Sabotage: Ausgabeform → 8 rot, SRID-Präfix → 3 rot ✅
D17.8k Geometrie: Beziehungen, Linien, GeoJSON, SPATIAL **Erledigt (M17, Runde 9), alles am echten `mariadb:11` (11.8.9) gemessen:** `ST_Touches`/`ST_Crosses`/`ST_Overlaps`/`ST_Relate`/`ST_Contains`/`ST_Within` an 23 Geometrien gegeneinander (529 Paare, 14 Spalten, `MariaDbGeometrieBeziehungTests`) über eine eigene DE-9IM-Matrix (`Geobeziehung`: Zeugenpunkte für Punkt und Kurve, Flächeninhalt von Schnitt und Differenz für die flächigen Zellen); die Befunde: `ST_Contains` meint „umfasst“ (ein Punkt auf dem Rand ist enthalten), `ST_Overlaps` heisst gleiche Dimension plus gemeinsame Punkte plus keiner umfasst den anderen (zwei sich kreuzende Linien überlappen, zwei Polygone mit gemeinsamer Kante auch), `ST_Crosses` ist nur für Punkt/Linie, Punkt/Fläche, Linie/Fläche und Linie/Linie definiert (sonst NULL) und rechnet wie Overlaps, die Ziffern 0 bis 2 in `ST_Relate` gelten wie `T`; **`ST_Union`/`ST_Intersection`/`ST_Difference`/`ST_SymDifference` an Linien gegen Linien** (324 Ergebnisse, `MariaDbGeoLinien`: zerlegen an den Schnittpunkten, vom kleineren zum größeren Ende ausrichten, nach Anfang und Ende ordnen, an Punkten mit genau zwei Stücken zu Linienzügen verketten, Geraden-Ecken entfallen); `ST_PointOnSurface` (Mitte des ersten Abschnitts der untersten Schicht der Ecken-Y-Werte, Quadrat mit Loch `POINT(5 1.5)`); GeoJSON: ungültige Struktur ist NULL mit 4048, abgeschnittenes JSON NULL mit 4037, kein JSON NULL ohne Warnung, `[1]` ist `POINT(1 0)`; `ST_IsSimple` einer Mehrfachlinie prüft die Teile untereinander; `SPATIAL`-Indizes erscheinen in `SHOW CREATE TABLE`, `SHOW INDEX` (`Index_type` SPATIAL, `Sub_part` 32) und `information_schema.STATISTICS` (kopiert mit `CREATE TABLE … LIKE`, entfernt mit `DROP INDEX`/`DROP TABLE`, kein `MATCH`-Ziel); `ST_Buffer` eines Punkts (126 Ecken im Uhrzeigersinn ab dem untersten Punkt, die Ecken 33 und 97 fehlen wie am Server), mit Abstand 0 der Wert selbst, mit negativem Abstand an einem Punkt leer, beim Schrumpfen eines konvexen Polygons ohne Löcher; die Einzelzahlen des Punktpuffers weichen in der letzten Dezimalstelle (ein Bit) ab. Außerdem: `JOIN` ohne `ON` ist in MariaDB-Sitzungen ein Kreuzverbund. **Rest als eigene Zeile D17.8g.** ✅
D17.8g ST_Buffer mit positivem Abstand an Linien, Polygonen und Mehrfachgeometrien **Erledigt (M17, Runde 10), 55 Messungen am echten `mariadb:11` (11.8.9) Punkt für Punkt gehalten (`MariaDbPufferRandTests`, Fixture `Orakel/buffer-runde4-5.tsv`, Toleranz 1e-9):** der Ring läuft im Uhrzeigersinn ab dem tiefsten (bei Gleichstand dem linkesten) Punkt; Rundungen (Ecken und Kappen) laufen in Schritten von 2π/128 und sind vom Ende der Rundung rückwärts auf dem Raster verankert, der Anfang (die Normale der eingehenden Kante) steht als eigener Punkt davor; eine einspringende Ecke ist der Schnittpunkt der verschobenen Kanten, ein Punkt auf der Geraden bringt keinen Punkt hervor; bei einer Linie fehlt der zweite Punkt der Anfangskappe (die am ersten Punkt), bei einer Linie mit drei oder mehr Punkten auch der der Endkappe; ein Polygon verliert keinen Punkt, seine Löcher schrumpfen und laufen ebenfalls im Uhrzeigersinn ab ihrem tiefsten Punkt, ein zugewachsenes Loch fehlt; eine geschlossene Linie ergibt Aussen- und Innenrand; die Teile einer Mehrfachgeometrie (auch `GEOMETRYCOLLECTION` aus Punkt und Linie) stehen in ihrer Reihenfolge als `MULTIPOLYGON`, berühren sie sich, werden ihre Puffer vereinigt (`MultiPolygon` zweier Quadrate ergibt ein Polygon); ein negativer Abstand an Punkten und Linien ist leer. ✅
D17.8n ST_Buffer: Linien, deren Kappen und Ecken sich überlappen **Erledigt (M17, Runde 10), 47 weitere Messungen am echten `mariadb:11` (11.8.9) Punkt für Punkt gehalten (L23 bis L47, J18, `MariaDbPufferRandTests`):** der rohe Rand einer Linie, die ihren eigenen Puffer berührt (`LINESTRING(0 0,4 0,4 4,0 4,0 1)`, spitze Winkel wie `(0 0,4 0,0 1)`, U-Formen mit Abstand über der halben Breite), wird an seinen Schnittpunkten (auch T-Stücken) zerlegt und behält nur die Kanten, die die Fläche in Hauptrichtung trennen (`MariaDbGeoAufloesen`); an einer einspringenden Ecke gilt der Schnittpunkt der verschobenen Kanten, solange er auf beiden Kanten liegt, sonst beide Endpunkte der verschobenen Kanten (die Schleife fällt bei der Auflösung weg); zwei gleiche Punkte hintereinander und ein Stachel ohne Fläche (A, B, A) entfallen; Löcher in Polygonen und Linien wachsen mit (L23 bis L26). ✅
D17.8o ST_Buffer mit negativem Abstand an Polygonen mit Loch und einspringender Ecke **Erledigt für die einfachen Gestalten (M17, Runde 11), 21 Messungen am echten `mariadb:11` (11.8.9) Punkt für Punkt gehalten (L1, L2, L4, L7, L8, L10, R1 bis R8, R10, R11, R13, R14, B14, `MariaDbPufferRandTests`):** der Ring läuft im Uhrzeigersinn, der Aussenring wird nach rechts (innen), ein Loch nach links (in die Fläche) versetzt; liegt der Versatz ausserhalb der Kurve, steht ein Bogen aus vier Punkten, nämlich Anfang `A`, dann `A + 64 s` und `A + 63 s` auf dem Raster (`s` die Richtung des Bogens: das Loch +1, der Aussenring -1), dann das Ende; innerhalb der Schnittpunkt der verschobenen Kanten (oder beide Endpunkte). Ein zusammenfallendes konvexes Polygon ist leer, solange der Abstand den Umkreis nicht erreicht. ✅
D17.8p ST_Buffer mit negativem Abstand: die Fälle, die sich aus den Messungen ableiten lassen **Erledigt (M17, Runde 9), 38 weitere Messfälle V1 bis V38 am echten `mariadb:11.8.9`:** Bögen mit Vier-Punkt-Gestalt bei rasterförmigen Normalen, Loch gegen Aussenring, Umkreis-Zusammenfall bei Rechtecken. Was nicht aus den Daten ableitbar ist, liefert hier 1105 statt eines falschen Ergebnisses (26 Fälle, u. a. L3, L5, L9, V1 bis V5, V8 bis V10 ohne Treffer, im Test als Ausnahmen benannt). Zwei Fälle liefern ein abweichendes Ergebnis (V21, V38); alles zusammen steht in D17.8q. ✅
D17.8q ST_Buffer negativ: das Rechteck jenseits des Umkreises (Schnitt der Kreisvielecke) **Erledigt (M17, Runde 12), 541 Messfälle W1 bis W541 in 0.05-Schritten am echten `mariadb:11.8.9` (`Orakel/buffer-runde10.tsv`):** beim Rechteck ist das Ergebnis leer unter dem Umkreis und über der kürzeren Seite, dazwischen der Schnitt der vier Kreisvielecke um die Ecken. Der Server schneidet die 128-Eck-Vielecke, nicht die Kreise: die Spitzen sind die Schnittpunkte zweier Sehnen, die Bogenpunkte die Rasterpunkte des absoluten Winkelrasters 2 pi / 128. Gemessen an 4x4 und 6x4; die Obergrenze (kurze Seite) ist nur an diesen zwei Rechtecken belegt. ✅
D17.8r ST_Buffer negativ als Flächenverschneidung (Loch erreicht Aussenring, L-Form, Bogenform, Fünfeck) **Erledigt (M17, Runde 13), 650 von 704 Messfällen exakt:** der Rohversatz je Ring (Vier-Punkt-Bogen an einspringenden Ecken auch neben dem Raster, voller Bogen im Raster 2 pi / 128 sonst) wird an allen Schnittpunkten zerlegt und nach der Windungszahl behalten (Aussenring plus, Loch minus), dazu der Schnitt mit dem Urbild. Daraus folgen ohne Sonderfall die Spitzen des Rechtecks, das Verschmelzen des Lochs mit dem Rand (MULTIPOLYGON), die L-Form, das Fünfeck (R9, V35, V36, W305 bis W334) und das Abschneiden an der Polygonkante (X auf der Kante bei R9). Das Rechteck behält seinen eigenen Weg (`Linse`). Die 54 Ausnahmen stehen in D17.8s. ✅
D17.8s Rest von ST_Buffer negativ: Rundungs-Artefakte, Pfeile der Bogenform, Loch-Dreieck **Erledigt bis auf eine begruendete Grenze (M17, Runden 10 und 11, 754 Messfaelle, 669 exakt; die 85 uebrigen benannt in `Orakel/buffer-ausnahmen.txt`):** gebaut ist der Puffer nach innen mit Umlaufsinn (ein im Uhrzeigersinn gegebener Ring wird spiegelverkehrt gerechnet, Y13/Y44), Zerlegung an allen Schnittpunkten, Behalten nach der Windungszahl, Schnitt mit dem Urbild. **Grenze, technisch nicht herleitbar:** alle 85 Faelle haengen nicht an der Gestalt, sondern an der Arithmetik der Geometriebibliothek des Servers, die nur aus ihrem Quelltext (GPL, fuer KuonenSQL ausgeschlossen - Regel wie D17.6g, nur Black-Box-Messung) ablesbar waere: (1) 43 Faelle mit zusaetzlichem `POINT` oder flaechenlosem Vieleck an einer Originalecke (Dreieck 6/5 bei 0.95, 1.10, 1.15, 1.45 bis 1.60, nicht bei 1.00, 1.05, 1.20 bis 1.40; Bogenform `POINT(4 4)`) - das Auftreten springt ohne Monotonie mit dem Abstand, entscheidend ist, ob eine interne Rechnung genau null ergibt; Black-Box ist das ohne die Rechenvorschrift Bit fuer Bit nicht vorhersagbar (die Flaeche davor stimmt jeweils exakt). (2) Bogenform ab 2.85 (W161 bis W164, Y2 bis Y4, Y6 bis Y20): vom Pfeilkreuz an der Spitze behaelt der Server einen Pfeil und die Raute, der zweite Pfeil hat dieselbe Windungszahl; welcher bleibt, haengt nur am Drehsinn, nicht an einer Groesse der Gestalt (alle vier Anfangspunkte gleich) - also an der Abarbeitungsreihenfolge der Bibliothek. (3) Loch-Dreieck am 45-Grad-Eck (V38, W529 bis W541, Y31 bis Y35, Y38/Y39, Y42/Y43, Y45 bis Y48): der Server schneidet den Bogenanfang an einer Geraden x+y=3-d*sqrt(2) ab, die aus keiner Rohkante entsteht; mit Aussenring 30x30, anderem Dreieck und umgekehrtem Umlaufsinn gemessen, keine Regel aus Messungen ableitbar. Jeder dieser Faelle ist im Test als benannte Ausnahme gefuehrt, die rot wird, sobald er doch stimmt. ✅
D17.8e Rest: Geometrie und Krypto (Teil 1) **Erledigt (M17), jeder Sollwert am echten `mariadb:11` (11.8.9) gemessen (`MariaDbGeometrieErweitertTests`, 109 Fälle):** `ST_StartPoint`/`ST_EndPoint`/`ST_PointN` (nur Linien), `ST_NumGeometries`/`ST_GeometryN` (nur Mehrfachgeometrien, sonst NULL), `ST_Envelope` (immer ein Fünfeck, auch für Punkt und Strecke), `ST_Centroid` (nur Flächen, nach Fläche gewichtet, mit Löchern), `ST_ExteriorRing`/`ST_NumInteriorRings`/`ST_InteriorRingN`, `ST_IsClosed`/`ST_IsRing` (ausserhalb von Linien -1), `ST_IsSimple`, `ST_Dimension`, die `MBR*`-Familie (`MBRContains`/`Within`/`Intersects`/`Disjoint`/`Equal(s)`/`Touches`/`Overlaps` über die Umschliessungen, auch entartet), `ST_AsGeoJSON` (mit Nachkommastellen und `bbox`) und `ST_GeomFromGeoJSON` (Point bis GeometryCollection, `Feature`, dritte Koordinate ignoriert, SRID-Argument), `ST_Union`/`ST_Intersection`/`ST_Difference`/`ST_ConvexHull` (der vorhandene Verschneider, Ringe in der Ordnung von MariaDB: Aussenring im Uhrzeigersinn, Anfang beim kleinsten X). **Runde 9, am echten `mariadb:11` (11.8.9) gemessen (`MariaDbGeometrieRestTests`):** `ST_SymDifference` (beide Differenzen in einem Wert, `MULTIPOLYGON` bei zwei Teilen; Anfangspunkt des zweiten Rings bei Schnittpunkten wie schon bei den Mengenoperationen nicht deckungsgleich), `ST_GeomFromText` mit dritter Koordinate oder `Z`/`M` ist NULL (dieser Server kennt keine Z/M-Werte), Geometriespalten sind mit `=`, `<>`, `<=>` vergleichbar (die Bytes), `LENGTH` einer Geometrie zaehlt die 25 Bytes der Leitung (SRID + WKB), vier Byte SRID + WKB als Hex-Literal in einer `POINT`-Spalte werden gelesen und alles andere ist 1416, `SPATIAL KEY`/`CREATE SPATIAL INDEX`/`ADD SPATIAL INDEX` werden angenommen und verlangen `NOT NULL` (1252). **Rest als eigene Zeile D17.8k.** ✅
D17.8m Rest: Geometrie (Beziehungen, Puffer, Kennzahlen), aufgegangen **Erledigt, in andere Zeilen aufgegangen:** `ST_Touches`/`ST_Crosses`/`ST_Overlaps`/`ST_Relate`, `ST_PointOnSurface`, Linie-gegen-Linie in den Mengenoperationen und GeoJSON-Warnungen stehen in D17.8k, der Punktpuffer und die gemessenen Formen des Linien- und Polygonpuffers mit ihrem Rest in D17.8g. Diese Zeile führt nichts mehr offen. ✅
D17.9 Katalog und Verwaltungsbefehle `SHOW DATABASES/TABLES/COLUMNS/INDEX/CREATE TABLE/VARIABLES/STATUS/PROCESSLIST/WARNINGS/GRANTS` (samt `LIKE`/`WHERE` und `FULL`), `DESCRIBE`/`DESC`, `USE`, `SET NAMES`, `SET [SESSION|GLOBAL] sql_mode`/andere Variablen, `KILL [CONNECTION|QUERY]`, `EXPLAIN`, `GRANT`/`REVOKE`/`CREATE USER`/`DROP USER` auf dieselben Anmeldungen wie T-SQL (S12.17) ✅
D17.9b `information_schema` im MySQL-Zuschnitt + ENUM/UNSIGNED-Rückübersetzung `information_schema` im MySQL-Zuschnitt für eine MariaDB-Sitzung (SCHEMATA/TABLES/COLUMNS/STATISTICS/KEY_COLUMN_USAGE/TABLE_CONSTRAINTS/REFERENTIAL_CONSTRAINTS/ROUTINES/VIEWS — Weiche nach `Sitzungsschalter.MariaDbDialekt`, eine T-SQL-Sitzung sieht weiterhin die eigene ISO-Sicht mit ANDEREN Spalten unverändert, siehe `SystemCatalogProvider.IsoAbfrage`); `SHOW CREATE TABLE`/`SHOW COLUMNS`/`information_schema.COLUMNS` lesen ENUM/UNSIGNED jetzt aus der CHECK-Einschränkung zurück (`MariaDbTypRueckuebersetzung`, EINE Stelle für alle drei); `SET(...)` bleibt mangels CHECK ein gewöhnliches `varchar(n)` ✅
D17.9c Feinkörnige Rechte und KILL QUERY **Erledigt:** die MySQL-Rechteliste bei `GRANT`/`REVOKE` (SELECT/INSERT/UPDATE/DELETE/EXECUTE/REFERENCES, `ALTER` auf Tabellen, `SHOW VIEW` → `VIEW DEFINITION`, auf `db.*` zusätzlich `CREATE`/`CREATE VIEW`/`CREATE ROUTINE`); Rechte ohne engeres T-SQL-Gegenstück (`TRIGGER`, `INDEX`, `DROP`, `ALTER` auf `db.*`, `SELECT ON *.*`) werden abgelehnt statt zu weit erteilt. Dazu ein Fund in der Engine: ein datenbankweites `GRANT SELECT TO u` wurde bei Objektzugriffen nie berücksichtigt (`PermissionManager.HasPermissionInherited`, DENY auf dem Objekt sticht). `KILL QUERY id` bricht nur die laufende Anweisung ab (Opfer: 1317/70100, Verbindung bleibt; über den Abbruch-Token der Engine, `Sitzungsdaten.BrichAnweisungAb`), `KILL CONNECTION` beendet jetzt wirklich die MariaDB-Verbindung (der Handgriff fehlte), unbekannte Kennung 1094; `WAITFOR` ist abbrechbar (schlief blind). 9 neue Tests, gegen `mariadb:11` gemessen (1094; 1317 nach der Fehlertabelle, am Orakel kein langlaufender Fall erzeugbar). ✅
D17.9d Rechte durchsetzen (EXECUTE), SHOW GRANTS, KILL QUERY über TDS **Erledigt, Sollformen am echten `mariadb:11` mit denselben Benutzern und Rechten gemessen:** `EXECUTE` auf Prozeduren wird durchgesetzt (ein Aufruf von aussen ohne das Recht scheitert mit **1370** `execute command denied to user 'u'@'%' for routine 'db.p'`; im Inneren einer Prozedur oder eines Triggers gilt die Besitzkette und es wird nicht erneut geprüft; Verwalter brauchen es nie) — das gilt auch für T-SQL-Clients, denn `EXEC` prüfte vorher nichts; `GRANT|REVOKE EXECUTE ON PROCEDURE|FUNCTION db.p` wird angenommen; ein verweigertes SELECT/INSERT/UPDATE/DELETE meldet **1142** mit Benutzer und `db.tabelle` (vorher 1105); `SHOW GRANTS` zeigt statt der groben Näherung die einzelnen Rechte je Ebene (`USAGE`, Datenbank, Tabellen, Routinen, in der Reihenfolge des echten Servers); `KILL QUERY id` von einer MariaDB-Verbindung auf eine **TDS-Sitzung** bricht deren laufende Anweisung ab (Fehler 50000 `Die Anweisung wurde durch KILL QUERY abgebrochen.`, die Verbindung bleibt; in der Ruhe geschieht nichts). 9 neue Tests. ✅
D17.9f Rechte an DDL und EXECUTE auf Funktionen **Erledigt, jede Meldung am echten `mariadb:11` (11.8.9) mit demselben Benutzer (nur `SELECT`) gemessen:** in MariaDB-Sitzungen verlangen `CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`, `DROP TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`, `TRUNCATE`, `ALTER TABLE`, `RENAME TABLE`, `CREATE`/`DROP INDEX` und `SHOW CREATE VIEW` ihr Recht (1142 `… command denied … for table `db`.`t``, bei `RENAME` `DROP, ALTER`; 1370 `alter routine`; 1044 fuer `CREATE ROUTINE`); `DROP`, `INDEX`, `TRIGGER`, `ALTER` (auch datenbankweit), `ALTER ROUTINE` und `CREATE TEMPORARY TABLES` lassen sich vergeben und stehen in `SHOW GRANTS` in der Reihenfolge des Servers; `EXECUTE` auf **Funktionen** wird beim Aufruf geprueft (1370). Dabei ein Fund: `GRANT ALL PRIVILEGES ON db.*` machte nur einen Datenbankbenutzer ohne Berechtigung (das erste `INSERT` scheiterte) — jetzt Mitglied von `db_owner`. Die Erkennung sitzt am MariaDB-Text (nicht am erzeugten T-SQL) und ist gegen Kommentare mitten im Schluesselwort gehaertet. 11 neue Tests. Reste in D17.9g ✅
D17.9e EXPLAIN aus dem Weg der Engine **Erledigt, jede Vergleichszeile am echten `mariadb:11` (11.8.9) mit denselben Tabellen und Daten gemessen:** `EXPLAIN` beschreibt den Weg, den die Engine beim Ausfuehren **wirklich** nimmt — dieselbe Entscheidung `Suchziel` (Indexsuche oder Durchlauf), ohne Zeilen zu lesen: `const`/`ref`/`range`/`ALL` mit `possible_keys`, `key`, `key_len` (nach Typ und Nullbarkeit), `ref`, `rows` (die Kandidaten des Indexes) und `Extra` (`Using where`, `Using index condition`, `Using filesort`, `Using temporary`, `Impossible WHERE …`, `No tables used`); Verbunde mit `Using join buffer (flat, BNLH join)` (die Engine sucht Partner ueber Faecher, nicht ueber Indizes). Was sie nicht beschreiben kann (Mengenoperationen, Unterabfragen, Tabellenfunktionen, `INSERT`), wird mit 1105 abgelehnt statt geraten. 10 neue Tests. Reste in D17.9h ✅
D17.9h EXPLAIN im Einzelnen **Erledigt (M17, Runden 8 und 9), jede Zeile am echten `mariadb:11` (11.8.9) mit denselben Tabellen gemessen (`MariaDbExplainRestTests`, `MariaDbExplainMengenTests`):** `UNION [ALL]` als `PRIMARY`/`UNION`/`UNION RESULT`, eine unkorrelierte Unterabfrage in der Auswahlliste als `SUBQUERY`, `EXPLAIN EXTENDED` mit `filtered`, `EXPLAIN INSERT`/`REPLACE … VALUES` und `INSERT … SELECT`; **Runde 9:** `EXCEPT` und `INTERSECT` (je ein Zweig: Zeilen `PRIMARY` und `EXCEPT`/`INTERSECT` mit `EXCEPT RESULT <except1,2>` bzw. `INTERSECT RESULT <intersect1,2>`), `UNION` und `EXCEPT` gemischt (`UNIT RESULT <unit1,2,3>`), abgeleitete Tabellen (einfach: der Server verschmilzt sie, es bleibt der Plan der inneren Abfrage als `SIMPLE`; mit Gruppieren, `DISTINCT` oder `LIMIT` ist es `PRIMARY` mit `<derived2>` (Zeilen der inneren Abfrage, bei `LIMIT n` höchstens n, `Using where` bei einer Bedingung der Hauptabfrage) und `DERIVED` mit `Using temporary`/`Using filesort`), und ein `UPDATE`/`DELETE` mit einer Bedingung ohne Index liest den Primärschlüssel (`type index`, `key PRIMARY`). **Runde 3 (22 weitere Pläne, `Weitere_Plaene_wie_der_Server`):** `EXCEPT`/`INTERSECT` mit beliebig vielen Zweigen (ein `RESULT <except1,2,3>`), `INTERSECT` vor `UNION`/`EXCEPT` und `UNION` mit `EXCEPT` in beiden Reihenfolgen als `UNIT RESULT`, `A EXCEPT B INTERSECT C` als abgeleitete Tabelle (`4|EXCEPT|<derived2>`, `DERIVED`, `INTERSECT RESULT <intersect2,3>`, `EXCEPT RESULT <except1,4>`), eine abgeleitete Tabelle mit Bedingung oder Sortierung der Hauptabfrage verschmilzt (`d.a` wird `a`, `a > 5` und `a < 9` schneiden sich zu einem Bereich mit 3 Zeilen), `DELETE` ohne Bedingung (`Deleting all rows`, keine Tabelle), `UPDATE` ohne Bedingung (`index`/`PRIMARY`), `ORDER BY <Primärschlüssel> LIMIT n` bei `DELETE`/`UPDATE`, `spalte IS NULL` als `ref`/`const`/`Using index condition`, `possible_keys` mit den Indizes aller Teile einer `AND`-Bedingung und `Using index condition; Using where`. **Rest als eigene Zeile D17.9k.** ✅
D17.9k Kostenplaner und EXPLAIN-Pläne des Servers, Kostenmodell im Clean Room **Erledigt (M17), jeder Plan und jede Kostenzahl am echten `mariadb:11` (11.8.9) gemessen:** die Engine plant einen MariaDB-Block selbst und führt genau den Plan aus, den `EXPLAIN` meldet - Durchlauf, überdeckender Indexdurchlauf, `ref`/`eq_ref`/`const`, Bereiche (auch über zwei Schlüsselteile), `index_merge` (union, sort_union), rowid filter, Verbundpuffer, `Range checked for each record`, Reihenfolge mit den kleinsten Kosten, Halbverbunde (Materialisierung, FirstMatch, LooseScan, DuplicateWeedout, herausgezogen), Antiverbunde (`MATERIALIZED`/`index_subquery`), MIN/MAX über die Indexenden, ORDER BY/GROUP BY/DISTINCT/LIMIT über Indizes. **Clean Room (Runde 19):** das Kostenmodell ist aus Black-Box-Messungen neu hergeleitet (Orakelrunden 1-10, k1, k2; Messreihen und Formeln in `docs/umsetzung.md`), kein Quelltext von MariaDB. Belegt: 160 + 24 Pläne gleich, alle 95 `FORMAT=JSON`-Pläne **mit den Kostenzahlen des Servers** (`MariaDbExplainJsonTests` vergleicht sie exakt, mit den Seiten, die die Engine schätzt), gut 100 Kostenzahlen und Seitenzahlen in `MariaDbKostenmodellTests`. **Grenze, genau benannt:** InnoDB rechnet mit seiner physischen Seitenbelegung - die Engine schätzt sie für Tabellen, die in der Reihenfolge des Schlüssels gefüllt sind (alle 13 gemessenen Tabellen exakt); nach Löschen oder ungeordnetem Einfügen (K85-K92: 161 statt 4 Seiten) und bei großen Tabellen, deren `n_rows` der Server nur stichprobenhaft schätzt, weichen Seiten und damit Kosten ab - dafür bräuchte es die Seitenbelegung von InnoDB selbst. Abgeleitete Tabellen im Verbund als eigene Zeile D17.9r. ✅
D17.9r Abgeleitete Tabellen im Verbund (`key0`, `LATERAL DERIVED`), Mengenoperationen als JSON **Erledigt (M17), jeder Plan und jede Kostenzahl am echten `mariadb:11` (11.8.9) gemessen (Runden 10, 11, k3):** eine abgeleitete Tabelle im Verbund (auch hinter FROM) plant die Engine selbst - einmal materialisiert mit `key0` über die verglichenen Spalten (Zeilen je Schlüssel ein Zehntel, höchstens 10, mindestens ein Hundertstel; DISTINCT eindeutig), gruppiert über eine Spalte mit Index innen als `LATERAL DERIVED` je Zeile davor für genau diesen Schlüssel gerechnet (die Ausführung tut das), eine einfache verschmilzt mit der Hauptabfrage (`SIMPLE`, ein Stern nennt nur ihre Spalten). Kosten aus den Traces k3: Nachbearbeiten n · (2 · (0.16097 + 0.002334) + 0.011361 · log2 n), je Schlüssel Zeilen mal das, Nachschlagen 0.16097 + Zeilen · 0.111724. `EXPLAIN FORMAT=JSON` dazu (`materialized`, `lateral`, `outer_ref_condition`) und für gemischte UNION/EXCEPT/INTERSECT (`<derivedN>` mit `<intersect…>`, `<unit…>`) sowie die abgeleitete UNION mit hinabgereichter Bedingung. Belegt: die Liste `explain-ausnahmen.txt` ist leer (D1-D20 gleich, mit Kostenzahlen), Ergebnisse von Hand nachgerechnet. ✅
D17.9g Rechte, die nicht durchgesetzt sind (Funktionen beim Lesen) **Erledigt (M17), jede Meldung am echten `mariadb:11` (11.8.9) mit denselben Rechten gemessen (`MariaDbDdlRechteRestTests`, 5 Fälle):** `DROP TRIGGER` (TRIGGER auf der Tabelle des Triggers), `CREATE OR REPLACE TABLE`/`VIEW` (DROP zusätzlich, auch für einen neuen Namen), `CREATE TABLE … SELECT` (INSERT auf der neuen Tabelle), `RENAME` (INSERT und CREATE auf dem neuen Namen, alle Paare), ein Name `db.name` einer anderen Datenbank (gegen deren Rechte); fehlende Rechte stehen in der Reihenfolge der Rechtebits des Servers (`INSERT, CREATE` / `CREATE, DROP`). Nebenbei: `CREATE OR REPLACE TABLE` wird übersetzt (`DROP TABLE IF EXISTS` + `CREATE TABLE`), und `DROP` auf ein unbekanntes Objekt meldet 1051/4092/1360/1305/1008 statt 1105 (`MariaDbDropUnbekanntTests`). **Runde 8, geprüft, nichts davon ist technisch unmöglich:** **die Rechte der Quelle von `CREATE TABLE … SELECT` waren schon durchgesetzt** (`SELECT` auf jeder Tabelle der Abfrage, auch im Verbund und in einer Unterabfrage, vor dem Anlegen; die Roadmap-Zeile war veraltet, jetzt mit `MariaDbDdlRechteQuelleTests` belegt); **Spaltenrechte** brauchen ein Rechtemodell mit Spaltenlisten, das die Engine auch für T-SQL nicht hat (`GRANT SELECT ON t(a)`) — die Sollformen sind am Server gemessen und hier festgehalten: nach `GRANT SELECT (a), UPDATE (a), INSERT (id, a) ON t` liest `SELECT a` (auch `COUNT(*)`), `SELECT b` meldet 1143 `SELECT command denied to user ... for column 'b' in table 't'`, `SELECT *` 1142 (Tabelle), `UPDATE t SET a = 1 WHERE id = 1` 1143 für die Spalte `id` (SELECT), `INSERT INTO t VALUES (…)` ohne Spaltenliste 1142, `SHOW GRANTS` nennt `GRANT SELECT (`a`), INSERT (`id`, `a`), UPDATE (`a`), REFERENCES (`id`) ON ...`, `SHOW COLUMNS` zeigt nur die erlaubten Spalten, `information_schema.COLUMN_PRIVILEGES` und `mysql.columns_priv` führen sie; ein T-SQL-Client bekommt keine DDL-Prüfung (sein Besitzmodell, so gewollt); das Opfer eines `KILL QUERY` über TDS sieht Fehler 50000 (SQL-Server-Fehlerzahlen, keine MariaDB-Zahl). **Runde 9:** das Recht `EXECUTE` auf eine Funktion wird beim Lesen der Anweisung geprüft (`PruefeFunktionsaufruf`, nur Abfragen von aussen, nicht in Routinenrümpfen), eine leere Tabelle oder ein nie erreichter Zweig meldet 1370 (`MariaDbDdlRechteFunktionLesenTests`; die Meldung ist am Server gemessen, ob er bei leerer Tabelle ebenso früh meldet, steht als Frage in Runde 2). Eine **unbekannte** Funktion war schon beim Lesen 1305 (`MariaDbFunktionPruefungTests`, vier Formen am Server gemessen: leere Tabelle, `AND` mit falscher Seite, `FALSE AND f()`, nur `HAVING`). **Rest als eigene Zeile D17.9j.** ✅
D17.9m Rest aus D17.9g: DDL-Prüfung über T-SQL, KILL-Nummer, Mehrdatenbank-Namen **Erledigt (M17), jeder Teil belegt:** die DDL-Prüfung über T-SQL prüft der Kern (S12.19j, am Orakel SQL Server 2025 gemessen: 262, 2760, 3701, 1088; `RechtemodellDdlTests`); Spaltenrechte und `WITH GRANT OPTION` seit D17.9j/D17.9l, die Weitergabe im Kern (S12.19j); die Mehrdatenbank-Namen bei Tabellen, Sichten, Prozeduren, Funktionen und Triggern seit D17.9p (`MariaDbMehrdatenbankObjekteTests`), ihre Fehlerfälle in D17.9q; **KILL-Nummer:** das TDS-Opfer eines `KILL QUERY` sieht jetzt die Meldung, die SQL Server für einen von aussen abgebrochenen Stapel hat — **3980**, Klasse 16, `The request failed to run because the batch is aborted, this can be caused by abort signal sent from client, or another request is running in the same session, which makes the session busy.` (am Orakel SQL Server 2025 aus `sys.messages` gelesen; vorher 50000 mit eigenem Text); die Verbindung bleibt, ein MariaDB-Opfer sieht weiter 1317 (`MariaDbKillTdsTests`, mit der alten Nummer rot). ✅
D17.9j Spaltenrechte, WITH GRANT OPTION (Anzeige), KILL-Fehlerzahl **Erledigt (M17, Runde 9), jede Meldung am echten `mariadb:11` (11.8.9) gemessen (`MariaDbDdlRechteSpaltenTests`):** die Spaltenrechte (`GRANT SELECT (a), UPDATE (a), INSERT (id, a) ON t`) gab es seit D17.16 — der alte Vermerk „Rechtemodell mit Spaltenlisten fehlt“ war veraltet; Runde 9 hat die Abweichungen zum Server geschlossen: `COUNT(*)` liest mit jedem Spaltenrecht, `SELECT *` ohne alle Spalten ist 1142 (Tabelle), nicht 1143, ein `INSERT` ohne Spaltenliste mit fehlenden Spalten 1142, und `SHOW GRANTS` führt Tabellen- und Spaltenrechte in **einer** Zeile (`GRANT SELECT, SELECT (`v`), INSERT (`id`, `v`), UPDATE (`v`) ON …`); `WITH GRANT OPTION` (Tabelle und Spalten) wird gespeichert (`PermissionManager.SetzeWeitergabe`), steht in `SHOW GRANTS` am Ende der Zeile und macht `IS_GRANTABLE` in `information_schema.TABLE_PRIVILEGES` zu `YES`; `REVOKE GRANT OPTION ON t FROM u` nimmt nur die Weitergabe, `REVOKE ALL PRIVILEGES, GRANT OPTION FROM u` jedes Recht bis auf `USAGE`; die Prüfung von `EXECUTE` auf eine Funktion beim Lesen der Abfrage kommt nach dem Tabellenrecht (`INSERT`/`UPDATE` ohne Recht sind 1142 vor 1370). Das Opfer eines `KILL QUERY` sieht in einer MariaDB-Sitzung 1317, ein TDS-Opfer die Nummer seines Protokolls (50000) — kein Fehler, die Nummern gehören dem Protokoll des Opfers. **Rest als eigene Zeile D17.9l.** ✅
D17.9l WITH GRANT OPTION durchsetzen **Erledigt (M17, Runde 9), jede Meldung am echten `mariadb:11` (11.8.9) mit zwei Benutzern gemessen (`MariaDbDdlRechteWeitergabeTests`, Fragen G5 bis G29):** wer an einer Tabelle die Weitergabe nicht hat, bekommt bei `GRANT` 1142 (`GRANT command denied … for table`), wer sie hat, gibt nur Rechte weiter, die er selbst an der Tabelle hat (sonst 1142 mit dem fehlenden Recht, bei `SELECT, INSERT` das erste fehlende), auch Spaltenrechte, `WITH GRANT OPTION` und `REVOKE`; ein unbekannter Empfänger ist 1133 mit dem Zustand 28000; ein Benutzer ohne Verwalterrecht darf kein `CREATE USER`/`DROP USER` (1227) und nicht die Rechte eines anderen lesen (1044, `to database 'mysql'`); nach einem `REVOKE` bleibt die Weitergabe als `GRANT USAGE ON `db`.`t` … WITH GRANT OPTION` stehen; `IS_GRANTABLE` ist an `TABLE_PRIVILEGES`, `COLUMN_PRIVILEGES` und `SCHEMA_PRIVILEGES` YES, `mysql.tables_priv` führt `Select,Grant`, und `WITH GRANT OPTION` auf einer ganzen Datenbank steht in `SHOW GRANTS`. Nicht gemessen und darum unverändert: die Weitergabe auf Datenbank- und Serverebene durch einen Nicht-Verwalter. ✅
D17.9i Benutzer, Rollen, Rechte und Datenbanken wie der Server **Erledigt (M17), jede Meldung am echten `mariadb:11` (11.8.9) gemessen (`MariaDbVerwaltungTests`, 18 Fälle; Nachspiel `np_admin2`, `np_rolle`, `np_dbzeichen`, `np_showkeep` mit `--text`: 0 Unterschiede bis auf Umgebung - Version, Laufzeit, Benutzername `root`/`sa`):** `RENAME USER` (Anmeldung, Benutzer, Rollen, Rechte in allen Datenbanken), `CREATE`/`ALTER`/`DROP USER` und `SET PASSWORD` mit den Fehlern des Servers, **Rollen** (global, als Serverrollen): `CREATE`/`DROP ROLE`, `GRANT rolle TO benutzer|rolle` (1959, 1133+1961, 1962), Rechte an eine Rolle, `SET ROLE`/`NONE`, `SET DEFAULT ROLE`, `CURRENT_ROLE()` - die Rechte einer Rolle gelten erst nach `SET ROLE`; `SHOW GRANTS` mit Kennwort-Abdruck (nur für Berechtigte), Rollen, ALL PRIVILEGES neben Tabellenrechten; `mysql.user` mit Rollen und `mysql.roles_mapping`; `CHARACTER SET`/`COLLATE` einer Datenbank (`CREATE`/`ALTER DATABASE`, `SHOW CREATE DATABASE`, `SCHEMATA`; 1115, 1273, 1253, 1302); die Diagnosefläche der Verwaltungsanweisungen; 1146 mit Datenbank, `USE` ohne Zugang als 1044. Dazu `CREATE TEMPORARY TABLE` (Sitzungstabelle, verdeckt die dauerhafte; `MariaDbTemporaerTests`, 7), `DROP … IF EXISTS`, `ALTER … IF [NOT] EXISTS` und `CREATE TABLE IF NOT EXISTS` mit den Hinweisen des Servers (`MariaDbObjektHinweiseTests`, 25), `TIMESTAMP` als eigener Typ, `TIME`/`YEAR`/`COLUMN_DEFAULT` wie der Server. ✅
D17.9n Rest aus D17.9i: Zeichensätze, ALL PRIVILEGES an Rollen, WITH ADMIN OPTION **Erledigt (M17), jeder Teil am echten `mariadb:11` belegt:** die Zeichensätze der Tabellen und Spalten sowie `@@character_set_database`/`@@collation_database` je Sitzung in D17.9o (`MariaDbDatenbankZeichensatzTests`); `ALL PRIVILEGES ON db.*` an eine Rolle ist ihr eigenes Recht (jedes Recht der Datenbank, keine Mitgliedschaft in `db_owner`, wirkt nur eingeschaltet, `SHOW GRANTS` zeigt `ALL PRIVILEGES`), `WITH ADMIN OPTION` wird durchgesetzt (ohne ADMIN 1698, `REVOKE ADMIN OPTION FOR`, `Admin_option`, `IS_GRANTABLE`) und ein Recht an eine Rolle in einer später angelegten Datenbank gilt sofort (S12.19, `RechtemodellMariaOrakelTests`). Sabotage beim Abschluss: `SHOW GRANTS` fasst die Einzelrechte nicht mehr zu `ALL PRIVILEGES` zusammen → der Orakeltest rot, zurückgebaut. ✅
D17.9o Zeichensätze je Datenbank (Teil von D17.9n) **Erledigt (M17, Runde 11), jede Zeile am echten `mariadb:11` (11.8.9) gemessen (`MariaDbDatenbankZeichensatzTests`, Y51 bis Y73):** `@@character_set_database` und `@@collation_database` folgen der aktuellen Datenbank (`USE`, `ALTER DATABASE`; sonst `utf8mb4`/`utf8mb4_uca1400_ai_ci`), und `SHOW VARIABLES` ebenso. Die Zeichensätze der Tabellen und Spalten (Vorgabe aus der Datenbank zur Anlegezeit, `SHOW CREATE TABLE` nennt einen Spaltenzeichensatz nur bei Abweichung, `information_schema.COLUMNS`/`TABLES`/`SCHEMATA`, `utf8mb4_bin` unterscheidet Gross und Klein) standen schon richtig und sind jetzt am Server belegt. Der Teil „Zeichensätze“ in D17.9n ist damit erledigt; dort bleiben die Rechte-Teile. ✅
D17.9p Mehrdatenbank-Namen bei Sichten, Prozeduren, Funktionen und Triggern (Teil von D17.9m) **Erledigt (M17):** `db.name` nennt die Datenbank auch bei `CREATE`/`DROP` von Sicht, Prozedur, Funktion und Trigger (bisher landete `CREATE VIEW db2.v` als Schema `db2` in der aktuellen Datenbank, `CALL db2.p()` fand nichts), `CALL db.p()` und `db.f(…)` laufen in der Datenbank der Routine (ihre Namen im Rumpf gehören dorthin), ein Trigger liegt in der Datenbank seiner Tabelle. `MariaDbMehrdatenbankObjekteTests` (die Sabotage, die Namen wieder als Schema zu lesen, macht ihn rot). Die Tabellen mit Datenbankname (`db.t`: SELECT, INSERT, UPDATE, DELETE, DDL, JOIN, `SHOW … FROM db`) standen schon. Damit entfällt die „Engine-Grenze“ in D17.9m (ein Name mit fremder Datenbank landet als Schema). ✅
D17.9q Fehlerfälle der Mehrdatenbank-Namen am Server messen **Erledigt (M17, Runde 18), jede Antwort am echten `mariadb:11` (11.8) mit denselben Anweisungen gemessen (Orakelrunde Z1 bis Z62, die verlorene Fragenliste Z1–Z45 neu geschrieben und erweitert; `MariaDbMehrdatenbankFehlerTests`, 36 Fälle):** im MariaDB-Dialekt ist der erste Teil von `db.name` immer eine Datenbank — Anlegen von Sicht, Prozedur, Funktion in einer unbekannten ist 1049, Aufruf, Lesen und `DROP` melden das Objekt mit dem Namen, wie geschrieben (1305 `PROCEDURE zq_b.p_fehlt`, 4092, 1360, 1051, 1146; bisher `dbo.p_fehlt` oder die aktuelle Datenbank), `IF EXISTS` als Hinweis mit derselben Nummer; `db.f()` ohne Funktion 1305 statt 1105; ein Trigger gehört in die Datenbank seiner Tabelle (1435 `Trigger in wrong schema`, auch ohne Datenbankteil am Trigger), eine Tabelle ohne Datenbankteil liegt in der Datenbank des Triggers; **der Rumpf einer Sicht liest in der beim Anlegen aktuellen Datenbank** (nicht in der der Sicht — bisher falsch, samt Test; `ViewDefinition.Namensdatenbank`, übersteht einen Neustart), der einer Routine in ihrer eigenen (eine fehlende Tabelle dort 1146 `zq_b.t`); `SHOW CREATE VIEW/PROCEDURE/FUNCTION/TRIGGER` auf Unbekanntes 1146 `db.v` bzw. 1305 mit Namen ohne Datenbank bzw. 1360, `SHOW TRIGGERS FROM db` liest die genannte Datenbank (unbekannt: 1049) in der Reihenfolge des Servers (Tabelle, Ereignis, Zeitpunkt, Anlegen), `SHOW PROCEDURE STATUS` und `SHOW FUNCTION STATUS` über alle Datenbanken, „already exists“ ohne Datenbankteil (nur der Trigger nennt sie). Sabotage: Namensdatenbank abgeschaltet → 2 rot; Meldungsnamen nicht umgeschrieben → 6 rot; je zurückgebaut. ✅
D17.10 Prozeduren, Funktionen, Trigger, Sichten `DELIMITER`-freie Körper über das Protokoll (der Anweisungs-Splitter hält `BEGIN…END`/`IF…END IF`/`WHILE…END WHILE` zusammen), `CREATE PROCEDURE`/`CREATE FUNCTION` mit `DECLARE … [DEFAULT …]`, `IF … THEN … [ELSEIF …]* [ELSE …] END IF`, `WHILE … DO … END WHILE`, `RETURN`, jeder Parameter/lokale Variable mit `@`-Präfix; `CALL` → `EXEC` (samt `OUTPUT` bei `OUT`/`INOUT`-Parametern); `CREATE TRIGGER … AFTER {INSERT|UPDATE|DELETE} … FOR EACH ROW` über einen erzeugten Cursor auf `inserted`/`deleted` (bei `UPDATE` über den Primärschlüssel korreliert) — ausgeführt auf demselben Ablauf-Ausführer wie T-SQL, kein zweiter Baum. `CREATE VIEW` braucht keine Übersetzung. **Erledigt; alles, was dieser Zeile folgte, steht in D17.10b bis D17.10i (alle ✅), es ist nichts Eigenes offen.** Gegen `mariadb:11` gemessen (`kuonensql-mariadb-orakel`, Port 13306), 8 neue Tests. ✅
D17.10b LOOP/REPEAT/LEAVE/ITERATE, SIGNAL, NOT-FOUND-Handler **Erledigt, gegen `mariadb:11` (Port 13306) gemessen:** `[marke:] LOOP … END LOOP`, `REPEAT … UNTIL … END REPEAT`, `[marke:] WHILE`, `LEAVE`/`ITERATE marke` (innerste Schleife → `BREAK`/`CONTINUE`, äußere → `GOTO` auf erzeugte Sprungmarke; `ITERATE` in `REPEAT` wertet die UNTIL-Bedingung vorher aus wie MariaDB), `SIGNAL SQLSTATE '45000' [SET MESSAGE_TEXT = …]` → `THROW 50000` (Antwort 1644/45000 mit Text), `DECLARE CONTINUE HANDLER FOR NOT FOUND aktion` (Aktion folgt jedem späteren `FETCH`, das nichts liefert; `FETCH c INTO v` → `FETCH NEXT FROM c INTO @v`). 6 neue Tests. ✅
D17.10c EXIT/CONTINUE-Handler für Fehler **Erledigt, gegen `mariadb:11` gemessen:** `DECLARE {EXIT | CONTINUE} HANDLER FOR {SQLEXCEPTION | <Fehlernummer> | SQLSTATE '…'}[, …] aktion` auf `TRY … CATCH` (`MariaDbRoutineUebersetzer.Handler.cs`): EXIT umschließt den Rest des Blocks, der äußere läuft danach weiter; CONTINUE umschließt jede Anweisung hinter der Deklaration; Handler gelten bis zum Blockende (auch der `NOT FOUND`-Handler jetzt); Fehlernummer und SQLSTATE werden über `Mariafehler` rückwärts auf die SQL-Server-Nummern abgebildet, ein nicht passender Fehler geht mit `THROW` unverändert weiter. 7 neue Tests. ✅
D17.10e Handler-Sonderformen, RESIGNAL, GET DIAGNOSTICS **Erledigt, jeder Sollwert am echten `mariadb:11` mit denselben Rümpfen gemessen:** `RESIGNAL` (gibt den Fehler des Handlers unverändert weiter, mit Zustand `THROW`), `SIGNAL SQLSTATE 'xxxxx'` mit beliebigem Zustand und `MYSQL_ERRNO` (kommt so beim Client an: 1234/45001; ohne Nummer 1644 mit dem Standardtext) — Zustand und Nummer reisen im Meldungstext zwischen zwei `U+0001` durch `THROW`, `Mariafehler` löst sie beim Client auf —, Handler auf einen eigenen Zustand oder eine eigene Nummer (`FOR SQLSTATE '45001'`, `FOR 1234`; die Bedingung liest den Meldungstext, ein nicht passender Zustand lässt den Fehler durch), `GET DIAGNOSTICS CONDITION 1 v = RETURNED_SQLSTATE|MYSQL_ERRNO|MESSAGE_TEXT` und `GET DIAGNOSTICS v = ROW_COUNT` (bei einem Engine-Fehler übersetzt in die MariaDB-Nummer und den Zustand: 1062/23000), und `LEAVE marke` einer beschrifteten `BEGIN … END`-Marke (auch aus einer inneren Schleife heraus). Ein CONTINUE-Handler fängt jetzt auch das `SIGNAL` der eigenen Anweisungsfolge (das lief vorher an ihm vorbei). 11 neue Tests. ✅
D17.10f `SELECT … INTO` und `NOT FOUND` **Erledigt, jeder Sollwert am echten `mariadb:11` gemessen:** `SELECT liste INTO var[, …] FROM …` im Prozedurrumpf (auch die Stellung hinter `FROM`, `DISTINCT`/`ALL`, mehrere Ziele, lokale Variablen, OUT-Parameter, Benutzervariablen) auf die Zuweisungsform von T-SQL; kein Treffer lässt die Variablen unverändert und löst den `NOT FOUND`-Handler aus (`CONTINUE` läuft weiter, `EXIT` beendet den Block, auch `SQLSTATE '02000'`; `GET DIAGNOSTICS` liefert `02000`/`1329`), ein Aggregat ohne Treffer ist eine Zeile und kein `NOT FOUND`, mehr als eine Zeile meldet 1172/`42000` (fangbar mit `FOR 1172`); dasselbe `EXIT HANDLER FOR NOT FOUND` beendet auch eine `FETCH`-Schleife. 12 Tests (`MariaDbSelectIntoTests`); Sabotage: `EXIT`-Signal abgeklemmt → genau die 4 `EXIT`-Tests rot, 1172-Prüfung abgeklemmt → genau die 2 1172-Tests rot ✅
D17.10i Rest: Handler und Diagnose **Teilweise erledigt (M17), am echten `mariadb:11` gemessen (`MariaDbHandlerRestTests`, 6 Fälle):** `FOR SQLWARNING` (fängt die Warnung 1329 eines leeren `SELECT … INTO`, als CONTINUE wie als EXIT — mehr Warnungen gibt die Engine nicht aus; ein Fehler geht vorbei), `RESIGNAL SET MESSAGE_TEXT = …, MYSQL_ERRNO = …` (der SQLSTATE des gefangenen Fehlers bleibt), `GET DIAGNOSTICS n = NUMBER` (1). Der **Engine-Fehler** „ein `OUT`-Parameter `VARCHAR(10)` nimmt die Länge des ersten zugewiesenen Werts an“ ist behoben (Ursache: das Binden des Aufrufer-Werts ohne Typ; jetzt nimmt jeder Wert den Typ des Parameters an, gegen SQL Server 2022 gemessen). **Nachtrag (D17.10i, erledigt, `MariaDbDiagnoseTests`, 24 Fälle, alle Werte am echten `mariadb:11` 11.8.9 gemessen):** `GET DIAGNOSTICS` liest alle Felder (`RETURNED_SQLSTATE`, `MYSQL_ERRNO`, `MESSAGE_TEXT`, `CLASS_ORIGIN` … `CURSOR_NAME`, `NUMBER`, `ROW_COUNT`) im Handler wie ausserhalb der Routine (Nummer ausser 1 ist 1758); der Text eines Engine-Fehlers ist der von MariaDB (`Duplicate entry '1' for key 'PRIMARY'`); `SIGNAL`/`RESIGNAL` mit allen Feldern, Zustand `01` als Warnung (1642), `02` als 1643, `00` als 1407 schon beim `CREATE`; `SQLWARNING` fängt jede Warnung (auch `SELECT 1/0` = 1365) und nimmt sie aus der Liste; Sitzungsvariablen (`@x`) im Rumpf von Prozedur, Funktion und Trigger (vorher ein Fehler 137) samt dynamischem Typ; `ROW_COUNT()` im Rumpf und die Summe im OK des `CALL`; ein Fehler im Rumpf beendet ihn (vorher lief er mit der nächsten Anweisung weiter und der Fehler erschien erst bei der übernächsten); 1406/1265/1264 bei zu langem oder zu grossem Wert einer Variablen oder eines Parameters; `OUT` nimmt den Wert des Aufrufers nicht an; `SELECT` im Rumpf nennt seine Spalten; Anweisungen ohne Tabelle lassen die Bedingungen der vorigen stehen; `CALL` einer fehlenden Prozedur ist 1305; `BEGIN` als Transaktionsbeginn. Dazu: der Hinweis 4094 (`At line n in db.routine`, je Routine, die der Fehler verlassen hat; die Zeile stammt aus Zeilenmarken im übersetzten Rumpf, `Routinenzeilen`), der `sql_mode` der Routine beim Anlegen, ein `UPDATE` meldet ohne `CLIENT_FOUND_ROWS` die geänderten statt der gefundenen Zeilen, `INSERT IGNORE` meldet die Doppelung als Warnung 1062, `@@warning_count`/`@@error_count`, `SHOW WARNINGS` mit ganzzahliger Nummer, ein `SELECT` mit gespeicherter Funktion löscht die Bedingungen der vorigen Anweisung. ✅
D17.10d BEFORE-Trigger **Erledigt, der Ablauf (Werte setzen, per `SIGNAL` ablehnen, vor dem Löschen protokollieren) mit denselben Triggern und Anweisungen am echten `mariadb:11` gemessen:** `CREATE TRIGGER … BEFORE {INSERT|UPDATE|DELETE} … FOR EACH ROW` wird ein `INSTEAD OF`-Trigger (`MariaDbRoutineUebersetzer.Vorher.cs`), der je Zeile den Rumpf mit `NEW.x`/`OLD.x` als typisierten Variablen ausführt und danach die Zeile mit den **geänderten** NEW-Werten selbst schreibt bzw. löscht; ein `SIGNAL` im Rumpf bricht die ganze Anweisung ab. `SHOW TRIGGERS` nennt sie `BEFORE`. Grenzen: je Tabelle und Ereignis nur EIN solcher Trigger (T-SQL-Regel; MariaDB erlaubt mehrere), die Spalten gelten zum Zeitpunkt von `CREATE TRIGGER`, UPDATE und DELETE brauchen einen Primärschlüssel. 5 neue Tests. ✅
D17.10g CREATE EVENT mit einem Zeitgeber **Erledigt, Sollwerte und Fehlernummern am echten `mariadb:11` (11.8.9) gemessen, der Zeitplan mit kurzen Takten (`EVERY 1 SECOND`):** `CREATE`/`ALTER`/`DROP EVENT`, `SHOW EVENTS`, `SHOW CREATE EVENT`, `information_schema.EVENTS`, ein Katalogobjekt je Datenbank, das den Neustart ueberlebt, und der Zeitgeber `MariaDbEreignisplaner` (ein Tick je Sekunde, voreingestellt aus wie `event_scheduler = OFF`, an mit `SET GLOBAL event_scheduler = ON` oder `KUONENSQL_MARIADB_EVENT_SCHEDULER=ON`), der faellige Rumpfe in einer eigenen MariaDB-Sitzung unter dem Definierer ausfuehrt; `STARTS`/`ENDS`/`AT` sind Ausdruecke, `ON COMPLETION [NOT] PRESERVE`, `ENABLE|DISABLE`, `COMMENT`, Recht `EVENT` (1044). 13 neue Tests. Reste in D17.10h ✅
D17.10h Ereignisse im Einzelnen **Teilweise erledigt (M17), am echten `mariadb:11` gemessen (`MariaDbEreignisRestTests`):** zusammengesetzte Intervalle (`EVERY '1:30' HOUR_MINUTE`, `DAY_HOUR`, `DAY_MINUTE`, `DAY_SECOND`, `HOUR_SECOND`, `MINUTE_SECOND`, `YEAR_MONTH`; Anzeige in `information_schema.EVENTS`, `SHOW EVENTS` und `SHOW CREATE EVENT` im Wortlaut mit Apostrophen, geplant wird mit der Summe in der kleinsten Einheit, ein Feld darf überlaufen, `'abc'`/`'0:0'` sind 1542) und der Eintrag `event_scheduler` in `SHOW PROCESSLIST` (bei eingeschaltetem Zeitgeber, `Daemon`, „Waiting for next activation“). **Offen:** eine fremde `DEFINER`-Angabe (der Anlegende ist immer der Definierer); `ON SLAVE`; die Warnung 1588 beim sofort verworfenen `AT` der Vergangenheit (nur das Verwerfen); ein Rumpf mit `DECLARE` (bräuchte eine Routine); gleichzeitige Rümpfe (sie laufen nacheinander); die Fortschrittsspalte `Progress` und `information_schema.PROCESSLIST` kennen den Zeitgeber-Faden nicht; die Nummer des Fadens ist frei gewählt **Weiter erledigt (D17.10h, Nachrunde):** Hinweise 1588/1544 und Warnung 1105, `DISABLE ON SLAVE`, fremde `DEFINER`-Angabe (1449), Rumpf mit `DECLARE`, `INTERVAL` in `STARTS`/`ENDS`, Zeitgeber in `information_schema.PROCESSLIST` (`MariaDbEreignisHinweiseTests`). **Runde 8, geprüft:** gleichzeitige Rümpfe sind eine Entscheidung des Zeitgebers (ein Faden, die Rümpfe laufen nacheinander; zwei Ereignisse zur selben Sekunde laufen in der Reihenfolge des Anlegens — am Server in einer Reihenfolge, die der Planer wählt); die Nummer des Fadens ist frei gewählt (am Server die nächste freie Verbindungsnummer, nicht vorhersagbar); beides ist kein Fehler, den ein Client sieht. **Runde 9, am echten `mariadb:11` (11.8.9) gemessen (`MariaDbEreignisGleichzeitigTests`):** jeder Rumpf eines fälligen Ereignisses läuft in einem eigenen Faden: ein langsamer Rumpf (`DO SLEEP(2.5)`) hält weder ein anderes Ereignis noch den nächsten Takt auf, dasselbe Ereignis überlappt sich selbst (acht Starts und acht Enden in acht Sekunden), und ein ausgeschalteter Zeitgeber lässt die laufenden Rümpfe zu Ende laufen; der Zeitgeber-Faden steht nur bei eingeschaltetem Zeitgeber in der Prozessliste, ohne laufende Rümpfe als eigene Zeilen; die Nummer des Fadens ist die nächste freie Verbindungsnummer beim Einschalten (so gebaut), die „freie Wahl“ und die „nacheinander“-Vermerke oben galten bis Runde 9. Nebenbei: `DO ausdruck[, …]` ist in MariaDB-Sitzungen eine Anweisung (wertet aus, wirft weg). Nichts Offenes. ✅
D17.11 Kurzmessung: bremst der MariaDB-Port den TDS-Port? **Kurz gemessen, ohne Dauerlast** (30.09.2026, ein Rechner, Release-Bau, 2000 Punktabfragen `WHERE id = ?` über EINE Verbindung je Protokoll, kürzeste von fünf Läufen; Werkzeug: MySqlConnector bzw. Microsoft.Data.SqlClient, nicht im Repo): TDS allein 818 µs je Abfrage, MariaDB-Port allein 153 µs; **TDS mit gleichzeitiger Last auf dem MariaDB-Port 870 µs (Faktor 1,06 — im Rauschen)**, MariaDB neben TDS 191 µs (Faktor 1,25); zum Vergleich der echte `mariadb:11` im Container über dieselbe Schleife 581 µs (dessen Weg führt über das Docker-Netz). **Was das NICHT belegt:** die Reihen sind nicht vergleichbar zwischen den Protokollen (TDS schickt jede Abfrage als parametrisierten RPC, MySqlConnector setzt den Wert clientseitig in den Text), es ist eine Verbindung je Protokoll und keine Belastung mit vielen — die Messreihe aus R13.5 mit mehreren Verbindungen und Dauerlast bleibt offen (**D17.11b**), weil dieser Auftrag ausdrücklich keine Dauerlast erzeugen sollte ✅
D17.11b Belastungsmessreihe (30 Minuten) **Erledigt (M17), volle 30 Minuten gemessen (`tools/dauerlast`, Rohdaten `tools/dauerlast/messung-2026-09-30-30min.txt`):** eine TDS- und eine MariaDB-Verbindung (pymysql) zugleich, je auf 50 Anfragen/s gedrosselt (Lesen, Ändern, Einfügen gemischt), eigener Serverprozess (Debug-Bau, eigene Ports), alle 60 s CPU-Anteil des Serverprozesses, Durchsatz, Working Set und offene Verbindungen. Ergebnis: Leerlauf 0,26 % eines Kerns; unter Last im Mittel 7,6 % eines Kerns (6,1-11,9 %, das sind 0,2-0,4 % der 32 Kerne), Durchsatz stabil 50/s je Protokoll, **90 000 MariaDB- und 90 000 TDS-Anfragen, 0 Fehler**, 2 offene Verbindungen über die ganze Zeit; das Working Set stieg von 88 auf 102 MB (die Tabelle wächst um rund 36 000 eingefügte Zeilen, das sind etwa 400 Byte je Zeile - ein Leck lässt sich daraus weder beweisen noch ausschliessen, ein Anstieg ohne Zeilenwachstum war nicht Gegenstand). Der erste Lauf (`messung-2026-09-30.txt`) hatte nach 28 von 30 Minuten durch einen Neustart der Sitzung geendet, dieser ist der vollständige. **Nicht Teil dieses Punkts, weil AGENTS.md es untersagt (CPU-/Speicherlast durch Agenten):** `sysbench` gegen einen echten MariaDB und mehrere Verbindungen je Protokoll - das bleibt eine Messung, die der Hauptchat auf Zuruf macht ✅
D17.12 Treiber-Abnahme (Teil 1: PyMySQL, mysql-connector, MySqlConnector/Dapper, `mariadb`-CLI, `mariadb-dump`) **Erledigt, am eigenen Server (Scratch, kein Fremdwerkzeug im Testlauf) mit den echten Treibern geprüft:** PyMySQL, mysql-connector-python (rein und C-Erweiterung, auch `prepared=True`), MySqlConnector 2.4 mit Dapper (Stapel, Parameter, Transaktion, vorbereitet) und die `mariadb`-CLI laufen. Drei echte Fehler gefunden: `@@session.sql_mode`/`@@autocommit`/`@@max_allowed_packet` u. a. (Tabelle jetzt EINE Quelle für `SELECT @@x` und `SHOW VARIABLES`, `@@session.`/`@@global.`-Vorsatz), das Ende einer Ergebnismenge war ohne `DEPRECATE_EOF` ein 7-Byte-OK statt des klassischen EOF (mysql-connector hing) und mit `DEPRECATE_EOF` ein OK mit Kopf `0x00`, `TIMESTAMP(x[, y])` fehlte (jeder `DateTime`-Parameter von MySqlConnector). **`mariadb-dump --single-transaction` → Wiederherstellung** läuft in beide Richtungen (KuonenSQL → echter MariaDB und zurück, Daten gleich): dafür `LOCK/UNLOCK TABLES`, `FLUSH PRIVILEGES|LOGS|STATUS|HOSTS|TABLES` (als OK **ohne** Sperre), `SAVEPOINT`/`ROLLBACK TO`/`RELEASE SAVEPOINT`, gemischtes `SET @alt=@@X, X=0`, `COMMIT`/`ROLLBACK` ohne Transaktion als stilles OK, `SELECT SQL_NO_CACHE …` (die folgenlosen Hinweise fallen weg, `SQL_CALC_FOUND_ROWS` bleibt Fehler), `FOREIGN_KEY_CHECKS=0` (Sitzungsschalter: kein Prüfen, keine Kaskade, Verweis auf spätere Tabelle, Elterntabelle darf gehen), **eigene Werte in `AUTO_INCREMENT`-Spalten** (waren ein Fehler 544; `0`/`NULL` vergeben den nächsten, `NO_AUTO_VALUE_ON_ZERO`, `LAST_INSERT_ID()` bleibt), unbenannter `FOREIGN KEY (a) REFERENCES …` (riss als Inline-`KEY` entzwei), Fehler 1452 (Kindzeile) statt 1451, `SHOW CREATE TABLE` mit Fremdschlüsseln/`text`/Zahlenvorgaben/Sichten, `SHOW CREATE VIEW`, `SHOW FIELDS`/`COLUMNS` auf Sichten, `SHOW TRIGGERS`/`FUNCTION|PROCEDURE|PACKAGE STATUS`/`EVENTS`, `ALTER TABLE … DISABLE|ENABLE KEYS`, `CREATE ALGORITHM=… DEFINER=… SQL SECURITY … VIEW`. rund 70 neue Tests. ✅
D17.12b Treiber-Abnahme, Teil 2 (PHP, Java, EF Core) **Erledigt, in Docker-Abbildern gegen den eigenen Server (Scratch, kein Fremdwerkzeug im Testlauf):** PHP `mysqli` und `PDO` (`php:8-cli`), MariaDB Connector/J 3.5 (`eclipse-temurin:21`) und EF Core mit Pomelo 9.0 (`dotnet/sdk:9.0`: `EnsureCreated`, Einfügen, Abfragen, Ändern, Löschen, Transaktion) laufen. Fünf echte Fehler gefunden und behoben: (1) ein zweites `COM_STMT_EXECUTE` ohne neue Typen (`new_params_bound_flag = 0`, so senden mysqli und PDO) las die Parameter als Text — 1366; (2) MariaDB Connector/J pipelinet `PREPARE` und `BULK_EXECUTE` mit der Kennung `0xFFFFFFFF` („die zuletzt vorbereitete“) — 1243; (3) `SET autocommit = 0` schaltete nicht auf implizite Transaktionen, `rollback()` nahm nichts zurück (jetzt `SET IMPLICIT_TRANSACTIONS`, das Einschalten schließt mit Commit ab, `@@autocommit` und das Statusbit folgen); (4) `@@performance_schema` fehlte (Connector/J fragt es beim Anmelden); (5) `DELETE … RETURNING 1` (Pomelo prüft so das Löschen) machte aus der Konstante `DELETED.1`; dazu `SAVEPOINT`/`ROLLBACK TO`/`RELEASE SAVEPOINT` mit Backtick-Namen. MySQL Connector/J (`com.mysql`) ist bewusst nicht Ziel: er fragt `@@transaction_read_only`, das es bei MariaDB nicht gibt (am Orakel gemessen). 12 neue Tests ✅
D17.12c Treiber-Abnahme, Rest **Erledigt (M17): 0 Unterschiede bei 90 Abfragen.** `tools/mariadb/gui_abfragen.py` spielt die Abfragen aus den QUELLTEXTEN von HeidiSQL (`dbconnection.pas`, `dbstructures.mysql.pas`) und DBeaver (`ext.mysql`) gegen einen echten `mariadb:11` und KuonenSQL nach und vergleicht je Abfrage Erfolg/Fehlernummer, Spaltennamen, Zeilenzahl **und** bei `SHOW CREATE TABLE|PROCEDURE|FUNCTION|TRIGGER|EVENT|DATABASE` den **Wortlaut** der Antwort (Definierer, Sortierung und Zeitpunkte herausgerechnet). Verlauf 48 → 9 → **0**. In dieser Runde: `SHOW CREATE PROCEDURE|FUNCTION|TRIGGER` im Wortlaut des Clients (`Catalog/MariaUrtext`, `MariaDb/MariaDbUrtext`: Kopf im Zuschnitt des Servers - `DEFINER`, Namen in Backticks, Merkmale je auf einer Zeile in der Reihenfolge des Servers, Rückgabetyp klein mit Anzeigebreite; Parameterliste und Rumpf verbatim samt Leerraum und Kommentaren; dauerhaft im Katalog, überholt sich selbst, wenn das Objekt neu angelegt wird), dazu `SHOW TRIGGERS` mit Rumpf und Definierer des Clients; **`CHECK`-Bedingungen in `SHOW CREATE TABLE`** (am Spaltenkopf, sonst `CONSTRAINT name CHECK (…)` hinter den Schlüsseln, `CONSTRAINT_1` für unbenannte), `AUTO_INCREMENT=n` in `SHOW CREATE TABLE` und `information_schema.TABLES` (der nächste Wert statt des Startwerts), Nachkommastellen im Standardwert (`DEFAULT 0.000`), **Kommentare an Tabellen und Spalten** (`COMMENT`: als `MS_Description` abgelegt, also auch in SSMS sichtbar; `SHOW CREATE TABLE`, `SHOW FULL COLUMNS`, `information_schema`), `Default` in `SHOW COLUMNS` ist ohne `DEFAULT`-Klausel SQL-NULL; **`!x` und Zeilenwerte** `(a,b)=(c,d)`, `<>`, `<`, `<=`, `>`, `>=`, `<=>`, `IN`, `NOT IN`, `ROW(…)` als Grammatik des gemeinsamen Parsers (`Dialekt.AusrufezeichenIstNicht`, `Dialekt.Zeilenwerte`); `mysql.user` lesbar (fehlte: die Sicht wurde nicht als Systemsicht erkannt), Spaltenkopf ohne Alias im Wortlaut des Clients (`CONCAT(`User`, '@', `Host`)`), `EXPLAIN` mit `db.tabelle` (fand die Tabelle nicht und nannte `db.tabelle`), die Abfrage von `mariadb-dump` auf `information_schema.FILES` (leer statt Fehler). Beim Nachprüfen der Sicherung gefunden und behoben: **ein Schlüssel in Backticks (`KEY `x` (`a`)`, so schreibt ihn `mariadb-dump` immer) wurde nicht erkannt** - jede Sicherung einer Tabelle mit Nebenschlüssel ließ sich nicht wiederherstellen; Routinenköpfe mit `DEFINER=`, `IF NOT EXISTS`, `COMMENT`, `CHARSET` an Parameter und Rückgabetyp; ein `-- Kommentar` vor dem `END` einer Routine verschluckte das `END`; `ALTER TABLE t COMMENT = '…'|ENGINE=…|CHARSET=…`. **`mariadb-dump --routines --triggers` läuft jetzt fehlerfrei durch und lässt sich in eine leere Datenbank zurückspielen** (Tabellen, Schlüssel, `CHECK`, Routinen, Trigger, Daten). **Benannt statt offen:** die GUIs selbst (DBeaver/HeidiSQL am Bildschirm, das Schema `mariadb` in SSMS) sind technisch nicht automatisierbar - sie brauchen eine Anzeige und Mausbedienung; belegt ist, was sie an den Server schicken (die Quelltext-Abfragen oben). `SHOW CREATE VIEW` gibt den (von KuonenSQL lesbaren) Text der Sicht zurück, nicht die vom Server umgeschriebene Form (`select `kunde`.`id` AS `id` …`) - nachgebaut wird der Umschreiber des Servers nicht. `LOCK TABLES` beantwortet OK ohne zu sperren (D17.12, ehrlich benannt: die Engine kennt keine Tabellensperre auf Zuruf des Clients). `TIMESTAMP` und `FLOAT` erscheinen als `datetime`/`double` (siehe D17.6e) ✅
D17.13 Dialekt-Beschreibung statt Dialekt-Schalter **Erledigt.** Nutzervorgabe: weitere Anschlüsse (Oracle, DB2, SAP MaxDB, Informix …) ohne Umbau des Kerns. Bestandsaufnahme in `docs/dialekte-architektur.md` (rund zwei Dutzend Stellen fragten `Sitzungsschalter.MariaDbDialekt`/`PostgreSqlDialekt` ab). Neu `Dialekte/Dialekt` (Eigenschaften mit T-SQL als Vorgabe: `LimitKlausel`, `WithRecursive`, `SperrklauselAmEnde`, `IfAlsFunktion`, `VorzeichenTypen`, `IntervallDatumsfunktionen`, `VergleichIstWert`, `TeilungDurchNullErgibtNull`, `GanzzahlTeilungNachkommastellen`, `ConcatMitNullErgibtNull`, `DatediffMitZweiArgumenten`, `FunktionsrechtBeimAufruf`, `AutowertWieAutoIncrement`; Methoden für eigene `@@`-Variablen, Katalogsichten und Kennwort-Ableitungen), `MariaDialekt`/`PgDialekt` in ihren Modulen, `Dialektregister` mit Selbstanmeldung je Modul. Die Sitzung hält **einen** Dialekt (`ExecutionEngine.SetzeDialekt`); das Protokoll trägt seine Kennung in Bit 20–27 (MariaDB = 1 = das bisherige Bit 1048576, also rückwärtsverträglich; PostgreSQL-Folgen spielt der Wiederanlauf jetzt auch mit ihrem Dialekt). `Prinzipalverwaltung` ruft keinen MariaDB-Code mehr. Wächter `DialektArchitekturTests`: kein Dialekt beim Namen ausserhalb der Module (Ausnahmeliste leer), Griffe in ein Modul nur mit begründeter Ausnahme (7 Dateien, Liste darf nur schrumpfen), ein fremder Testdialekt mit `LIMIT` übersteht den Neustart ✅
D17.13b Gemeinsame Nahtstellen der Protokoll-Anschlüsse **Erledigt.** Was MariaDB und PostgreSQL doppelt taten, steht an einer Stelle: `IProtokollanschluss` (Name, Port, Startangaben, eine Verbindung bedienen — die Einstellungen beider Anschlüsse implementieren sie), `Program.RunAnschlussAsync` (Hörer, Annahmeschleife, eigene Sitzung derselben Engine — vorher je Anschluss zwei Methoden), `Protokollanschluss.LiesPort` (Argument vor Umgebung, 1–65535, sonst aus), `Anschlusssitzung` (Anmelden bei der Sitzungsverwaltung, `KILL` von aussen, Abbruch der laufenden Anweisung, Aufräumen — auch für den Ereignis-Zeitgeber ohne Leitung) und `Anmeldevorpruefung` (gesperrt / Betriebssystem / ohne Kennwort / prüfen). Handschlag, Pakete und Anmeldeverfahren bleiben im Modul. **Fund dabei (Sicherheit):** eine Anmeldung `FROM WINDOWS` hat keinen Kennworthash und kam über MariaDB **und** PostgreSQL mit **jedem** Kennwort durch (sie galt als „ohne Kennwort“); jetzt abgewiesen (1045 bzw. `28000`), weil nur TDS das Kennwort im Klartext hat und es dem System vorlegen kann ✅
D17.13c Checkliste für einen neuen Anschluss **Erledigt.** `docs/neuer-dialekt.md`: welche Dateien ein neuer Anschluss (Oracle, DB2, SAP MaxDB, Informix …) anlegt (Dialekt-Beschreibung mit Kennung und Selbstanmeldung, `IProtokollanschluss`, Verbindung über `Anschlusssitzung`, Anmeldung nach `Anmeldevorpruefung`, Fehler-/Typcodes), wie Abweichungen beschrieben statt verzweigt werden (vorhandene oder neue Eigenschaft, Methode für Code, Text-Übersetzer nur als benannte offene Scheibe), das Orakel-Vorgehen, die Tests (Parser ohne Leitung, T-SQL-Gegenfall, Wiederanlauf, Robustheit, Sabotage) und die Roadmap. Der Wächter `DialektArchitekturTests` hält die Regel fest; `docs/dialekte-architektur.md` nennt, was bewusst noch im Kern liegt (Sitzungszustand eines Dialekts, gespeicherte MariaDB-Ableitungen, `pg_catalog`, MariaDB-Funktionsbibliothek) samt nächstem Schritt ✅
D17.14 WordPress-Abnahme **Erledigt (vier Stellen, an denen WordPress 7.1 scheiterte; `MariaDbWordPressTests`, Leitung, Schema wie bei WordPress).** (1) `SELECT SQL_CALC_FOUND_ROWS … LIMIT a, b` ist kein Syntaxfehler mehr, `FOUND_ROWS()` liefert die Zeilenzahl ohne `LIMIT` (die Abfrage läuft dafür ein zweites Mal ohne ihr oberstes `LIMIT`), ohne das Stichwort die Zeilen der letzten Abfrage (`MariaDbGefundeneZeilen`, `Sitzungsschalter.LetzteGefundeneZeilen`). (2) `SELECT DISTINCT … ORDER BY` auf eine nicht ausgewählte Spalte (statt 145): die Spalte läuft verdeckt mit, entdoppelt wird nur über die sichtbaren Spalten. (3) Nicht gruppierte Spalten in Auswahlliste, `HAVING` und `ORDER BY` (statt 8120/8121/8127): der Wert der ersten Zeile der Gruppe, wie MariaDB 11 im Standard-`sql_mode`; mit `ONLY_FULL_GROUP_BY` im Modus bleibt die strenge Prüfung. (2) und (3) hängen am neuen `Dialekt.FreieGruppierungUndSortierung` (nur MariaDB, T-SQL unverändert). (4) `DELETE a, b FROM t a, t b …`: dieselbe Tabelle unter zwei Aliasen (der Alias des Ziels fehlte im `DELETE` je Ziel). Sabotage: jede der vier Wirkungen einzeln zurückgenommen, alle 4 Tests rot. ✅
D17.15 CMS-Abnahme **Erledigt (fünf Stellen aus Joomla 6, Ghost 6, Drupal 11 und Nextcloud/Redmine/Kanboard; `MariaDbCmsTests`, `FehlerprotokollTests`).** (1) Vorbereitete Anweisungen (`COM_STMT_PREPARE`) kennen alle Katalogbefehle wie das Textprotokoll: `SHOW …` (Spaltenform durch Ausführen mit NULL-Platzhaltern), `SET`, DDL, `CALL` laufen beim EXECUTE zuerst durch die Katalogbefehle, dann durch den T-SQL-Ausführer. (2) `ALTER TABLE … ADD PRIMARY KEY name(spalten)` (knex) und ein nachträglicher Primärschlüssel auf nullbaren Spalten macht sie still `NOT NULL` (`Dialekt.PrimaerschluesselMachtNichtNull`). (3) Impliziter Commit bei DDL/GRANT/REVOKE (`Dialekt.DdlSchreibtImplizitFest`, nur MariaDB): offene Transaktion davor und danach festgeschrieben, `TEMPORARY`-Tabellen ausgenommen, in einer Funktion 1422; Trigger noch nicht erkannt. (4) `KUONENSQL_FEHLERPROTOKOLL=1`: eine Zeile `[SQL-FEHLER]` je Fehler auf stdout (`Fehlerprotokoll`). (5) Ein Bezeichner in Backticks, eckigen Klammern oder Anführungszeichen ist nie ein Schlüsselwort (`Token.Begrenzt`; Regression `key`/`index`/`unique` in `CREATE TABLE`). (6) Zeichenkettenliteral '*' in der Auswahlliste wird nie als Stern erweitert (T-SQL und MariaDB). (7) Binärprotokoll: DATE/DATETIME/TIMESTAMP/TIME im MariaDB-Binärformat, BLOB roh. (8) `SHOW KEYS ... WHERE`, 1064 mit Quelltext ab der Fehlerstelle und Zeile, DDL im Trigger 1422, `KUONENSQL_ANWEISUNGSPROTOKOLL=1` (eine Zeile `[SQL]` je Kommando), Fehlerprotokoll mit `Original=` und sp_execute-Text. (9) `ON DELETE/UPDATE RESTRICT`, `utf8` ist `utf8mb3`, `GET_LOCK` und Verwandte (sitzungsgebunden, Stufen, Freigabe beim Verbindungsende), JSON wie LONGTEXT vergleichbar, `ALTER ADD NOT NULL` ohne Standardwert füllt mit der Typvorgabe, TEXT/MEDIUMTEXT/BLOB ausserhalb der Zeile (keine 8060-Grenze), `ALTER ADD ... AUTO_INCREMENT PRIMARY KEY` inline mit FIRST/AFTER (die Spalte steht wirklich dort), `DROP FOREIGN KEY/INDEX` auf Fehlendes meldet 1091. (10) Kimai: eine Anweisung nur aus einem Kommentar (`#`, `--`, `/* */`) ist OK, nur wirklich Leeres bleibt 1065. Kanboard: ein Index ohne Namen heisst wie seine erste Spalte, bei Kollision `name_2`, `name_3` (UNIQUE, KEY, INDEX, FULLTEXT; `MariaIndexnamen`), `DROP INDEX name` trifft ihn. Redmine: `JOIN … USING (spalten)` und `NATURAL [LEFT/RIGHT] JOIN` (`Dialekt.JoinUsing`, Umschreibung im Parser mit Spalten aus dem Katalog, `*` nennt die Spalte einmal und zuerst). Nicht reservierte Woerter (`end`, `start`, `status`) auch im Ausdruck. BookStack: `RENAME TABLE` (auch `ALTER TABLE … RENAME TO`, mehrere Paare, ueber Datenbanken) nimmt AUTO_INCREMENT-Zaehler, Indizes, Fremdschluessel (ausgehend und eingehend), Trigger (auch der Rumpf eines BEFORE-Triggers und `SHOW CREATE TRIGGER`), Volltextindizes mit, Tabellenrechte bleiben am alten Namen wie bei MariaDB (`CatalogManager.RenameTable`, `ExecutionEngine.TabelleUmbenennen`); beim Datenbankwechsel 1435 bei Triggern, Fremdschluessel ziehen mit und gelten ueber Datenbanken (`ForeignKeyDefinition.ReferencedDatabase`, Kind- und Elternseite, Aktionen laufen in der Datenbank des Kinds; Kind zieht: Schluessel zeigt in die alte Datenbank, Eltern ziehen: der Schluessel folgt, am Orakel gemessen). `REFERENCES db.tabelle` in CREATE/ALTER TABLE legt den Schluessel ueber Datenbanken direkt an (die Datenbank der Kindtabelle faellt weg, wenn sie gleich ist). Falsch gebaute Fremdschluessel (unbekannte Datenbank, Elterntabelle nicht in der Datenbank des Kinds, Zielspalten ohne Index) melden wie MariaDB 1005 errno 150, `DROP DATABASE` einer Elterndatenbank mit fremden Kindern 1451; `PRIMARY KEY indexname (spalten)` verlor bisher still den Schluessel. Zusatz: `DELETE/UPDATE db.tabelle` mit der gewaehlten Datenbank scheiterte mit 1146, jetzt geht es. Offen, benannt: mehrere VARCHAR(4000) in einer Zeile (über 8060 Bytes) scheitern weiter; CALL im Binärprotokoll sendet nur die letzte Ergebnismenge; DDL-Erkennung in Triggern nur für DML-Trigger. Sabotage: je Wirkung einzeln zurückgenommen, Tests rot. ✅
D17.16 Katalog und Rechte für Admin-Werkzeuge **Erledigt (Adminer, phpMyAdmin; `MariaDbAdminKatalogTests`, Leitung).** (1) Ein Benutzer mit nur `GRANT ALL ON kanboard.*` kommt nicht mehr in `wordpress`: Anmeldung mit Datenbank und `COM_INIT_DB` melden 1044 (`MariaKatalog.DarfOeffnen`, dieselbe Regel wie `USE`); `VIEW ANY DATABASE` zählt dafür nicht (hat jeder über `public`). (2) `SHOW DATABASES`, `information_schema.SCHEMATA/TABLES/COLUMNS` zeigen nur Datenbanken mit Recht, immer `information_schema`, für sa/sysadmin zusätzlich `mysql`, `performance_schema`, `sys` (nur als Namen, ohne eigenen Katalog); `master`/`model`/`msdb`/`tempdb` nie in `SHOW DATABASES`/`SCHEMATA` (in `TABLES`/`COLUMNS` nur, wenn die Sitzung gerade darin steht). (3) `CREATE_TIME` ist die Anlagezeit der Tabelle in ganzen Sekunden (`DATETIME`), `UPDATE_TIME` die der letzten Zeilenänderung (`NULL`, solange nie geändert), `TABLE_ROWS` die echte Zeilenzahl, `DATA_LENGTH` aus den Seiten auf 16 KiB gerundet. **Rest geschlossen:** die Anlagezeit steht in der Datendatei (Version 2, Version 1 wird weiter gelesen, ein Sicherungssatz bleibt im Format 1); `SHOW TABLES/TABLE STATUS/COLUMNS/INDEX FROM <fremde Datenbank>` melden 1044; `mysql`, `performance_schema`, `sys` sind Schemata (`MariaEingebauteSchemata`: `mysql.db/tables_priv/columns_priv/procs_priv/proc/global_priv/func/proxies_priv/user/roles_mapping`, `performance_schema.global_/session_variables/status`, `users/hosts/accounts`, `sys.version/sys_config`) samt `USE`, `SHOW TABLES/COLUMNS/INDEX/TABLE STATUS FROM` und `information_schema.USER_/SCHEMA_/TABLE_/COLUMN_PRIVILEGES` (Verwalter sieht alle, sonst nur eigene Rechte); ein Name ohne Datenbank meldet in einer Sitzung ohne gewählte Datenbank 1046. **Zweiter Rest geschlossen:** nach `USE mysql`/`performance_schema`/`sys`/`information_schema` lösen Namen ohne Datenbank dort auf (auch `SHOW TABLES`, `DESCRIBE`); ohne gewählte Datenbank meldet jede Anweisung mit einem Namen ohne `db.` 1046 (`MariaDbKeineDatenbank` liest den Text Wort für Wort; CTE-Namen, `FROM` in Funktionen, Zeichenketten und Kommentare sind ausgenommen), nie ein stilles `master`; `mysql.proc` zeigt Parameter, Rückgabe, Merkmale, Definierer und Rumpf aus der Urfassung; Rechte auf Spalten (`GRANT SELECT (a, b), INSERT (c) ON db.t`, `REVOKE`, `PermissionManager.Spaltenrechte`, dauerhaft) mit 1143 bei der ersten fehlenden Spalte und 1221 für Rechte ohne Spaltenstufe, Anzeige in `SHOW GRANTS`, `mysql.columns_priv`/`tables_priv.Column_priv`, `information_schema.COLUMN_PRIVILEGES`; ein Benutzer mit nur einem Tabellenrecht darf die Datenbank wählen (MariaDB prüft jedes Recht in der Datenbank, belegt durch Test). **Grenze:** Spaltenrechte gelten für Abfragen, `INSERT` und `UPDATE` in der aktuellen Datenbank (andere Datenbank: nur Tabellenrecht); die Schreibweise der Spaltenzeile in `SHOW GRANTS` folgt der Anleitung, nicht einer Messung am Server. Sabotage: Sichtbarkeitsfilter ausgeschaltet, Test rot. ✅
D17.17 App-Abnahme Runde 4 **Erledigt (Funde der frischen Installation von Redmine, Ghost, BookStack, Kanboard, Snipe-IT, Nextcloud; `MariaDbAppRunde4Tests`).** (1) Redmine: `DROP COLUMN` liess die Typbedingung der Spalte (`TYV__tabelle__spalte` aus `TINYINT(1)`, `UNSIGNED` …) stehen, jedes folgende `UPDATE` der Tabelle scheiterte mit 1054 `Unknown column` (Redmine benennt `mail_notification` in `mail_notification_bool` um und entfernt sie). Jetzt faellt die Typbedingung mit der Spalte. (2) Kanboard: `ALTER TABLE … MODIFY/CHANGE spalte … UNIQUE` (bzw. `PRIMARY KEY`) verwarf den Inline-Schluessel still; MariaDB legt einen weiteren eindeutigen Index an (`name_2`, wenn `name` schon besteht, am Orakel gemessen) bzw. meldet bei vorhandenem Primaerschluessel 1068. Kanboard entfernt danach `name` und `name_2`; fehlte `name_2`, brach die Migration ab und lief bei jedem Aufruf erneut (1091 auf `name`). (3) BookStack: `DROP COLUMN id` liess den Primaerschluessel auf der entfernten Spalte stehen, das folgende `ADD PRIMARY KEY (role_id, …)` meldete 1068. Jetzt wie MariaDB 11.8 (gemessen, `Dialekt.SpalteEntfernenNimmtSchluesselMit`): einspaltige Indizes der Spalte und `CHECK`-Bedingungen nur auf ihr fallen mit, eine Spalte in einem mehrspaltigen Index oder Primaerschluessel meldet 1072, unter einem Fremdschluessel 1553, in einer `CHECK`-Bedingung mit weiteren Spalten 1054 `in 'CHECK'`, die letzte Spalte 1090. (4) Nebenbefund: `information_schema.TABLE_CONSTRAINTS` zeigte die internen Typbedingungen (`TYV__…`) als `CHECK`; jetzt wie `CHECK_CONSTRAINTS` und wie MariaDB nur die eigenen (gemessen). Wie MariaDB, am Orakel belegt, nichts zu tun: Ghost 1068/1061 (knex-migrator legt `migrations_lock` mit Primaerschluessel an und schickt bei jedem Start erneut `add primary key` und `add unique`, MariaDB meldet dasselbe), Snipe-IT 1091 (`*_company_id_foreign` wurde nie angelegt, die Migration loescht sie vorsorglich), Nextcloud 1142 (`mysql.user` ohne Recht). Sabotage: je Wirkung einzeln zurueckgenommen, Tests rot. ✅
D17.18 App-Abnahme Runde 5 **Erledigt (Funde nach frischem Neuaufbau: Redmine, Kanboard, BookStack; `MariaDbAppRunde5Tests`).** (1) Redmine: `RENAME COLUMN` (auch `sp_rename` auf eine Spalte) setzte den AUTO_INCREMENT-/IDENTITY-Zaehler zurueck; das folgende `INSERT` ohne `id` meldete 1062 `Duplicate entry '1' for key 'PRIMARY'`, die Migration brach ab. Der Zaehler bleibt jetzt. (2) BookStack: geklammerte Abfragen als Glied einer Mengenoperation (`(SELECT …) UNION (SELECT …)`, so schreibt Laravel jede Vereinigung) und als Quelle von `INSERT … [(spalten)] (SELECT …)` meldeten 1064. Jetzt auch als ganze Anweisung, in `FROM ((…) UNION (…)) x`, mit eigener Sortierung oder Grenze in der Klammer (gilt nur dort) und mit `ORDER BY`/`LIMIT` hinter der letzten Klammer (gilt fuer das Ganze); T-SQL kennt dieselbe Schreibweise; am Orakel gemessen. (3) Kanboard: ein Fremdschluessel, dessen Spalten kein Index vorn fuehrt, bekam nur einen angezeigten Index (`tabelle_ibfk_N`); MariaDB legt einen echten an, benannt nach `CONSTRAINT`, sonst `FOREIGN KEY indexname`, sonst der ersten Spalte (`user_id_2` bei Kollision), und er bleibt nach `DROP FOREIGN KEY` (gemessen). Kanboards `DROP KEY user_id` scheiterte, die Migration lief bei jedem Aufruf erneut (1091 auf `user_has_notifications_ibfk_1`); die 1050 auf `password_reset` kam von einer zweiten Anfrage, die dieselbe Migration parallel anstiess (mit MariaDB derselbe Wettlauf). `DROP FOREIGN KEY`/`DROP CONSTRAINT` entfernt bei gleichem Namen zuerst den Fremdschluessel, der Index bleibt. Sabotage: je Wirkung einzeln zurueckgenommen, Tests rot. ✅

Beispiele

Typen

AUTO_INCREMENT und der übliche Spaltentyp

AUTO_INCREMENT vergibt die Schlüsselnummer selbst; DECIMAL rechnet exakt.

CREATE TABLE pruefstand_mitarbeiter (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), gehalt DECIMAL(9,2));
INSERT INTO pruefstand_mitarbeiter (name, gehalt) VALUES ('Anna', 5500.00);
SELECT id, name, gehalt FROM pruefstand_mitarbeiter;
DROP TABLE pruefstand_mitarbeiter;
Erwartetes Ergebnis
id | name | gehalt
1 | Anna | 5500.00

JSON und ENUM

JSON_EXTRACT liest einen Wert aus einer JSON-Spalte; ENUM erlaubt nur die aufgezählten Werte.

CREATE TABLE pruefstand_typen (id INT PRIMARY KEY, info JSON, status ENUM('offen', 'erledigt'));
INSERT INTO pruefstand_typen VALUES (1, '{"anzahl":3}', 'offen');
SELECT id, JSON_EXTRACT(info, '$.anzahl') AS anzahl, status FROM pruefstand_typen;
DROP TABLE pruefstand_typen;
Erwartetes Ergebnis
id | anzahl | status
1 | 3 | offen

DDL

CREATE TABLE IF NOT EXISTS und CREATE OR REPLACE VIEW

IF NOT EXISTS macht ein zweites CREATE TABLE ungefährlich; CREATE OR REPLACE tauscht eine Sicht ohne DROP davor.

CREATE TABLE IF NOT EXISTS pruefstand_orte (id INT PRIMARY KEY, name VARCHAR(30));
CREATE TABLE IF NOT EXISTS pruefstand_orte (id INT PRIMARY KEY, name VARCHAR(30));
INSERT INTO pruefstand_orte VALUES (1, 'Bern'), (2, 'Chur');
CREATE OR REPLACE VIEW pruefstand_sicht AS SELECT id, name FROM pruefstand_orte WHERE id = 1;
CREATE OR REPLACE VIEW pruefstand_sicht AS SELECT id, name FROM pruefstand_orte WHERE id = 2;
SELECT id, name FROM pruefstand_sicht;
DROP VIEW pruefstand_sicht;
DROP TABLE pruefstand_orte;
Erwartetes Ergebnis
id | name
2 | Chur

ALTER TABLE MODIFY/CHANGE und RENAME TABLE

MODIFY ändert einen Spaltentyp, CHANGE benennt eine Spalte um und ändert ihren Typ, RENAME TABLE die ganze Tabelle.

CREATE TABLE pruefstand_alter (id INT PRIMARY KEY, txt VARCHAR(10));
ALTER TABLE pruefstand_alter MODIFY txt VARCHAR(40);
ALTER TABLE pruefstand_alter CHANGE txt beschreibung VARCHAR(40);
RENAME TABLE pruefstand_alter TO pruefstand_umbenannt;
INSERT INTO pruefstand_umbenannt VALUES (1, 'Eine laengere Beschreibung');
SELECT id, beschreibung FROM pruefstand_umbenannt;
DROP TABLE pruefstand_umbenannt;
Erwartetes Ergebnis
id | beschreibung
1 | Eine laengere Beschreibung

DML

ON DUPLICATE KEY UPDATE

Trifft der Schlüssel schon zu, läuft der UPDATE-Teil statt eines Fehlers.

CREATE TABLE pruefstand_zaehler (id INT PRIMARY KEY, anzahl INT);
INSERT INTO pruefstand_zaehler (id, anzahl) VALUES (1, 1) ON DUPLICATE KEY UPDATE anzahl = anzahl + 1;
INSERT INTO pruefstand_zaehler (id, anzahl) VALUES (1, 1) ON DUPLICATE KEY UPDATE anzahl = anzahl + 1;
SELECT id, anzahl FROM pruefstand_zaehler;
DROP TABLE pruefstand_zaehler;
Erwartetes Ergebnis
id | anzahl
1 | 2

INSERT IGNORE und REPLACE

INSERT IGNORE übergeht einen Schlüsselkonflikt stillschweigend, REPLACE löscht die alte Zeile und setzt die neue.

CREATE TABLE pruefstand_unikate (id INT PRIMARY KEY, wert VARCHAR(30));
INSERT INTO pruefstand_unikate VALUES (1, 'Erste');
INSERT IGNORE INTO pruefstand_unikate VALUES (1, 'Zweite');
REPLACE INTO pruefstand_unikate VALUES (1, 'Dritte');
SELECT id, wert FROM pruefstand_unikate;
DROP TABLE pruefstand_unikate;
Erwartetes Ergebnis
id | wert
1 | Dritte

UPDATE mit ORDER BY und LIMIT

LIMIT begrenzt, wie viele Zeilen ein UPDATE/DELETE trifft; ORDER BY legt fest, welche.

CREATE TABLE pruefstand_limit (id INT PRIMARY KEY, wert INT);
INSERT INTO pruefstand_limit VALUES (1, 10), (2, 20), (3, 30);
UPDATE pruefstand_limit SET wert = wert + 1 ORDER BY id LIMIT 2;
SELECT id, wert FROM pruefstand_limit ORDER BY id;
DROP TABLE pruefstand_limit;
Erwartetes Ergebnis
id | wert
1 | 11
2 | 21
3 | 30

RETURNING

RETURNING gibt die eingefügte Zeile gleich mit dem INSERT zurück.

CREATE TABLE pruefstand_returning (id INT AUTO_INCREMENT PRIMARY KEY, wert INT);
INSERT INTO pruefstand_returning (wert) VALUES (42) RETURNING id, wert;
DROP TABLE pruefstand_returning;
Erwartetes Ergebnis
id | wert
1 | 42