Databases

Every database module of Hilde binds values apart from the statement's text: a value never becomes SQL. Connections, transactions and cursors are linear values, so a programme that forgets to close one does not compile.

SQLite

Standard.Sqlite checks every @query and @execute against the schema that the migration files build, when the programme is compiled. A query that names a missing column, or SQL outside the accepted forms, does not compile; these are the H8 error codes, see the SQL section of the error codes. Standard.Search adds full-text search with FTS5.

Standard.SqlCipher is the same with an encrypted file; its key is a linear value, wiped when it is closed.

Distributed SQL: SQLite replicated through Raft

Standard.SqliteReplica runs one SQLite database on several machines and keeps them the same, after the pattern of rqlite but written from scratch in Hilde. It is built on Standard.Raft: every write is an entry of the Raft log, and every node applies the entries in the same order to its own SQLite file. Standard.RaftTcp carries the Raft messages between the nodes over TCP.

The tests run a cluster of three nodes on files, with partitions: a leader cut off from the others never gets its write applied and never makes a linearizable read, while the leader on the other side does. Because Raft owns no network, disk or clock, every run of these tests is the same.

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 is a client over the system's libpq, with the types and the shape of 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 is a client over the system's libmariadb with the same types and functions as Standard.Postgres, so a programme moves between the two with little change.

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 opens a database in a file, with external access and extensions switched off and the configuration locked. SCHLUCKI keeps its tables in SQLite or DuckDB.

One interface

Standard.Database is one interface over SQLite, SQLCipher, PostgreSQL and MariaDB; the backend is chosen when the database is opened, and the code that reads and writes stays the same. Statements use question marks on every backend; for PostgreSQL they become $1, $2. A driver is in a programme only when its module is imported: Standard.Database.Sqlite, Standard.Database.Postgres or Standard.Database.MariaDB. An error is classified the same way on every backend.

Source: github.com/heikozim/hilde.