Choosing the SQLite driver¶
tempest-db-js runs SQLite on two drivers. Node's built-in one is the default
and needs no install; better-sqlite3 is one option away when you need what it
exposes.
| Driver | Install | When to use |
|---|---|---|
node:sqlite (default) |
nothing — ships with Node ≥ 20 | The normal case |
better-sqlite3 |
npm install better-sqlite3 |
pragma(), loadable extensions, or the driver the rest of your service already uses |
The default: nothing to do¶
Say nothing and you run on node:sqlite:
import { Model, column, createSyncEngine, insert, select } from "tempest-db-js";
class Note extends Model {
static override tablename = "notes";
id = column.integer().primaryKey();
title = column.varchar(80).notNull();
}
using engine = createSyncEngine("sqlite:///app.db");
const session = engine.session();
session.execute(insert(Note).values({ id: 1, title: "hello" }));
console.log(session.execute(select(Note)).all());
// [ { id: 1, title: "hello" } ]
Switching to better-sqlite3¶
Install the package and name the driver. Two ways, same effect:
The engine option wins over the suffix when both appear — so a URL coming from the environment can be overridden in a test without rewriting the string.
Your models, queries and returned rows are identical on both drivers: the
same bigint, Date, boolean and JSON coercion, the same RETURNING, the
same stream(). Swapping drivers does not change your code.
Both are synchronous
Either one serves createSyncEngine and createEngine alike — the async
engine wraps the sync driver. SQLite has no truly async driver in Node; the
difference is the API shape you write, not I/O.
Driver options¶
driverOptions goes straight to the chosen driver's constructor, so each one
takes whatever its own documentation documents:
using reader = createSyncEngine("sqlite:///app.db", {
driver: "better-sqlite3",
driverOptions: { readonly: true }, // (1)!
});
- A
better-sqlite3option. Onnode:sqlitethe same idea is spelled{ readOnly: true }— different names, because it is the driver's API, not ours.
Connection pragmas¶
A SQLite pragma is per connection, not per file — so it belongs to the engine, not to a migration. Only one has a default that changes behavior:
using engine = createSyncEngine("sqlite:///app.db", {
sqlite: {
foreignKeys: true, // (1)!
journalMode: "wal", // (2)!
busyTimeoutMs: 5000, // (3)!
synchronous: "normal", // (4)!
},
});
- Defaults to
true. Turned on by us, not by SQLite. - Readers stop blocking the writer. Needs a real file.
- How long a writer waits on a lock before giving up.
- Durability vs write throughput.
Without foreign_keys = ON, your FK is decorative
SQLite ships with foreign-key enforcement off, per connection. Without turning
it on, an orphan INSERT is accepted and ON DELETE CASCADE never fires — the
constraint is in the schema and does nothing.
tempest-db-js turns it on by default, so the same model behaves the same on all
three databases. Set foreignKeys: false only for the case it exists for: loading
a dump whose insert order does not respect the graph.
A refused pragma is an error, not silence
SQLite answers a pragma it cannot honor by keeping the old value and saying nothing. Every pragma is read back after it is written, and a mismatch throws:
createSyncEngine("sqlite://:memory:", { sqlite: { journalMode: "wal" } });
// Error: SQLite refused PRAGMA journal_mode = wal for ":memory:" and stayed on
// "memory" — an in-memory database cannot use WAL.
An in-memory database cannot do WAL. Better to learn that when the engine opens than when the latency did not improve.
A table-rebuilding migration turns FK enforcement back on
SQLite cannot alter a constraint, so the migration engine rebuilds the table
(CREATE new / copy / DROP old), turning FK enforcement off around the copy and
back on at the end. With foreignKeys: false, such a migration leaves it on.
Passing sqlite to a PostgreSQL or MySQL engine throws — pragmas do not exist there,
and silently ignoring them is how a durability choice gets lost.
A wrong name is an error, not silence¶
A driver this package does not ship fails when the engine is created:
createSyncEngine("sqlite:///app.db", { driver: "sqlite3" });
// Error: Unknown SQLite driver "sqlite3"; supported: "node:sqlite" (built-in, default)
// and "better-sqlite3".
The URL suffix is more forgiving — on purpose
sqlite+aiosqlite:///app.db and postgresql+asyncpg://… do not throw:
those are Python-ecosystem drivers, and a URL copied from a Python service
should still connect (on that database's default driver here). A suffix
tempest-db-js recognizes selects; one it does not is ignored.
The driver option, on the other hand, is an explicit choice you made in
TypeScript — so it rejects what does not exist instead of ignoring it.
PostgreSQL and MySQL¶
Each runs on exactly one driver (postgres / postgres.js and mysql2). Naming
that driver is accepted and changes nothing; naming another one throws, for the
same reason as above:
createEngine("postgresql://app@localhost/app", { driver: "postgres" }); // ok
createEngine("postgresql://app@localhost/app", { driver: "asyncpg" });
// Error: Unknown postgresql driver "asyncpg"; tempest-db-js runs postgresql on "postgres".
Recap¶
- SQLite runs on
node:sqliteby default — zero install. { driver: "better-sqlite3" }orsqlite+better-sqlite3://switches drivers; the option wins over the suffix.driverOptionsis forwarded to the chosen driver's constructor.- An unknown name in the option throws; a foreign-ecosystem suffix in the URL is ignored, so Python service URLs keep working. 🚀