SQL-first persistence for Node.js. A zero-dependency wrapper over the built-in
node:sqlite DatabaseSync. SQL lives in .sql files tagged with comment
markers; SqlRite turns each tagged block into a JavaScript method.
- Requires Node
>=25.0.0, npm>=11.1.0. - No runtime dependencies.
- Two facades over one core: async (Worker thread) and sync.
npm install @possumtech/sqlrite- Input: one or more directories of
.sqlfiles containing tagged blocks. - Output: an object whose methods are generated from those tags.
- No implicit methods (
find,save, …). Every operation is an explicit SQL block.
A block runs from its tag line to the next tag (or end of file). Empty blocks are skipped.
| Tag | Maps to | Behavior |
|---|---|---|
-- INIT: <name> |
(runs at open) | Executed once when the DB opens. Use idempotent DDL (CREATE TABLE IF NOT EXISTS) and PRAGMAs. Supports $var templating via the params option. |
-- EXEC: <name> |
db.<name>(params) |
db.exec() of the block, with optional $var string templating. Trusted, developer-authored SQL only — see EXEC. |
-- PREP: <name> |
db.<name>.{all,get,run}(params) |
Prepared statement. The only parameterized path for runtime values. |
-- INIT names are not deduplicated. Duplicate -- EXEC/-- PREP names emit a
warning; the last definition wins.
Directories are scanned recursively for .sql files. Files are sorted by
basename, numerically (001-*.sql before 002-*.sql), across all directories
merged into one execution plan.
DB operations run in a dedicated Worker; methods return Promises. Construct only
via open() — the constructor throws otherwise.
import SqlRite from "@possumtech/sqlrite";
const sql = await SqlRite.open({ path: "data.db", dir: "sql" });
await sql.addUser.run({ name: "Alice", meta: { theme: "dark" } });
const user = await sql.getUserByName.get({ name: "Alice" });
await sql.close(); // or: await using sql = await SqlRite.open(...)import { SqlRiteSync } from "@possumtech/sqlrite";
const sql = new SqlRiteSync({ dir: ["migrations", "src/users"] });
const users = sql.getUserByName.all({ name: "Alice" });
sql.close(); // or: using sql = new SqlRiteSync(...)new SqlRiteSync() does not register custom functions. Use the async
SqlRiteSync.open() if you pass functions (registration is async).
| Import | Export |
|---|---|
@possumtech/sqlrite |
default SqlRite (async), named SqlRiteSync |
@possumtech/sqlrite/sync |
default SqlRiteSync |
@possumtech/sqlrite/core |
default SqlRiteCore (static utilities) |
A -- PREP method exposes three modes:
| Mode | Use | Returns (sync / async) |
|---|---|---|
.run(params) |
INSERT/UPDATE/DELETE |
{ changes, lastInsertRowid } |
.get(params) |
single row | row object or undefined |
.all(params) |
multiple rows | array of row objects |
- Use named parameters (
$name,:name, or@name). The JS interface takes an object; keys map to parameter names. Leading$/:/@on keys is stripped, so{ name }binds$name. - Object and array parameter values are
JSON.stringify-ed on input. Output is not parsed — callJSON.parse()yourself.
-- PREP: addUser
INSERT INTO users (name, meta) VALUES ($name, $meta);
-- PREP: searchUsers
SELECT * FROM users WHERE name REGEXP $pattern;Integer columns are read as JS number by default; a value above 2^53 − 1
throws on read rather than losing precision. Append the bigint flag to a
-- PREP tag to read that statement's integer columns as BigInt instead:
-- PREP: feeBalance bigint
SELECT SUM(amount) AS total FROM ledger WHERE account = $account;The flag is scoped to that one statement. The returned value is a BigInt
(typeof total === "bigint"): arithmetic cannot mix BigInt and number, and
JSON.stringify throws on BigInt (supply a replacer). For a connection-wide
default, pass readBigInts: true in options (it passes through to
DatabaseSync). Passing a BigInt as a parameter already works without the
flag.
-- EXEC runs db.exec() (one or more statements) after $var templating. It
is for developer-authored SQL with constant or developer-supplied inputs —
DDL, PRAGMAs, maintenance. It is not a parameterized path: templated values
are string-interpolated (string values are single-quote escaped; numbers,
booleans, and null are inlined; identifiers are not handled). Do not pass
untrusted input through EXEC. For runtime values, use -- PREP with .run().
-- EXEC: insertKv
INSERT INTO kv (key, val) VALUES ($key, $val);sql.insertKv({ key: "role", val: "admin" }); // val is escaped, not boundtransaction(calls) runs a list of -- PREP statements atomically: it issues
BEGIN, runs each call with bound parameters, then COMMIT; any error triggers
ROLLBACK and the error is rethrown (async: rejects). It is the bound,
runtime-safe primitive — the values are parameters, not interpolated. In the
async facade the whole batch is one Worker round-trip.
await sql.transaction([
{ name: "debit", params: { id: from, amt } },
{ name: "credit", params: { id: to, amt } },
]);
// both commit, or neither doesEach call is { name, params, mode }. name must be a -- PREP statement
(an unknown name throws and rolls back); mode is "run" (default), "get",
or "all". transaction returns the array of per-call results. Only -- PREP
statements participate; -- EXEC is the templated, non-transactional path.
-- INIT blocks support $var substitution from the params option (same
templating rules as EXEC).
-- INIT: configure
PRAGMA cache_size = $cacheSize;await SqlRite.open({ dir: "sql", params: { cacheSize: 5000 } });| Option | Type | Default | Description |
|---|---|---|---|
path |
string |
":memory:" |
SQLite database file path. |
dir |
string | string[] |
"sql" |
Directories scanned for .sql files. |
functions |
string | string[] |
— | Module paths for custom SQL functions. |
params |
object |
— | $var substitutions for -- INIT blocks. |
All other keys pass through to the node:sqlite DatabaseSync constructor
(e.g. readOnly, timeout, allowExtension). Unknown keys are ignored;
invalid option types throw at construction.
SqlRite applies these via PRAGMA on every connection: journal_mode = WAL,
synchronous = NORMAL.
It also sets these DatabaseSync options, each overridable by passing the same
key in your own options:
| Option | SqlRite default | Effect |
|---|---|---|
enableForeignKeyConstraints |
true |
Enforces foreign keys. |
enableDoubleQuotedStringLiterals |
false |
Rejects double-quoted string literals (a misspelled "identifier" errors instead of becoming a string). |
defensive |
true |
Blocks SQL that can corrupt the file: writable_schema, journal_mode=OFF, schema_version, shadow-table writes. |
REGEXP—col REGEXP $patternusing JavaScriptRegExp. Compiled patterns are cached per connection. ANULLsubject yields no match.uuid()—crypto.randomUUID(). Usable as a column default:id TEXT PRIMARY KEY DEFAULT (uuid()).
Point functions at JS modules. Each module's filename becomes the SQL function
name. Functions are registered before any SQL block loads, so they are available
in -- INIT and prepared statements. Modules resolve dependencies from the
host app's node_modules.
// db/getTokens.js
export const deterministic = true; // optional; enables query optimization
export default (text) => text.length; // required; the handlerconst sql = await SqlRite.open({ dir: "sql", functions: ["./db/getTokens.js"] });-- PREP: longPosts
SELECT * FROM posts WHERE getTokens(body) > 1000;node scripts/codegen.js [dir] # writes SqlRite.d.tsGenerates TypeScript declarations for the dynamically generated methods from the
.sql files in dir (default "sql").
- Integer columns are read as JS
numberunless a statement opts intoBigInt(see bigint reads); without it, a value above2^53 − 1throws on read rather than losing precision. transaction()composes-- PREPstatements only.-- EXECand-- INITare templated, not bound, and do not participate intransaction().- The async facade processes one Worker message at a time; calls are serialized, not concurrent.
- Discover methods: grep for
-- PREP:/-- EXEC:. - Discover schema: grep for
-- INIT:. - Add an operation: add a tagged block to a
.sqlfile, then calldb.<name>(runcodegen.jsto refresh types). - Bind runtime values with
-- PREP+ an object of named parameters; never interpolate untrusted input through-- EXEC. - Group dependent writes with
transaction([{ name, params }, …])for atomicity. - Read integers beyond
2^53with abigint-flagged-- PREP.
MIT © @wikitopian