Skip to content

Five-minute SQLite quickstart

This path uses Node >=22.18.0 and node:sqlite. You do not need a database server. The first query is a plain tagged template. It does not need the SQLBraid compiler. The second query adds dynamic @braid and uses the lowering command that ships with SQLBraid.

All SQLite adapters share the SQLite dialect. The subpath selects the physical adapter:

Subpath Physical boundary Important limits
@sqlbraid/sqlite/node-sqlite Node DatabaseSync / StatementSync synchronous physical calls; exact INTEGER strings; native iterate() stream
@sqlbraid/sqlite/better-sqlite3 better-sqlite3 statements synchronous and event-loop blocking; statement-local safeIntegers(true); native iteration
@sqlbraid/sqlite/libsql @libsql/client requires intMode: "string"; interactive transactions; no pinned session or stream fallback
@sqlbraid/sqlite/wasm SQLite WASM OO1 OO1 statement ownership; async-generator adaptation for streams
@sqlbraid/sqlite/d1 Cloudflare D1 prepared binds; no streaming or callback transactions

In Deno 2.9.3, the node:sqlite iterator turns SQLite step errors into a normal EOF. Thus, SQLBraid rejects streaming on Deno with BRAID_STREAM_UNSUPPORTED. It does not return rows that are silently truncated. Materialized queries and transactions stay available. The native iterator of Node is not affected.

The public Database API stays async for all adapters. Awaitable<T> is only the type of the physical QueryExecutor SPI. It lets synchronous adapters return plain results without Promise wrappers. It does not make better-sqlite3 non-blocking.

Terminal window
mkdir braid-sqlite && cd braid-sqlite
pnpm init --init-type module
pnpm add sqlbraid @sqlbraid/sqlite
pnpm add --save-dev typescript @types/node@22
mkdir src

Create src/index.ts:

import { DatabaseSync } from "node:sqlite";
import { createNodeSqliteDatabase } from "@sqlbraid/sqlite/node-sqlite";
import { sql } from "@sqlbraid/sqlite";
interface UserRow {
id: string;
name: string;
}
const native = new DatabaseSync(":memory:");
try {
native.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)");
native.prepare("INSERT INTO users (name) VALUES (?)").run("Ada");
const db = createNodeSqliteDatabase(native);
const requestedId = 1;
const users = await db.all(sql.rows<UserRow>`
SELECT id, name FROM users WHERE id = ${requestedId}
`);
console.log(users);
} finally {
native.close();
}

Node 22.18.0 can run this erasable TypeScript directly:

Terminal window
node src/index.ts

The output is a row array, such as [{ id: "1", name: "Ada" }]. The requested ID is a value that the driver binds. It is not interpolated SQL text.

The node:sqlite adapter renders the logical statement to text with ? placeholders. Then it uses the documented DatabaseSync.prepare(text) and StatementSync APIs. Materialization and hint validation occur before the statement executes. The adapter prepares the statement again for each call and reports the reuse strategy simple. SQLBraid does not call SQLTagStore through an undocumented callable path.

In src/index.ts, replace the requestedId declaration and the query block with this code:

const requestedId: number | undefined = 1;
const users = await db.all(sql.rows<UserRow>`
SELECT id, name
FROM users
/*@braid where*/
/*@braid if ${requestedId !== undefined}*/
AND id = ${requestedId}
/*@braid end*/
/*@braid end*/
`);
console.log(users);

Build the TypeScript source with the SQLBraid compiler lowering. Then run the emitted JavaScript:

Terminal window
pnpm add @sqlbraid/template
pnpm add --save-dev @sqlbraid/cli
pnpm exec sqlbraid build --file src/index.ts --out-file build/index.js
node build/index.js

The compiler lowers the guarded template into build/index.js. The generated code imports its helpers from @sqlbraid/template, so the application needs it as a direct dependency.

  • @braid where emits WHERE only when a child emits SQL. It removes a leading AND or OR.
  • The condition is evaluated before the guarded value. Expressions in inactive branches are not evaluated.
  • The requested ID stays an ordinary SQLite bind.
  1. DatabaseSync owns the in-memory SQLite resource.
  2. createNodeSqliteDatabase(native) connects that physical resource to the SQLBraid runtime.
  3. sql.rows<UserRow> declares that the statement returns rows with the shape of UserRow.
  4. An ordinary value becomes a driver bind (? for SQLite). It never becomes SQL text.
  5. The compiler keeps the lazy guarded evaluation of the dynamic template.

