sqlite

js/database/sqlite.ts

fino:database/sqlite — SQLite database access via system libsqlite3.

SQLite C API reference: https://www.sqlite.org/c3ref/intro.html

Uses dlopen to load the system-installed libsqlite3. The re-exported sqliteAvailable flag reports whether those bindings loaded, so code that may run without system SQLite can branch instead of catching. Release coverage requires the library to be present; CI should set FINO_REQUIRE_SQLITE=1 when running SQLite tests so missing bindings fail the lane instead of skipping it. All file I/O is routed through the realm's FileSystem provider via a JS-implemented sqlite3_vfs, so virtual providers (MemoryFileSystem, S3FileSystem, etc.) work transparently.

The release baseline focuses on core connection, statement, transaction, vector-helper, extension-loading, and VFS-backed file behavior. SQLite-native behavior that is already available through SQL or PRAGMA, such as PRAGMA busy_timeout, should be used directly. Node-style convenience APIs for backup, serialize/deserialize, busy-timeout helpers, and broader WAL/concurrency parity are outside this baseline.

Supported SQLite C/VFS subset

Database covers the core C API lifecycle used by this module: open/close, prepare/step/finalize, parameter binding, column reads, transactions through SQL, and trusted extension loading. It does not wrap backup, serialization, or busy-timeout convenience APIs; use SQLite SQL, PRAGMA statements, or native extensions for those features.

Fino's JavaScript sqlite3_vfs implements the file operations SQLite needs for normal database and journal I/O through the active FileSystem provider: open, delete, access, full-pathname, read, write, truncate, sync, file size, sector size, device characteristics, locking, unlock, reserved-lock checks, randomness, sleep, current time, and last-error callbacks.

Deterministic file controls are supported for lock state, size hints, chunk size, file pointer, last errno, persistent WAL state, powersafe overwrite, disabled mmap size, moved-file checks, lock timeout, and data version. Unsupported controls such as VFS name/proxy hooks, PRAGMA interception, temp-filename allocation, atomic write groups, size limits, reserve bytes, external-reader, checksum-file, null-I/O, and filestat return SQLITE_NOTFOUND so SQLite can use its normal fallback paths.

Usage:

import { Database } from 'fino:database/sqlite';
const db = await Database.open('/path/to.db');
await db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)');
const stmt = db.prepare('INSERT INTO t VALUES (?, ?)');
await stmt.run(1n, 'hello');
const rows = await db.prepare('SELECT * FROM t').all();
await db.close();

Interfaces

