====== Datenbanken & SQL ======
Daten dauerhaft ablegen, abfragen und ändern: ''std.db'' spricht SQLite über die C-Bibliothek, PostgreSQL, MySQL und Redis dagegen **nativ über das jeweilige Netzprotokoll** — ohne Client-Bibliothek. Diese Seite zeigt den Arbeitsweg je Backend, die gemeinsamen Muster (Prepared Statements, Transaktionen, Ressourcen) und benennt, was heute trägt.
→ [[lyx_-_programmiersprache:units:db|std.db — Unit-Referenz]] · [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]] · [[lyx_-_programmiersprache:sprache:capabilities|Capabilities]]
----
===== 1. Welches Backend? =====
^ Aufgabe ^ Unit ^ Verbindung ^ Zustand ^
| Eingebettete Datenbank, eine Datei | ''std.db.sqlite'' | libsqlite3 | **einsatzfähig** — außer ''REAL'', siehe Abschnitt 5 |
| Relationale Datenbank im Netz | ''std.db.postgres'' | natives Protokoll v3 | einsatzfähig — Host **nur als IPv4** |
| MySQL / MariaDB | ''std.db.mysql'' | natives Protokoll 4.1 | Abfragen ja, **Prepared Statements nein** — Abschnitt 8 |
| Cache, Warteschlange, Zähler | ''std.db.redis'' | RESP | ⚠️ lokaler Server nicht erreichbar, Abschnitt 9 |
| Sehr kleiner Redis-Zugriff | ''std.db.redis_simple'' | RESP | nur ''Set''/''Get''/''Ping'' |
| Graphdaten (Knoten, Kanten) | ''std.graphdb'' | eigene Datei | eigenständige Familie, 7 Units |
| MongoDB | ''std.net.mongo'' | Wire Protocol | liegt bei den Netz-Units, nicht unter ''db'' |
Faustregel: **SQLite, solange es geht.** Eine Datei, kein Server, kein Netz, keine Zugangsdaten — und für die allermeisten Anwendungen schnell genug. Ein Server-Backend lohnt, wenn mehrere Prozesse gleichzeitig schreiben oder die Daten getrennt vom Programm leben sollen.
----
===== 2. SQLite — der vollständige Weg =====
import std.io;
import std.db.sqlite;
fn main(): int64 {
var db: int64 := SQLiteOpen("kunden.db"c); // ":memory:" für flüchtige DB
if (db == 0) { PrintLn("Öffnen fehlgeschlagen"); return 1; }
if (!SQLiteExec(db, "CREATE TABLE IF NOT EXISTS kunden (id INTEGER PRIMARY KEY, name TEXT, umsatz_cent INTEGER)"c)) {
PrintLn(SQLiteErrmsg(db));
SQLiteClose(db);
return 1;
}
// Einfügen mit Prepared Statement
var stmt: int64 := SQLiteStmtPrepare(db, "INSERT INTO kunden (name, umsatz_cent) VALUES (?, ?)"c);
if (stmt == 0) { PrintLn(SQLiteErrmsg(db)); SQLiteClose(db); return 1; }
SQLiteBindStr(stmt, 1, "Meier GmbH"c);
SQLiteBindInt(stmt, 2, 125050);
SQLiteStmtStep(stmt);
SQLiteStmtReset(stmt); // dasselbe Statement erneut verwenden
SQLiteClearBindings(stmt);
SQLiteBindStr(stmt, 1, "Schulze AG"c);
SQLiteBindInt(stmt, 2, 89000);
SQLiteStmtStep(stmt);
SQLiteStmtFinalize(stmt);
Print("letzte ID: "); PrintLn(IntToStr(SQLiteLastInsertId(db)));
Print("geaendert: "); PrintLn(IntToStr(SQLiteChanges(db)));
// Abfragen
var q: int64 := SQLiteStmtPrepare(db, "SELECT id, name FROM kunden WHERE umsatz_cent > ? ORDER BY umsatz_cent DESC"c);
SQLiteBindInt(q, 1, 50000);
while (SQLiteStmtStep(q) == SQLITE_ROW) {
var name: pchar := SQLiteColumnText(q, 1);
Print(IntToStr(SQLiteColumnInt(q, 0))); Print(" | "); PrintLn(name);
}
SQLiteStmtFinalize(q);
SQLiteClose(db);
return 0;
}
Ausgabe:
letzte ID: 2
geaendert: 1
1 | Meier GmbH
2 | Schulze AG
==== Die vier Schritte eines Statements ====
- **''SQLiteStmtPrepare''** übersetzt das SQL einmal. Rückgabe ''0'' heißt Fehler — ''SQLiteErrmsg'' nennt den Grund.
- **''SQLiteBind…''** setzt die Werte. Die Parameter sind **1-basiert**.
- **''SQLiteStmtStep''** führt aus. ''SQLITE_ROW'' (100) heißt „eine Zeile liegt bereit", ''SQLITE_DONE'' (101) „fertig".
- **''SQLiteStmtFinalize''** gibt das Statement frei. Ohne diesen Aufruf bleibt es am Verbindungsobjekt hängen.
Die Spalten in ''SQLiteColumn…'' sind dagegen **0-basiert**. Diese Asymmetrie stammt aus SQLite selbst und ist die häufigste Fehlerquelle beim Einstieg.
^ Werte setzen ^ Werte lesen ^
| ''SQLiteBindInt'', ''SQLiteBindStr'', ''SQLiteBindFloat'', ''SQLiteBindNull'', ''SQLiteBindBlob'' | ''SQLiteColumnInt'', ''SQLiteColumnText'', ''SQLiteColumnFloat'', ''SQLiteColumnBlob'', ''SQLiteColumnBytes'' |
| — | ''SQLiteColumnIsNull'', ''SQLiteColumnType'', ''SQLiteColumnCount'', ''SQLiteColumnName'' |
==== Ohne Ergebnismenge ====
''SQLiteExec(db, sql)'' führt ein Statement in einem Schritt aus — richtig für ''CREATE TABLE'', ''PRAGMA'', ''DELETE'' ohne Parameter. **Niemals** mit selbst zusammengesetztem SQL aus fremden Daten (Abschnitt 4).
==== Verwaltung ====
^ Funktion ^ Zweck ^
| ''SQLiteLastInsertId'' | ID der zuletzt eingefügten Zeile |
| ''SQLiteChanges'' | Zahl der geänderten Zeilen der letzten Anweisung |
| ''SQLiteTableExists'', ''SQLiteDropTable'' | Schema prüfen und aufräumen |
| ''SQLiteSetJournalMode(db, "WAL"c)'' | WAL statt Rollback-Journal — mehrere Leser neben einem Schreiber |
| ''SQLiteSetCacheSize'' | Seiten im Cache |
| ''SQLiteVacuum'' | Datei verdichten |
| ''SQLiteErrmsg'', ''SQLiteErrno'' | Klartext und Code des letzten Fehlers |
Für jeden Dienst, der nebenläufig liest, gehört ''SQLiteSetJournalMode(db, "WAL"c)'' direkt hinter das ''SQLiteOpen''.
----
===== 3. Transaktionen =====
if (!SQLiteBegin(db)) { PrintLn(SQLiteErrmsg(db)); return 1; }
var ok: bool := SQLiteExec(db, "UPDATE konto SET stand = stand - 100 WHERE id = 1"c);
if (ok) { ok := SQLiteExec(db, "UPDATE konto SET stand = stand + 100 WHERE id = 2"c); }
if (ok) { SQLiteCommit(db); }
else { SQLiteRollback(db); PrintLn(SQLiteErrmsg(db)); }
* ''SQLiteBegin'' beginnt verzögert (die Sperre entsteht erst beim ersten Schreiben). ''SQLiteBeginImmediate'' nimmt die Schreibsperre sofort — richtig, wenn zwei Prozesse um dieselbe Datei konkurrieren, sonst droht ''SQLITE_BUSY'' mitten in der Transaktion. ''SQLiteBeginExclusive'' sperrt die ganze Datei.
* ''SQLiteInTransaction'' beantwortet, ob eine läuft.
* **Jeder Fehlerpfad braucht sein ''Rollback''.** Ein früher ''return'' zwischen ''Begin'' und ''Commit'' hinterlässt eine offene Transaktion und damit eine gesperrte Datei.
* Ein Massenimport gehört in **eine** Transaktion. Ohne sie schreibt SQLite jede einzelne Zeile mit eigener Synchronisierung auf die Platte — der Unterschied liegt bei Faktor Hundert.
PostgreSQL kennt zusätzlich Sicherungspunkte: ''PGSavepoint'', ''PGRollbackTo'', ''PGReleaseSavepoint'', dazu ''PGBeginReadOnly'' und ''PGBeginSerializable''.
----
===== 4. SQL-Injection: Prepared Statements sind nicht optional =====
Falsch — und zwar unabhängig davon, wie sicher die Quelle wirkt:
// NIEMALS so:
var sql: int64 := alloc(512);
StrReplace(sql as pchar, "SELECT * FROM kunden WHERE name = 'X'"c, "X"c, eingabe);
SQLiteExec(db, sql as pchar);
Richtig:
var q: int64 := SQLiteStmtPrepare(db, "SELECT id FROM kunden WHERE name = ?"c);
SQLiteBindStr(q, 1, eingabe);
while (SQLiteStmtStep(q) == SQLITE_ROW) { … }
SQLiteStmtFinalize(q);
Der Unterschied ist nicht Bequemlichkeit, sondern die Grenze zwischen **Code und Daten**: Ein gebundener Wert kann das Statement nicht mehr verändern, egal was darin steht. Ein zusammengesetzter String kann es immer.
Dieselbe Regel bei PostgreSQL: ''PGStmtPrepare'' + ''PGBind…'' + ''PGStmtExecute''. Bei Redis sind Schlüssel und Werte ohnehin getrennte Protokollfelder — dort entsteht das Problem nicht. **Bei MySQL/MariaDB steht dieser Schutz derzeit nicht zur Verfügung**, siehe Abschnitt 8.
**Ausnahme, die keine ist:** Tabellen- und Spaltennamen lassen sich nicht binden. Wo sie variabel sein müssen, gehören sie gegen eine **feste Liste erlaubter Namen** geprüft, nicht maskiert.
----
===== 5. Zahlen mit Nachkommastellen =====
> **''REAL''-Spalten verlieren ihren Wert.** ''SQLiteBindFloat(st, 1, 1250.5)'' speichert ''0.0'', und ''SQLiteColumnFloat'' liefert ''0'' — auch bei einem Wert, den SQL selbst korrekt sieht (''SELECT CAST(f*10 AS INTEGER)'' rechnet richtig). Betroffen sind beide Richtungen; ''INTEGER'' und ''TEXT'' arbeiten einwandfrei. Geprüft mit lyxc 1.0.16F und 1.0.16F.
>
> Bis zur Behebung: Beträge und Messwerte als **ganze Zahlen** führen — Geld in Cent, Längen in Millimetern, Prozent in Hundertsteln. Das ist bei Geld ohnehin die richtige Wahl, weil binäre Gleitkommazahlen 0,10 € nicht exakt darstellen können.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1329|Issue #1329]])
// statt umsatz REAL:
CREATE TABLE kunden (id INTEGER PRIMARY KEY, name TEXT, umsatz_cent INTEGER)
// Anzeige: 125050 -> "1250,50"
var euro: int64 := cent / 100;
var rest: int64 := cent % 100;
----
===== 6. Capabilities und die Sandbox =====
> **Mit ''@capabilities'' stirbt ''std.db.sqlite'' beim ersten Zugriff.** Der Prozess endet mit ''Ungültiger Betriebssystemaufruf'' (Exit 159) — der seccomp-Filter kennt ''fcntl'' nicht, das SQLite für seine Dateisperren braucht. Gewährte ''fs''-Capabilities ändern daran nichts.
>
> Ein SQLite-Programm läuft damit heute **entweder** gehärtet **oder** überhaupt — nicht beides. Für Server-Backends (PostgreSQL, MySQL, Redis) gilt das nicht; dort genügen die Netz-Capabilities.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1276|Issue #1276]])
Für die Netz-Backends:
@capabilities([system.exit, system.memory.heap, network.tcp.connect])
import std.io grant [system.write];
import std.db.postgres grant [network.tcp.connect, system.memory.heap];
→ Die vollständige Capability-Matrix steht im [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]].
----
===== 7. PostgreSQL =====
''std.db.postgres'' spricht das Frontend/Backend-Protokoll v3 selbst — inklusive MD5-Authentifizierung, Prepared Statements und Transaktionssteuerung.
import std.io;
import std.db.postgres;
fn main(): int64 {
var conn: int64 := PGConnect("127.0.0.1"c, 5432, "app"c, "geheim"c, "kundendb"c);
if (conn == 0 || PGIsConnected(conn) == 0) {
PrintLn(PGError(conn));
return 1;
}
var v: pchar := PGServerVersion(conn);
Print("Server: "); PrintLn(v);
var res: int64 := PGQuery(conn, "SELECT id, name FROM kunden ORDER BY id"c);
if (res == 0) { PrintLn(PGError(conn)); PGClose(conn); return 1; }
Print("Zeilen: "); PrintLn(IntToStr(PGNumRows(res)));
while (PGFetchRow(res) != 0) {
var name: pchar := PGGetStr(res, 1);
Print(IntToStr(PGGetInt(res, 0))); Print(" | "); PrintLn(name);
}
PGFreeResult(res);
PGClose(conn);
return 0;
}
> **Der Host muss eine IPv4-Adresse in Textform sein.** ''pgTCPConnect'' zerlegt die Zeichenkette Byte für Byte (''pgParseIPv4Byte'') und fragt kein DNS — ''"localhost"'' oder ''"db.example.com"'' führen zu keiner Verbindung. Namen also vorher selbst auflösen (''DNSResolveGoogle'' aus ''std.net.dns'', siehe [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]]) oder die Adresse konfigurieren.
^ Aufgabe ^ Funktion ^
| Verbindung | ''PGConnect'', ''PGClose'', ''PGIsConnected'', ''PGServerVersion'', ''PGBackendPID'' |
| Abfrage | ''PGQuery'', ''PGFreeResult'' |
| Ergebnis | ''PGNumRows'', ''PGNumFields'', ''PGAffectedRows'', ''PGFetchRow'', ''PGDataSeek'' |
| Werte | ''PGGetStr'', ''PGGetInt'', ''PGGetFloat'', ''PGGetBool'', ''PGIsNull'' |
| Spalten | ''PGGetFieldName'', ''PGGetFieldTypeOid'' |
| Prepared | ''PGStmtPrepare'', ''PGBindInt''/''Str''/''Float''/''Bool''/''Null'', ''PGStmtExecute'', ''PGStmtClose'', ''PGStmtFree'' |
| Transaktion | ''PGBegin'', ''PGCommit'', ''PGRollback'', ''PGBeginReadOnly'', ''PGBeginSerializable'', ''PGSavepoint'', ''PGRollbackTo'', ''PGReleaseSavepoint'' |
| Fehler | ''PGError'', ''PGErrno'', ''PGGetTxStatus'' |
Ein Prepared Statement bekommt hier einen **Namen** und lebt auf dem Server: ''PGStmtPrepare(conn, "kunde_nach_id"c, "SELECT name FROM kunden WHERE id = $1"c)''. PostgreSQL nummeriert die Platzhalter (''$1'', ''$2'') statt sie mit ''?'' zu schreiben.
----
===== 8. MySQL / MariaDB =====
Geprüft gegen MariaDB 10.11 über TCP.
import std.io;
import std.db.mysql;
fn main(): int64 {
var conn: MySQLConn := MySQLConnect("127.0.0.1"c, 3306, "maria"c, "geheim"c, "maria"c);
if (conn.fd < 0) { PrintLn(MySQLError(conn)); return 1; }
var res: MySQLResult := MySQLQuery(conn, "SELECT id, name, umsatz_cent FROM kunden ORDER BY id"c);
Print("Zeilen: "); PrintLn(IntToStr(MySQLNumRows(res)));
var row: int64 := MySQLFetchRow(res);
while (row != 0) {
Print(IntToStr(MySQLGetRowInt(res, row, 0))); Print(" | ");
var name: pchar := MySQLGetRowStr(res, row, 1);
Print(name); Print(" | ");
PrintLn(IntToStr(MySQLGetRowInt(res, row, 2)));
row := MySQLFetchRow(res);
}
MySQLFreeResult(res);
MySQLClose(conn);
return 0;
}
Ausgabe:
Zeilen: 2
1 | Meier GmbH | 125050
2 | Schulze AG | 89000
> **Das zweite Argument von ''MySQLGetRowStr'' ist ein Zeiger, kein Index.** Es erwartet den Rückgabewert von ''MySQLFetchRow''. Wer dort eine Zeilennummer übergibt, bekommt bei ''0'' eine leere Zeichenkette und ab ''1'' einen Speicherzugriffsfehler — die Zahl wird als Adresse gelesen.
>
> **Der gelieferte Zeiger zeigt auf einen gemeinsamen Puffer** und gilt nur bis zum nächsten Wertzugriff; auch ''MySQLGetRowInt'' überschreibt ihn. Ein vorab geholter Name ist nach dem nächsten Aufruf ein anderer Wert. Entweder sofort verwenden oder wegkopieren.
==== Was gegen MariaDB 10.11 trägt ====
^ Bereich ^ Zustand ^
| ''MySQLConnect'' (''mysql_native_password''), ''MySQLClose'' | ✅ |
| ''MySQLQuery'', ''MySQLNumRows'', ''MySQLNumFields'', ''MySQLFreeResult'' | ✅ |
| ''MySQLFetchRow'' + ''MySQLGetRowStr''/''MySQLGetRowInt'' | ✅ |
| ''MySQLAffectedRows'', ''MySQLInsertId'' | ✅ |
| ''MySQLBegin''/''Commit''/''Rollback'' — **Wirkung** | ✅ |
| ''MySQLBegin''/''Commit''/''Rollback'' — **Rückgabewert** | ❌ liefert ''false'', obwohl die Transaktion greift |
| ''MySQLStmtPrepare'' und die ganze Prepared-Familie | ❌ Server antwortet ''#08S01 Unknown command'' |
| ''MySQLGetFieldName'', ''…Type'', ''…Length'' | ❌ leere Hüllen: ''""'' bzw. ''0'' |
> **Prepared Statements arbeiten gegen MySQL/MariaDB nicht.** ''MySQLStmtPrepare'' liefert ein Statement mit ''stmt_id = 0'' und ''param_count = 0'', jedes ''MySQLStmtBind…'' gibt ''false'' zurück, und ''MySQLStmtExecute'' fügt nichts ein — der Server lehnt das Paket mit ''#08S01 Unknown command'' ab.
>
> **Damit fehlt bei diesem Backend der Schutz aus Abschnitt 4.** Wer Werte aus fremder Quelle verarbeitet, nimmt bis auf Weiteres SQLite oder PostgreSQL — dort arbeiten Prepared Statements. Geht das nicht, muss jede Eingabe vor dem Einsetzen gegen eine enge Positivliste geprüft werden (Ziffern, feste Auswahl); Maskieren allein reicht nicht.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1332|Issue #1332]])
> **Der Host muss eine IPv4-Adresse in Textform sein — ein Name lässt die Verbindung hängen.** ''MySQLConnect'' zerlegt ''host'' mit ''parseIPv4Byte'' und kennt kein DNS; ''"localhost"'' ergibt 0.0.0.0, und ''sys_connect'' wartet daraufhin unbegrenzt, statt einen Fehler zu melden. Namen also selbst auflösen und die Adresse übergeben.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1332|Issue #1332]])
^ Aufgabe ^ Funktion ^
| Verbindung | ''MySQLConnect'', ''MySQLClose'', ''MySQLError'', ''MySQLErrno'' |
| Abfrage | ''MySQLQuery'', ''MySQLFreeResult'', ''MySQLAffectedRows'', ''MySQLInsertId'' |
| Ergebnis | ''MySQLNumRows'', ''MySQLNumFields'', ''MySQLFetchRow'', ''MySQLGetRowStr'', ''MySQLGetRowInt'', ''MySQLIsNull'', ''MySQLDataSeek'' |
| Transaktion | ''MySQLBegin'', ''MySQLCommit'', ''MySQLRollback'', ''MySQLSetAutoCommit'', ''MySQLInTransaction'' |
Die Ergebnismenge liegt vollständig im Speicher — bei großen Abfragen also ''LIMIT'' setzen.
----
===== 9. Redis =====
Kein SQL, sondern Schlüssel-Wert-Speicher: Cache, Warteschlange, Zähler, Sitzungsdaten.
import std.io;
import std.db.redis;
fn main(): int64 {
var conn: RedisConn := RedisConnect("db.example.com"c, 6379);
if (conn.fd < 0) { PrintLn("Redis nicht erreichbar"); return 1; }
RedisSet(conn, "kunde:1"c, "Meier GmbH"c);
var r: RedisReply := RedisGet(conn, "kunde:1"c);
if (r.kind == REDIS_REPLY_BULK) {
var s: pchar := r.str_val;
PrintLn(s);
}
RedisLPush(conn, "queue"c, "job1"c);
Print("Laenge: "); PrintLn(IntToStr(RedisLLen(conn, "queue"c)));
RedisClose(conn);
return 0;
}
> **Ein Redis auf demselben Rechner ist nicht erreichbar.** ''RedisConnect'' löst **jeden** Host über Google DNS auf (''GetHostByName'' → ''DNSResolveGoogle''). Damit scheitern sowohl ''"127.0.0.1"'' — eine IP-Adresse wird als Name abgefragt — als auch ''"localhost"'', weil ''/etc/hosts'' nicht gelesen wird. Nur öffentlich auflösbare Namen funktionieren, und jede Verbindung kostet zusätzlich eine externe DNS-Anfrage.
>
> Zu beachten: ''std.db.postgres'' und ''std.db.mysql'' machen das **Gegenteil** und akzeptieren nur eine literale IPv4. Der ''host''-Parameter bedeutet also je nach Unit etwas anderes.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1330|Issue #1330]])
^ Bereich ^ Funktionen ^
| Verbindung | ''RedisConnect'', ''RedisClose'', ''RedisPing'' |
| Schlüssel | ''RedisSet'', ''RedisGet'', ''RedisDel'', ''RedisExists'' |
| Listen | ''RedisLPush'', ''RedisRPush'', ''RedisLPop'', ''RedisRPop'', ''RedisLRange'', ''RedisLLen'' |
| Hashes | ''RedisHSet'', ''RedisHGet'', ''RedisHGetAll'', ''RedisHDel'' |
Die Antwort kommt als ''RedisReply'' mit ''kind'' (''REDIS_REPLY_NIL'', ''…_BULK'', ''…_INTEGER'', ''…_ARRAY'', ''…_ERROR'', ''…_STATUS''), ''int_val'', ''str_val'' und ''str_len''. **''kind'' zuerst prüfen** — ein fehlender Schlüssel liefert ''REDIS_REPLY_NIL'', nicht etwa eine leere Zeichenkette.
''std.db.redis_simple'' ist die Sparversion mit ''RedisConnect'', ''RedisClose'', ''RedisPing'', ''RedisSet'', ''RedisGet'' — nützlich, wenn nur ein Wert abgelegt werden soll.
----
===== 10. Wiederkehrende Muster =====
==== Alles wieder freigeben ====
^ Angelegt mit ^ Freigegeben mit ^
| ''SQLiteOpen'' | ''SQLiteClose'' |
| ''SQLiteStmtPrepare'' | ''SQLiteStmtFinalize'' — auch im Fehlerfall |
| ''PGConnect'' | ''PGClose'' |
| ''PGQuery'' | ''PGFreeResult'' |
| ''PGStmtPrepare'' | ''PGStmtClose'' (Server) **und** ''PGStmtFree'' (Speicher) |
| ''MySQLConnect'' | ''MySQLClose'' |
| ''MySQLQuery'' | ''MySQLFreeResult'' |
| ''RedisConnect'' | ''RedisClose'' |
Ein Dienst, der pro Anfrage eine Verbindung öffnet und nicht schließt, läuft nach einigen tausend Anfragen in das Deskriptorlimit — und zwar erst im Betrieb, nicht im Test.
==== Fehler auf drei Ebenen ====
^ Ebene ^ Erkennbar an ^ Beispiel ^
| Verbindung | ''0'' bzw. ''fd < 0'' | Server aus, Passwort falsch, Datei nicht schreibbar |
| Anweisung | ''Prepare'' gibt 0, ''Exec'' gibt ''false'' | Syntaxfehler, Tabelle fehlt, Constraint verletzt |
| Ergebnis | ''SQLITE_DONE'' statt ''SQLITE_ROW'', ''NumRows'' = 0 | die Abfrage lief, fand aber nichts |
Die dritte Ebene ist **kein** Fehler. Wer „keine Zeile" wie „Fehler" behandelt, meldet Störungen, wo nur nichts zu finden war.
==== Die Frage nach dem Schema ====
Lyx bringt kein Migrationswerkzeug mit. Bewährt hat sich eine Versionsnummer in der Datenbank selbst:
SQLiteExec(db, "CREATE TABLE IF NOT EXISTS schema_version (v INTEGER)"c);
// beim Start lesen, mit der erwarteten Version vergleichen, gezielt nachziehen
----
===== 11. Zustand auf einen Blick =====
^ Unit ^ Zustand ^
| ''std.db.sqlite'' — Öffnen, Exec, Prepare/Bind/Step, Transaktionen, Verwaltung | ✅ geprüft |
| ''std.db.sqlite'' — ''REAL'' (''BindFloat''/''ColumnFloat'') | ❌ Wert geht verloren ([[https://github.com/SEOLizer/LyX-Compiler/issues/1329|#1329]]) |
| ''std.db.sqlite'' — unter ''@capabilities'' | ❌ Exit 159, ''fcntl'' fehlt im Filter ([[https://github.com/SEOLizer/LyX-Compiler/issues/1276|#1276]]) |
| ''std.db.postgres'' — Protokoll, Abfragen, Prepared, Transaktionen | ✅ aus dem Quelltext; Host nur als IPv4 |
| ''std.db.mysql'' — Verbindung, Abfragen, Zeilenzugriff, Transaktionswirkung | ✅ gegen MariaDB 10.11 geprüft |
| ''std.db.mysql'' — Prepared Statements, Feld-Metadaten, Transaktions-Rückgabewerte | ❌ ([[https://github.com/SEOLizer/LyX-Compiler/issues/1332|#1332]]) |
| ''std.db.redis'' — Befehle | ✅; **Verbindung** nur zu öffentlich auflösbaren Namen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1330|#1330]]) |
Die SQLite- und MySQL-Beispiele dieser Seite sind übersetzt und gegen eine echte Datenbank ausgeführt worden (SQLite-Datei bzw. MariaDB 10.11); der PostgreSQL-Abschnitt folgt dem Quelltext der Unit, ohne laufenden Server geprüft zu sein.
----
===== 12. Weiterführend =====
* [[lyx_-_programmiersprache:units:db|std.db — Unit-Referenz]] · [[lyx_-_programmiersprache:units:db:sqlite|sqlite]] · [[lyx_-_programmiersprache:units:db:postgres|postgres]] · [[lyx_-_programmiersprache:units:db:mysql|mysql]] · [[lyx_-_programmiersprache:units:db:redis|redis]]
* [[lyx_-_programmiersprache:units:graphdb|std.graphdb]] — Knoten und Kanten statt Tabellen
* [[lyx_-_programmiersprache:units:net:mongo|std.net.mongo]] — MongoDB Wire Protocol
* [[lyx_-_programmiersprache:guides:netzwerk|Netzwerk-Guide]] — Namensauflösung, Capabilities, Timeouts
* [[lyx_-_programmiersprache:guides:daten-serialisierung|Daten & Serialisierung]] — was aus der Datenbank herausfällt, weiterverarbeiten
* [[lyx_-_programmiersprache:sprache:capabilities|Capabilities]] · [[lyx_-_programmiersprache:sprache:ffi|FFI]] (SQLite bindet libsqlite3 ein)