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.

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.

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.

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.