interface DatabaseOptions {

Options for opening a SQLite database.

Options control the filesystem provider, open mode, and INTEGER result mapping for the connection. They are read once by Database.open(); changing the object afterwards has no effect on the connection.

import { Database } from 'fino:database/sqlite';

const db = await Database.open('/data/app.db', {
  readonly: true,
  safeIntegers: false,
});
await db.close();

Properties

fs?: FileSystem

FileSystem provider used by Fino's SQLite VFS.

Defaults to a new DiskFileSystem. Supplying a custom provider lets SQLite read and write through virtual filesystems — a database file, its journal, and its WAL all flow through this provider rather than direct disk I/O.

import { Database } from 'fino:database/sqlite';
import { DiskFileSystem } from 'fino:file';

const db = await Database.open('/data/app.db', { fs: new DiskFileSystem() });
await db.close();
readonly?: boolean

Open the database in read-only mode.

The default is false, which opens read-write and creates the database if needed. Read-only connections reject writes at SQLite level and fail when the file does not exist.

safeIntegers?: boolean

Return INTEGER columns as bigint when true.

The default is true. Set to false to coerce INTEGER results to JavaScript number, accepting precision loss for values outside the safe integer range. This does not affect lastInsertRowid, which is always a bigint.

Types

type SqlValue = null | undefined | bigint | number | string | Uint8Array

Values accepted for SQLite parameter binding and returned from result rows.

null and undefined bind as SQL NULL. A whole number or a bigint binds as INTEGER; fractional numbers bind as REAL. Strings bind as UTF-8 text, and Uint8Array binds as BLOB. Reads map back the same way: INTEGER columns decode as bigint (or number when safeIntegers is false), TEXT as string, BLOB as Uint8Array, and NULL as null.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (id INTEGER, score REAL, name TEXT, data BLOB)');
await db.prepare('INSERT INTO t VALUES (?, ?, ?, ?)')
  .run(1n, 0.5, 'ada', new Uint8Array([1, 2]));
await db.close();

Classes

class Statement {

Prepared SQLite statement with lazy compilation and typed row helpers.

Statements are created by Database.prepare() and compile on first use. Positional parameters are bound from rest arguments; pass one plain object to bind named parameters without the leading :, $, or @. Call finalize() when a reusable statement is no longer needed. Parameter binding is strict: positional calls must provide exactly one value per bind slot, and named-parameter objects must exactly cover the statement's named parameters without extra keys.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
const stmt = db.prepare('INSERT INTO users VALUES (?, ?)');
await stmt.run(1n, 'Ada');
stmt.finalize();
await db.close();

Constructors

constructor(db: Database, sql: string, safeIntegers: boolean)

Create a statement wrapper over sql for an open connection.

Application code should normally call Database.prepare() instead of this constructor so the statement inherits the database's integer mapping and is tracked for cleanup at close(). safeIntegers controls whether INTEGER columns are returned as bigint. Compilation remains lazy until the first execution method is called.

import { Database, Statement } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
const stmt = new Statement(db, 'SELECT 1 AS value', true);
console.log(await stmt.get());
stmt.finalize();
await db.close();

Methods

run(...params: SqlValue[]): Promise<{ changes: number; lastInsertRowid: bigint; }>

Execute the statement and return write metadata.

Parameters may be positional values or one named-parameter object. The statement is reset after execution. Parameter counts are validated before binding. SQL errors reject with the database error message. Result rows, if any, are discarded; the resolved object carries the connection's change count and last insert rowid.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (name TEXT)');
const result = await db.prepare('INSERT INTO t VALUES (?)').run('hello');
console.log(result.changes, result.lastInsertRowid);
await db.close();
get(...params: SqlValue[]): Promise<Record<string, SqlValue> | undefined>

Execute and return the first row.

Returns undefined when the query produces no rows. Column names are used as object keys. Parameter counts are validated before binding. The statement is reset before returning or throwing.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
const row = await db.prepare('SELECT 42 AS answer').get();
console.log(row?.answer);
await db.close();
all(...params: SqlValue[]): Promise<Record<string, SqlValue>[]>

Execute and return all rows.

This buffers every result row in memory. Use iterate() for large result sets. Parameter counts are validated before binding. The statement is reset before returning or throwing, and rows resolve in result order.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
const rows = await db.prepare('SELECT 1 AS n UNION ALL SELECT 2').all();
console.log(rows.length);
await db.close();
async *iterate(...params: SqlValue[]): AsyncGenerator<Record<string, SqlValue>>

Async-iterate rows one at a time.

The statement remains active for the duration of iteration and is reset in a finally block when iteration finishes, throws, or is abandoned early. Parameter counts are validated before binding. Each step is serialized on the connection individually, so other operations on the same connection may interleave between rows.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
for await (const row of db.prepare('SELECT 1 AS n').iterate()) {
  console.log(row.n);
}
await db.close();
finalize(): void

Finalize and free this statement.

Calling finalize() more than once is allowed. Any later execution method throws Statement is finalized.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
const stmt = db.prepare('SELECT 1');
stmt.finalize();
await db.close();

class Database {

SQLite database connection backed by Fino's SQLite VFS.

Open connections with Database.open(). The connection owns a native sqlite3* pointer and a per-connection VFS registration; call close() when finished. Close finalizes any statements that were not explicitly finalized. If the connection remains open when its Realm shuts down, Fino closes it before disposing the Realm isolate so SQLite cannot retain VFS pointers into released isolate memory. Methods throw after the connection has been closed.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (value TEXT)');
await db.close();

Getters

get ptr(): ArrayBuffer

Internal sqlite3 pointer for statement helpers.

This getter exposes the native pointer wrapper used by this module. It is public for Statement integration but is not needed by normal application code. The value is a Fino pointer buffer containing the sqlite3* address and becomes invalid after close().

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
console.log(db.ptr.byteLength);
await db.close();
get vectorsAvailable(): boolean

Whether the current sqlite build supports extension loading and sqlite-vec was found. Probed lazily on first access.

The probe tries FINO_SQLITE_VEC_PATH first when present, then common platform paths, and returns true only when sqlite-vec actually loaded. A failed probe caches false. Access may enable extension loading on the connection.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
console.log(db.vectorsAvailable);
await db.close();
get changes(): number

Number of rows changed by the most recent DML statement.

This mirrors sqlite3_changes() for the connection. It is meaningful after INSERT, UPDATE, DELETE, and similar statements. It throws only if SQLite bindings are unavailable.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (value TEXT)');
await db.prepare('INSERT INTO t VALUES (?)').run('x');
console.log(db.changes);
await db.close();
get lastInsertRowid(): bigint

Row ID of the most recent INSERT.

This mirrors sqlite3_last_insert_rowid() and returns a bigint regardless of safeIntegers, because rowids may exceed JavaScript's safe integer range.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY)');
await db.prepare('INSERT INTO t DEFAULT VALUES').run();
console.log(db.lastInsertRowid);
await db.close();

Static Methods

static async open(path: string, opts: DatabaseOptions = {}): Promise<Database>

Open a database at path. Use ':memory:' for an in-memory database. Pass { fs } to route I/O through a custom FileSystem provider.

By default, the database opens read-write and is created if missing. { readonly: true } opens read-only. Each connection registers a private Fino VFS name so file operations go through the chosen filesystem provider. Throws when SQLite is unavailable, open fails, or VFS registration fails.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:', { safeIntegers: true });
await db.close();

Methods

exec(sql: string): Promise<void>

Execute one or more SQL statements with no result rows.

This uses sqlite3_exec() and is best for schema setup, pragmas, and simple SQL batches. Use prepare() for parameter binding or reading result rows. Treat this as a trusted-SQL-only API: never concatenate untrusted user input into exec() strings. Throws on SQL errors or when the database is closed.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)');
await db.close();
prepare(sql: string): Statement