For a write, use sql.command and db.execute. For exactly one row, use db.one. It throws a cardinality error if the result does not contain exactly one row. Read SQL tags and result kinds.

node:sqlite is a Node runtime API. It is not a server version. The profile records Node and the SQLite library that Node bundles.

SQLite surface Profile representation Status/caveat
INTEGER storage string Canonical exact decimal text; native bigint is internal transport only.
REAL storage JavaScript number SQLite binary64 approximate value.
STRICT tables SQLite-native affinity enforcement A schema feature, not a SQLBraid parser guarantee.
non-STRICT tables / ANY SQLite dynamic values The returned representation follows the stored value and driver.
JSON1 text Parse/validate JSON text with Standard Schema.
BLOB Buffer/bytes Keep binary or explicitly encode it.
RETURNING materialized rowset Output is accumulated before delivery; DML-returning streaming is not claimed.
  • The native binding uses ? placeholders and StatementSync. iterate() is the stream primitive. The bulk strategy is a prepared loop.
  • SQLite has no stored-procedure transport. Thus, registered functions and table-valued extensions stay ordinary SQL row queries.
  • Native SQLite SQL passes through without grammar rewriting. Transparency is not grammar support.
  • Exact integer strings and proven bigint transport are bind details.
  • Ordinary undefined IN values fail before acquisition with BRAID_BIND_VALUE_UNSUPPORTED. null is SQL NULL.
  • Arrays and other nested or container values stay unclassified, unless a test for the storage class proves them.
Terminal window
pnpm add @sqlbraid/sqlite better-sqlite3
import Database from "better-sqlite3";
import { createBetterSqlite3Database } from "@sqlbraid/sqlite/better-sqlite3";
import { sql } from "@sqlbraid/sqlite";
const native = new Database(":memory:");
try {
const db = createBetterSqlite3Database(native);
const rows = await db.all(sql.rows`SELECT 1 AS value`);
console.log(rows);
} finally {
native.close();
}

Save this code as src/better-sqlite3.ts and run node src/better-sqlite3.ts. The output is [ { value: '1' } ].

SQLBraid applies safeIntegers(true) to each statement. It exposes exact INTEGER values as decimal strings. iterate() is the real stream primitive. Bulk uses a prepared loop. Native calls block the event loop. If that is a problem, use a worker. Routines and active cancellation are unsupported.

Terminal window
pnpm add @sqlbraid/sqlite @libsql/client
import { createClient } from "@libsql/client";
import { createLibsqlDatabase } from "@sqlbraid/sqlite/libsql";
import { sql } from "@sqlbraid/sqlite";
const client = createClient({ url: "file:app.db", intMode: "string" });
try {
const db = createLibsqlDatabase(client, { intMode: "string" });
const rows = await db.all(sql.rows`SELECT 1 AS value`);
console.log(rows);
} finally {
client.close();
}

Save this code as src/libsql.ts and run node src/libsql.ts. The output is [ { value: '1' } ]. The client creates the file app.db.

The explicit intMode: "string" option is required. SQLBraid cannot infer the integer mode of an opaque client.

  • Transactions use the interactive transaction handle of libSQL. Ordinary calls do not claim one pinned session.
  • The adapter uses native batch() for bulk.
  • The adapter rejects db.stream() with BRAID_STREAM_UNSUPPORTED. It does not buffer a complete result.

Local file: libSQL clients and libSQL clients with an unknown protocol omit the optional command.insertId. The native binding rounds ROWID through Number before it returns a bigint. This occurs for each intMode. Query rows, transactions, bulk execution and affectedRows stay supported. For an exact ID, write INSERT ... RETURNING id in your SQL and use sql.rows. SQLBraid does not rewrite SQL or send a compensating query.

The SQLite inspector uses introspectionScope: "main" by default. Metadata capture does not inspect attached schemas. Missing fields do not prove that indexes or constraints are absent. The better-sqlite3 and libSQL targets are Compatible.

libSQL transaction options:

  • If the transaction options are omitted or empty, the adapter calls client.transaction() without a mode.
  • readOnly: false selects "write".
  • The local @libsql/client@0.18.0 file transport emits BEGIN TRANSACTION READONLY, but it does not block writes. Thus, SQLBraid reports read-only as guarded and rejects readOnly: true before it begins.
  • Opaque clients without a transport protocol get the same guard.
  • Remote transports that expose and enforce the documented "read" mode keep that option.

better-sqlite3 accepts Uint8Array bind views. It converts them to Buffer immediately before native calls.