Datenbanken
Jedes Datenbankmodul von Hilde bindet Werte getrennt vom Text der Anweisung: Ein Wert wird nie zu SQL. Verbindungen, Transaktionen und Cursor sind lineare Werte; ein Programm, das vergisst, einen davon zu schließen, übersetzt nicht.
SQLite
Standard.Sqlite prüft jedes @query und @execute beim Übersetzen gegen das Schema, das die Migrationsdateien aufbauen. Eine Abfrage, die eine fehlende Spalte nennt, oder SQL außerhalb der erlaubten Formen übersetzt nicht; das sind die Fehlercodes H8, siehe den Abschnitt SQL der Fehlercodes. Standard.Search ergänzt Volltextsuche mit FTS5.
Standard.SqlCipher ist dasselbe mit verschlüsselter Datei; sein Schlüssel ist ein linearer Wert und wird beim Schließen gelöscht.
Verteiltes SQL: SQLite, repliziert über Raft
Standard.SqliteReplica betreibt eine SQLite-Datenbank auf mehreren Rechnern und hält sie gleich, nach dem Vorbild von rqlite, aber eigenständig in Hilde geschrieben. Es baut auf Standard.Raft auf: Jeder Schreibvorgang ist ein Eintrag im Raft-Log, und jeder Knoten wendet die Einträge in derselben Reihenfolge auf seine eigene SQLite-Datei an. Standard.RaftTcp überträgt die Raft-Nachrichten zwischen den Knoten über TCP.
- Schreiben geht über den Leader.
proposeWriteprüft die Anweisung, bereitet sie gegen die Datenbank des Leaders vor und legt sie ins Log. Sie läuft auf jedem Knoten, sobald der Eintrag festgeschrieben ist. Ein Follower lehnt einen Schreibvorgang ab. - Nur gebundene Werte. Ein Schreibvorgang ist eine einzelne Anweisung, deren Werte alle Parameter sind; ein Literal in Anführungszeichen wird abgelehnt, im Text stehen also keine Daten.
- Keine Ergebnisse, die sich zwischen Knoten unterscheiden.
random,randomblob, die aktuelle Zeit und das aktuelle Datum sowie eine Zeitfunktion mitnowwerden abgelehnt. Stattdessen bindet der Aufrufer die Zeit oder die Zufallszahl, die er gewählt hat, als Wert. Auchattach,detach,pragma,vacuumund Transaktionsanweisungen werden abgelehnt; jeder Schreibvorgang steht für sich. - Überall dasselbe Ergebnis. Lehnt eine Constraint einen Schreibvorgang ab, dann auf jedem Knoten mit demselben SQLite-Code.
resultOfliefert das Ergebnis eines Schreibvorgangs über seinen Log-Index: ob er lief, wie viele Zeilen er geändert hat und den Code, wenn er nicht lief. Die letzten 1024 Ergebnisse werden aufbewahrt. - Zwei Arten zu lesen.
queryLocalliest, was der Knoten angewendet hat, unter Umständen hinter dem Leader. Für einen linearisierbaren Lesevorgang legt der Leader eine Barriere an, einen leeren Eintrag in seinem Term; hat er sie angewendet und ist in diesem Term noch Leader, liestqueryLinearizable, und kein anderer Leader kann etwas festgeschrieben haben, das dieser Knoten nicht hat. - Snapshots sind die Datenbank, kopiert mit der Online-Backup-Funktion von SQLite; ein zurückgefallener Knoten wird daraus wiederhergestellt.
Die Tests betreiben einen Verbund aus drei Knoten auf Dateien, mit Netzpartitionen: Ein abgeschnittener Leader bekommt seinen Schreibvorgang nie angewendet und kann nie linearisierbar lesen, der Leader auf der anderen Seite schon. Weil Raft weder Netz noch Platte noch Uhr besitzt, läuft jeder dieser Tests bei jedem Lauf gleich.
case proposeWrite(&!node, &store, "INSERT INTO kv (k, v) VALUES (?, ?)", &values) of
when Right(right as index: Nat64) do
skip;
when Left(left as refusal: Nat8) do
skip;
end case;
PostgreSQL
Standard.Postgres ist ein Client über die libpq des Systems, mit den Typen und dem Aufbau von Standard.Sqlite.
- Verbindung.
openWriteröffnet eine Verbindung, die schreiben darf,openReadereine, deren Transaktionen alle nur lesen; ein Schreibvorgang scheitert dann auch auf dem Server. Beide nehmen Host, Port, Datenbank, Benutzer, Passwort und eine Datei mit CA-Zertifikaten. - TLS. Jede Verbindung läuft über TLS mit sslmode verify-full: Das Zertifikat des Servers muss auf die angegebene CA-Datei zurückführen und den Hostnamen tragen. Ohne diese Prüfung lässt sich keine Verbindung öffnen.
- Werte. Werte werden als
$1,$2gebunden und in der Binärform ihres Typs übertragen; ein Text, der wie SQL aussieht, bleibt Text. - Typen. Int64, Float64, Bool, Text, Bytes, Null und Zeitpunkte als Mikrosekunden seit 1970-01-01 UTC, gesendet als timestamptz. Eine Spalte, die Null sein kann, liest man mit ihrer
OrNone-Funktion; eine Lesefunktion des falschen Typs hält das Programm an, denn das ist ein Fehler im Programm, nicht in den Daten. - Transaktionen.
beginliefert eine lineare Transaction, die nurcommitoderrollbackbeendet. Ein verschachteltesbeginwird abgelehnt, bevor etwas gesendet wird. Ist eine Anweisung in der Transaktion gescheitert, rollt der Commit zurück und scheitert mit 25P02. - Migrationen.
migratewendet die mit@migrationseingebetteten Migrationen an, jede in einer eigenen Transaktion, und vermerkt sie in der Tabelle hilde_migrations. - Fehler tragen den SQLSTATE;
isUniqueViolation,isConstraintViolationundisBusyordnen sie ein.
case openWriter(host, 5432, "app", "app", asSpan(&password), caFile) of
when Right(right as made: Connection[Writing]) do
var db: Connection[Writing] := made;
let values: Values := withText(withInt64(noValues(), 42), "Ada");
let changed: Either[PgError, Nat64] :=
execute(&!db, "INSERT INTO people (id, name) VALUES ($1, $2)", values);
closeDatabase(db);
when Left(left as error: PgError) do
logLine(describe(error));
end case;
MariaDB
Standard.MariaDB ist ein Client über die libmariadb des Systems, mit denselben Typen und Funktionen wie Standard.Postgres; ein Programm wechselt mit wenigen Änderungen zwischen beiden.
- Verbindung.
openWriterundopenReader, mit denselben Argumenten wie bei PostgreSQL; eine lesende Verbindung macht jede Transaktion schreibgeschützt. - TLS. Jede Verbindung verlangt TLS, und das Zertifikat des Servers wird gegen die CA-Datei geprüft, einschließlich des Hostnamens.
- Werte. Eine Anweisung wird vorbereitet, und ihre Werte werden an ihre Fragezeichen gebunden, getrennt von ihrem Text.
- Typen. Dieselben wie bei PostgreSQL. Ein Zeitpunkt ist Mikrosekunden seit 1970-01-01 UTC und wird als UTC-Datum mit Uhrzeit zwischen den Jahren 0 und 9999 gesendet. Spalten werden so gelesen, wie libmariadb sie umwandelt; eine Umwandlung, die libmariadb ablehnt, hält das Programm an.
- Transaktionen.
begin,commitundrollbackwie bei PostgreSQL. - Migrationen. MariaDB schreibt eine Änderung an der Form einer Tabelle sofort fest; eine Migration, die mittendrin scheitert, wird deshalb nicht rückgängig gemacht. Ihre Nummer wird nur vermerkt, wenn sie vollständig gelaufen ist.
- Fehler tragen die MariaDB-Fehlernummer und werden mit denselben drei Funktionen eingeordnet.
case openWriter("db.local", 3306, "shop", "app", asSpan(&password), "ca.pem") of
when Right(right as db: Connection[Writing]) do
let values: Values := withText(withInt64(noValues(), 7), "apple");
let changed: Either[MariaError, Nat64] :=
execute(&!db, "insert into stock (id, name) values (?, ?)", values);
closeDatabase(db);
when Left(left as failed: MariaError) do
printLn(describe(failed));
end case;
DuckDB
Standard.DuckDB öffnet eine Datenbank in einer Datei, mit abgeschaltetem Zugriff nach außen, ohne Erweiterungen und mit gesperrter Konfiguration. SCHLUCKI hält seine Tabellen in SQLite oder DuckDB.
Eine Schnittstelle
Standard.Database ist eine Schnittstelle über SQLite, SQLCipher, PostgreSQL und MariaDB; das Backend wird beim Öffnen gewählt, und der Code, der liest und schreibt, bleibt derselbe. Anweisungen verwenden auf jedem Backend Fragezeichen; für PostgreSQL werden daraus $1, $2. Ein Treiber ist nur dann im Programm, wenn sein Modul importiert wird: Standard.Database.Sqlite, Standard.Database.Postgres oder Standard.Database.MariaDB. Ein Fehler wird auf jedem Backend gleich eingeordnet.
Quelltext: github.com/heikozim/hilde.