Compile a SQL statement and return a reusable Statement. Compilation is lazy — it happens on the first .run/.get/.all/.iterate call.

Throws immediately if the database is closed. SQL syntax errors are thrown later when the statement first compiles.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
const stmt = db.prepare('SELECT ? AS value');
console.log(await stmt.get(123));
stmt.finalize();
await db.close();
async transaction<T>(fn: () => Promise<T>): Promise<T>

Run fn inside a BEGIN/COMMIT transaction. Rolls back on throw.

The transaction starts with BEGIN, commits if fn resolves, and attempts ROLLBACK if fn throws, then rethrows fn's error. The resolved value is whatever fn returned. Nested transaction behavior depends on SQLite and the SQL executed by fn; this helper does not create savepoints.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE TABLE t (value TEXT)');
await db.transaction(async () => {
  await db.prepare('INSERT INTO t VALUES (?)').run('ok');
});
await db.close();
loadExtension(path: string, entryPoint?: string): void

Load a SQLite extension from path. Requires a SQLite build with extension loading enabled (e.g. Homebrew sqlite on macOS).

Loading an extension executes native code inside the current process. Only load trusted extension libraries from trusted paths.

The optional entryPoint symbol is passed through to sqlite3_load_extension. Throws when an absolute path does not exist, with SQLite's extension error message when loading fails, and if the database is closed.

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
db.loadExtension('/usr/local/lib/sqlite-vec.dylib');
await db.close();
async close(): Promise<void>

Close the database and unregister the VFS.

Calling close() more than once is allowed. The close runs after any in-flight queued operations, and any statements created by this connection are finalized before the native database handle is closed. Closing also removes the automatic Realm-shutdown cleanup registered by open().

import { Database } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.close();

Functions

function vec(arr: Float32Array | number[]): string

Encode a Float32Array or number[ as the '[x,y,z]' text literal that sqlite-vec's vec0 virtual table expects in INSERT and MATCH expressions.

The returned string is not SQL-escaped; bind it as a parameter or use it only where sqlite-vec expects a vector literal. Numbers are converted through Float32Array when the input is a regular array, so values round to float32 precision.

import { Database, vec } from 'fino:database/sqlite';

const db = await Database.open(':memory:');
await db.exec('CREATE VIRTUAL TABLE docs USING vec0(embedding float[3])');
await db.prepare('INSERT INTO docs (rowid, embedding) VALUES (?, ?)')
  .run(1n, vec([0.1, 0.2, 0.3]));
await db.close();

function vecDecode(blob: Uint8Array): Float32Array

Decode a sqlite-vec BLOB column back to a Float32Array. sqlite-vec stores vectors as little-endian float32 blobs.

The returned array views a sliced copy of the input buffer, so it is aligned for Float32Array use and independent of the original byte offset. Invalid byte lengths that are not multiples of four follow Float32Array construction rules and may throw.

import { vecDecode } from 'fino:database/sqlite';

const bytes = new Uint8Array(new Float32Array([1, 2]).buffer);
const vector = vecDecode(bytes);
console.log(vector[0]);