Files
SnowLuma/.pnpm-store/v11/files/e0/0d7b3ccb3fbf062f0f147c9ef45764b950105aad3dba9d0336120cb70da9ed7496a666d2977f220eb41576a910161870e86870e1faab22dde0a8f541db2c4a
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

4955 lines
193 KiB
Plaintext

// The JS half of the driver. The native binding (lib/sqlite3-binding.js)
// provides the Database/Statement/Backup classes and the constants; this
// file adds the callback conveniences around them — the statement cache,
// Database#run/get/all/each/map on top of Statement, backup creation,
// trace/profile/change event wiring — and then installs the promise API
// from lib/promises.js over the finished surface.
//
// Types come from lib/native.d.ts (the native layer's shape); everything
// this file adds to the public surface is declared in lib/augment.d.ts
// and emitted into the generated lib/sqlite3.d.ts.
import diagnostics_channel from 'node:diagnostics_channel';
import { EventEmitter } from 'node:events';
import os from 'node:os';
import path from 'node:path';
import { migrate } from './migrate.js';
import { pool } from './pool.js';
import {
associateStatement,
installPromiseApi,
retracePromiseApi,
} from './promises.js';
import binding from './sqlite3-binding.js';
import { extendTrace } from './trace.js';
/**
* A native class (Database, Statement or Backup) before the EventEmitter
* prototype is copied onto it.
*
* @typedef {new (...args: never[]) => object} NativeClass
*/
/**
* `sqlite3.cached` — a registry of connections shared by resolved
* database path. Special filenames (`''`, `':memory:'`) are never
* cached; a second call with the same path returns the open connection
* and still fires the callback once it is ready.
*
* @typedef {object} CachedRegistry
* @property {(filename: string, callback?: (this: import('./sqlite3-binding.js').Database, err: Error | null) => void) => import('./sqlite3-binding.js').Database} Database Open (or reuse) a connection, optionally with a callback.
* @property {Record<string, import('./sqlite3-binding.js').Database>} objects The registry itself, keyed by resolved path.
*/
/**
* The constructor type of the v9 `Database` wrapper: every pre-v9
* positional form plus the {@link OpenOptions} object forms. Declared
* explicitly (rather than as `typeof` the class) so the namespace typedef
* below does not reference the module it lives in — that self-reference
* is a type-resolution cycle.
*
* @typedef {new (filename: string, a?: number | OpenOptions | ((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void), b?: ((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void) | OpenOptions) => import('./sqlite3-binding.js').Database} DatabaseConstructor
* @since 9.0.0
*/
/**
* The public `sqlite3` namespace object the package exports as its
* default: the native binding (the five classes and every SQLite
* constant with its literal value) plus the JS-layer `verbose`,
* `cached`, `open`, `deserializeFromBytes` and `pool`. `Database` is the
* v9 wrapper constructor (a real subclass of the native class) so the
* {@link OpenOptions} constructor forms typecheck; instances satisfy the
* native type everywhere.
*
* @typedef {import('./sqlite3-binding.js').NativeBinding & {
* Database: DatabaseConstructor,
* verbose: () => sqlite3,
* cached: CachedRegistry,
* open: import('./promises.js').OpenFunction,
* deserializeFromBytes: (bytes: Uint8Array | ArrayBuffer | DataView, options?: import('./native.js').DeserializeOptions) => Promise<import('./sqlite3-binding.js').Database>,
* pool: typeof import('./pool.js').pool,
* iterdump: (db: import('./sqlite3-binding.js').Database) => AsyncGenerator<string, void, void>,
* migrate: typeof import('./migrate.js').migrate,
* subscribeQueries: (onMessage: (message: { sql: string, database: import('./sqlite3-binding.js').Database, duration: bigint, durationMs: number }) => void) => () => void,
* flushQuerySpans: () => void,
* }} sqlite3
*/
const sqlite3 = /** @type {sqlite3} */ (/** @type {unknown} */ (binding));
const { Database: NativeDatabase, Statement, Backup, Session, Blob } = sqlite3;
/**
* Compiles a function that builds one result row from its arguments.
*
* The addon calls this once per result shape and caches what it returns, then
* builds every row with a single call into it. That is much faster than
* storing each column into a fresh object from C++: a generated function has
* one monomorphic shape, so V8 allocates the row with its final layout
* instead of growing and re-shaping it column by column.
*
* The generated object is a plain object literal, which is what makes it a
* drop-in for the previous per-column stores — same prototype, same
* result-column order, same last-duplicate-wins collapse, and the same
* treatment of a `__proto__` column (the literal form assigns the prototype
* rather than creating an own property, exactly as a property store did).
*
* `new Function` is the only way to get a per-shape monomorphic builder, and
* it is unavailable in realms that forbid code generation from strings. The
* addon treats a throw here as "no factory" and falls back to its own store
* loop, so this is a performance feature that degrades rather than fails.
* @param {string[]} names the result column names, in column order.
* @param {boolean} wantArray true for the `{ rowMode: 'array' }` shape.
* @returns {(...values: unknown[]) => unknown} the compiled row builder.
*/
function makeRowFactory(names, wantArray) {
const params = names.map((_, i) => `v${i}`).join(',');
if (wantArray) {
return new Function(`return function(${params}){return [${params}]}`)();
}
// JSON.stringify is what escapes the column names into the source: they
// come from user SQL and can contain quotes, backslashes and newlines.
const body = names
.map((name, i) => `${JSON.stringify(name)}:v${i}`)
.join(',');
return new Function(`return function(${params}){return {${body}}}`)();
}
sqlite3.setRowFactoryGenerator(makeRowFactory);
/**
* Copies `source`'s prototype onto `target`, giving the native classes
* EventEmitter behaviour without a runtime class hierarchy.
*
* @param {NativeClass} target the native class to extend.
* @param {NativeClass} source the class whose prototype is copied.
* @returns {void}
* @private
*/
function inherits(target, source) {
Object.assign(target.prototype, source.prototype);
}
inherits(NativeDatabase, EventEmitter);
inherits(Statement, EventEmitter);
inherits(Backup, EventEmitter);
inherits(Session, EventEmitter);
inherits(Blob, EventEmitter);
// --- Node permission model, extension policy, untrusted files (D11) -----
//
// Node's --permission model restricts the JS fs layer; this package's C
// layer calls open(2) directly, so without these checks a program run with
// --permission --allow-fs-read=/data could read and write any file on the
// system through a SQLite connection (proven by probe, see
// docs/security.md). The checks below run at the JS boundary every open
// path goes through. This is defence in depth, not a sandbox: SQL that
// reaches the filesystem through channels the checks and the ATTACH gate
// do not cover (an unrestricted custom authorizer, a VFS extension) can
// still touch it — docs/security.md names what remains open.
/**
* True when Node's permission model is active for this process (or worker
* environment). `process.permission` exists only under `--permission` on
* every supported Node (observed on 24 and 26; there is no `isEnabled`
* method — it was removed before Node 24), so its presence is the gate
* and the cost when the model is off is one property read.
*
* @returns {boolean} whether the permission model is active.
* @private
*/
function permissionModelActive() {
return typeof process.permission?.has === 'function';
}
/**
* Builds the refusal error for a permission-model denial: Node's own
* `ERR_ACCESS_DENIED` shape (code plus `permission` and `resource`
* properties) with a message that names the path, the scope and a remedy
* that actually works.
*
* @param {'FileSystemRead' | 'FileSystemWrite'} permission the denied scope.
* @param {string} resource the path that was denied.
* @param {string} detail what the operation needed and why.
* @returns {Error} the ERR_ACCESS_DENIED-shaped error.
* @private
*/
function accessDenied(permission, resource, detail) {
const flag =
permission === 'FileSystemRead'
? '--allow-fs-read'
: '--allow-fs-write';
const err = new Error(
`${detail} The Node permission model denies ${permission === 'FileSystemRead' ? 'fs.read' : 'fs.write'} for ${resource}; start Node with ${flag} to permit it (or drop --permission).`,
);
/** @type {any} */ (err).code = 'ERR_ACCESS_DENIED';
/** @type {any} */ (err).permission = permission;
/** @type {any} */ (err).resource = resource;
return err;
}
/**
* Parses a SQLite URI filename (`file:` prefix, only interpreted when the
* open used `OPEN_URI`) using SQLite's own grammar — the WHATWG `URL`
* parser is wrong here: it turns the relative `file:foo.db` into the
* root-absolute `/foo.db`.
*
* Refuses URI forms this package cannot map to a checkable path, rather
* than passing them through: an unparsed URI would reach `open(2)`
* unchecked, which is exactly the hole the checks exist to close.
*
* @param {string} uri the `file:` URI.
* @returns {{ memory: true } | { memory: false, path: string, readonly: boolean }}
* the parsed target: in-memory, or a path with its write mode.
* @throws {Error} ERR_ACCESS_DENIED for URI forms that cannot be checked.
* @private
*/
function parseSqliteUri(uri) {
/**
* @param {string} why the reason the URI cannot be checked.
* @returns {never}
*/
const refuse = (why) => {
throw accessDenied(
'FileSystemRead',
uri,
`Cannot check ${why} against the permission model; this package refuses file: URIs it cannot parse rather than opening them unchecked.`,
);
};
if (!/^file:/i.test(uri)) refuse('a URI without a file: scheme');
let rest = uri.slice(5);
if (rest.startsWith('//')) {
const end = rest.search(/[/?]/);
const authority = end === -1 ? rest.slice(2) : rest.slice(2, end);
if (end !== -1) rest = rest.slice(end);
if (authority && authority.toLowerCase() !== 'localhost') {
refuse(`the non-local URI authority '${authority}'`);
}
}
let query = '';
const q = rest.indexOf('?');
if (q !== -1) {
query = rest.slice(q + 1);
rest = rest.slice(0, q);
}
let target = '';
try {
target = decodeURIComponent(rest);
} catch {
refuse('a URI path with invalid percent-escapes');
}
if (target === '') refuse('a URI with an empty path');
const params = new URLSearchParams(query);
const mode = params.get('mode');
if (target === ':memory:' || mode === 'memory') {
return { memory: true };
}
if (mode !== null && mode !== 'ro' && mode !== 'rw' && mode !== 'rwc') {
refuse(`the URI mode parameter '${mode}'`);
}
return {
memory: false,
path: target,
readonly: mode === 'ro' || params.get('immutable') === '1',
};
}
/**
* Requires fs.read permission for one path under the permission model.
*
* @param {string} abs the absolute path to check.
* @param {string} what the operation being checked, for the message.
* @returns {void}
* @throws {Error} ERR_ACCESS_DENIED naming the path and the remedy.
* @private
*/
function requireReadPermission(abs, what) {
if (process.permission?.has('fs.read', abs)) return;
throw accessDenied(
'FileSystemRead',
abs,
`${what} requires reading ${abs}.`,
);
}
/**
* Requires fs.write permission for one path. A directory granted either
* exactly (`--allow-fs-write=/data`) or by wildcard (`--allow-fs-write=/data/*`)
* satisfies the check; an exact-file grant does not extend to a directory.
*
* @param {string} abs the absolute path to check.
* @param {string} what the operation being checked, for the message.
* @returns {void}
* @throws {Error} ERR_ACCESS_DENIED naming the path and the remedy.
* @private
*/
function requireWritePermission(abs, what) {
const permission = process.permission;
if (typeof permission?.has !== 'function') return;
// Called on the object, not through a detached reference: whether
// `has` happens to ignore its receiver is an implementation detail,
// and this is a security check.
if (
permission.has('fs.write', abs) ||
permission.has('fs.write', path.join(abs, '*'))
) {
return;
}
throw accessDenied(
'FileSystemWrite',
abs,
`${what} requires writing ${abs}.`,
);
}
/**
* Checks one database-file open (the flags a writable open needs on the
* containing directory are why the directory is checked too: SQLite
* creates the -journal/-wal/-shm sidecar files beside the database).
*
* @param {string} abs the absolute database path.
* @param {string} who 'Opening' or the backup role, for messages.
* @returns {void}
* @throws {Error} ERR_ACCESS_DENIED naming what failed.
* @private
*/
function checkDatabaseFileOpen(abs, who) {
requireReadPermission(abs, `${who} ${abs}`);
requireWritePermission(abs, `${who} ${abs}`);
const dir = path.dirname(abs);
if (
!process.permission?.has('fs.write', dir) &&
!process.permission?.has('fs.write', path.join(dir, '*'))
) {
throw accessDenied(
'FileSystemWrite',
dir,
`${who} ${abs} also requires writing the directory ${dir}: a writable SQLite database creates its -journal, -wal and -shm files beside it. Grant the directory with the wildcard form (--allow-fs-write="${dir}${path.sep}*"), which covers the database file and its sidecar files.`,
);
}
}
/**
* Enforces the permission model for one `Database` open. Every open path
* (the constructor, `sqlite3.open`, the cached registry, pool workers)
* goes through the `Database` wrapper below, which calls this before the
* native open is scheduled. No-op — one property read — when the model is
* off.
*
* @param {string} filename the filename as passed.
* @param {number | undefined} mode the open mode as passed (undefined is
* the native default: read-write create).
* @returns {void}
* @throws {Error} ERR_ACCESS_DENIED naming the path, the scope and a
* working remedy.
* @private
*/
function assertOpenPermitted(filename, mode) {
if (!permissionModelActive()) return;
if (typeof filename !== 'string') return; // the native TypeError is better
if (filename === ':memory:') return;
const effective =
typeof mode === 'number' && Number.isInteger(mode)
? mode
: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE;
/** @type {string} */
let target = filename;
let writable = (effective & sqlite3.OPEN_READONLY) === 0;
if (effective & sqlite3.OPEN_URI) {
const parsed = parseSqliteUri(filename);
if (parsed.memory) return;
target = parsed.path;
if (parsed.readonly) writable = false;
}
if (target === '') {
// '' is SQLite's private temporary database: a real file under the
// temp directory, created and written by SQLite itself.
requireWritePermission(
os.tmpdir(),
"Opening '' (SQLite creates a private temporary database under the temp directory)",
);
return;
}
const abs = path.resolve(target);
if (writable) {
checkDatabaseFileOpen(abs, 'Opening');
} else {
requireReadPermission(abs, `Opening ${abs} read-only`);
}
}
// --- Extension loading policy (Deliverable 11 §2.2) ------------------------
//
// loadExtension loads and executes an arbitrary shared library — the same
// class of operation --allow-addons gates. Under the permission model it
// is refused unless explicitly allowlisted; a `{ deny: true }` policy
// disables it permanently on the connection. The policy is JS-layer state
// (the native entry point is refused before anything is scheduled), and
// the SQL load_extension() function is unreachable: it is off by default
// in the vendored SQLite (probed — see the note in src/database.cc) and
// loadExtension re-disables the C-API gate after every call.
/**
* @typedef {object} ExtensionPolicy
* @property {boolean} untrusted the connection was opened `{ untrusted: true }`.
* @property {boolean} permadeny `configure('extensionPolicy', { deny: true })` was applied.
* @property {boolean} configured an explicit policy was applied; its allowlist then governs
* even when the permission model is off.
* @property {Set<string>} allow allowed extension paths (as written).
* @private
*/
/** @type {WeakMap<object, ExtensionPolicy>} */
const extensionPolicies = new WeakMap();
/**
* Reads (creating on first use) a connection's extension policy.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {ExtensionPolicy} the policy record.
* @private
*/
function extensionPolicyFor(db) {
let policy = extensionPolicies.get(db);
if (policy === undefined) {
policy = {
untrusted: false,
permadeny: false,
configured: false,
allow: new Set(),
};
extensionPolicies.set(db, policy);
}
return policy;
}
/**
* Applies a `configure('extensionPolicy', …)` request.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {unknown} spec `{ allow: [...] }` or `{ deny: true }`.
* @returns {void}
* @throws {TypeError} when the policy is malformed or the connection is
* hardened past extension loading.
* @private
*/
function applyExtensionPolicy(db, spec) {
const policy = extensionPolicyFor(db);
if (policy.permadeny) {
throw new TypeError(
"loadExtension is permanently disabled on this connection: an earlier configure('extensionPolicy', { deny: true }) cannot be reversed",
);
}
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
throw new TypeError(
"configure('extensionPolicy') requires an options object",
);
}
const known = new Set(['allow', 'deny']);
for (const key of Object.keys(spec)) {
if (!known.has(key)) {
throw new TypeError(
`extensionPolicy received unknown option '${key}'`,
);
}
}
const deny = /** @type {Record<string, unknown>} */ (spec).deny;
if (deny !== undefined && typeof deny !== 'boolean') {
throw new TypeError("extensionPolicy option 'deny' must be a boolean");
}
if (deny === true) {
policy.permadeny = true;
policy.configured = true;
policy.allow.clear();
return;
}
const allow = /** @type {Record<string, unknown>} */ (spec).allow;
if (allow === undefined) {
throw new TypeError(
"extensionPolicy requires 'allow' (an array of paths) or 'deny: true'",
);
}
if (!Array.isArray(allow)) {
throw new TypeError(
"extensionPolicy option 'allow' must be an array of paths",
);
}
const entries = allow.map((entry, i) => {
if (typeof entry !== 'string' || entry.length === 0) {
throw new TypeError(
`extensionPolicy allow[${i}] must be a non-empty string path`,
);
}
return entry;
});
if (permissionModelActive()) {
// Loading a shared library reads (and executes) it: every allowed
// path must at least be readable under the permission model,
// checked here — at declare time — because this is the only point
// where JavaScript runs.
for (const entry of entries) {
requireReadPermission(
path.resolve(entry),
`Allowing the extension ${entry} for loadExtension`,
);
}
}
policy.allow = new Set(entries);
policy.configured = true;
}
/**
* Refuses or admits one loadExtension call under the active policies.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {unknown} filename the extension path as passed.
* @returns {void}
* @throws {Error} ERR_ACCESS_DENIED or TypeError when the call is refused.
* @private
*/
function assertExtensionAllowed(db, filename) {
const policy = extensionPolicyFor(db);
if (policy.permadeny) {
throw new Error(
'loadExtension is disabled on this connection by its extension policy ({ deny: true } was applied, or it was opened { untrusted: true })',
);
}
// An explicit allowlist governs in both modes; with no policy at all,
// only the permission model refuses (the pre-v9 behaviour is kept
// when the model is off).
if (!policy.configured && !permissionModelActive()) return;
if (typeof filename !== 'string') return; // the native TypeError is better
const matches =
policy.allow.has(filename) || policy.allow.has(path.resolve(filename));
if (matches) return;
if (!permissionModelActive()) {
throw new Error(
`loadExtension is refused: the extension policy configured on this connection permits only its allowlisted paths. Add ${JSON.stringify(filename)} with db.configure('extensionPolicy', { allow: [...] }).`,
);
}
throw accessDenied(
'FileSystemRead',
filename,
`Loading the extension ${filename} executes native code, which the Node permission model gates: loading shared libraries is what --allow-addons governs. To load this extension, declare it explicitly with db.configure('extensionPolicy', { allow: [${JSON.stringify(path.resolve(filename))}] }) and grant its path fs.read.`,
);
}
// --- ATTACH gate wiring (Deliverable 11 §2.1) -------------------------------
//
// `ATTACH DATABASE '...' AS x` and `VACUUM INTO '...'` (which SQLite
// implements through an internal ATTACH) reach open(2) from SQL, where no
// JS check can run. The native `_setAttachGate` arms an authorizer
// pre-filter that denies SQLITE_ATTACH unless the target matches an
// allowlist; the allowlist is permission-checked here, at declare time,
// for the same reason as the extension policy. When the permission model
// is active every connection gets an empty (deny-all) gate at open.
/**
* Applies a `configure('attachPaths', …)` request.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {unknown} value an array of allowed target paths, or null to
* disarm the gate.
* @returns {void}
* @throws {TypeError | Error} when the list is malformed, a path is not
* permitted, or the connection is untrusted.
* @private
*/
function applyAttachPaths(db, value) {
const policy = extensionPolicyFor(db);
if (policy.untrusted) {
throw new TypeError(
'untrusted connections cannot allow ATTACH: they were opened with the deny-all gate as part of their hardening',
);
}
if (value === null || value === undefined) {
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._setAttachGate)
).call(db, false, []);
return;
}
if (!Array.isArray(value)) {
throw new TypeError(
"configure('attachPaths') requires an array of paths, or null to disarm the gate",
);
}
const entries = value.map((entry, i) => {
if (typeof entry !== 'string' || entry.length === 0) {
throw new TypeError(
`attachPaths[${i}] must be a non-empty string path`,
);
}
return entry;
});
if (permissionModelActive()) {
for (const entry of entries) {
// ATTACH opens its target read-write-create by default, so the
// checks match a writable open of the same path. A read-only
// URI (file:...?mode=ro / immutable=1) needs only fs.read.
if (/^file:/i.test(entry)) {
const parsed = parseSqliteUri(entry);
if (parsed.memory) continue;
requireReadPermission(
path.resolve(parsed.path),
`Allowing ATTACH of ${entry}`,
);
if (!parsed.readonly) {
checkDatabaseFileOpen(
path.resolve(parsed.path),
`Allowing ATTACH of ${entry}`,
);
}
} else {
checkDatabaseFileOpen(
path.resolve(entry),
`Allowing ATTACH of ${entry}`,
);
}
}
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._setAttachGate)
).call(db, true, entries);
}
// --- Untrusted database files (Deliverable 11 §2.3) -------------------------
// One option flag standing in for a page of SQLite hardening lore. The
// values are deliberately conservative and documented in
// docs/security.md; they are applied as queued configuration before any
// user work can run (the open is FIFO-ahead of them).
// Capacity of the implicit statement cache the synchronous paths keep when
// the caller has not opted into cacheStatements(). Same default as that
// one; see Database#_statementForSync.
const SYNC_STMT_CACHE_MAX = 64;
/** @type {Array<[number, number]>} */
const UNTRUSTED_LIMITS = [
// LIMIT_LENGTH: one string, BLOB, table or row budget (SQLite default
// 1 GiB). 64 MiB bounds a hostile record without clipping real ones.
[sqlite3.LIMIT_LENGTH, 64 * 1024 * 1024],
// LIMIT_SQL_LENGTH: largest compiled statement (default 1 GiB).
[sqlite3.LIMIT_SQL_LENGTH, 1024 * 1024],
// LIMIT_EXPR_DEPTH: parser recursion per expression (default 1000).
[sqlite3.LIMIT_EXPR_DEPTH, 100],
// LIMIT_VDBE_OP: opcodes per prepared statement (default 250M).
[sqlite3.LIMIT_VDBE_OP, 25000],
// LIMIT_ATTACHED: no ATTACH at all, behind the deny-all gate.
[sqlite3.LIMIT_ATTACHED, 0],
];
/**
* Applies the untrusted-file hardening to a freshly constructed
* connection. Runs immediately after `super()` in the wrapper: every call
* below schedules onto the connection queue behind the still-pending
* open, so the hardening is in place before any user work runs, with no
* window in between (the queue is FIFO and the open has not completed).
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {void}
* @private
*/
function applyUntrustedHardening(db) {
const policy = extensionPolicyFor(db);
policy.untrusted = true;
policy.permadeny = true;
// Defensive mode + distrust the schema + writable_schema off: the
// three switches that stop a hostile file's schema (views, triggers,
// CHECK constraints) from invoking dangerous built-ins or from
// rewriting sqlite_schema. _dbConfig is the native core; the JS
// dbConfig() wrapper is dual-mode and would return promises here.
const dbConfig = /** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._dbConfig)
);
/**
* The hardening verbs cannot realistically fail, but a failure must
* surface somewhere: route it to the connection's 'error' event
* rather than letting it vanish into a fire-and-forget call.
*
* @param {import('./native.js').SqliteError | null} err
*/
const onHardeningError = (err) => {
if (err) db.emit('error', err);
};
dbConfig.call(db, sqlite3.DBCONFIG_DEFENSIVE, 1, onHardeningError);
dbConfig.call(db, sqlite3.DBCONFIG_TRUSTED_SCHEMA, 0, onHardeningError);
dbConfig.call(db, sqlite3.DBCONFIG_WRITABLE_SCHEMA, 0, onHardeningError);
// Resource ceilings and the deny-ATTACH authorizer gate.
for (const [id, value] of UNTRUSTED_LIMITS) {
db.configure('limit', id, value);
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._setAttachGate)
).call(db, true, []);
}
// --- The Database wrapper ----------------------------------------------------
//
// Every connection goes through this wrapper (the namespace rebind below
// points sqlite3.Database, sqlite3.open, the cached registry and the pool
// workers at it). It runs the permission-model checks before the native
// open is scheduled, accepts the v9 open options, and applies the
// untrusted hardening. Instances are indistinguishable from native ones:
// instanceof holds in both directions and every prototype method — the
// ones below and the promise layer — applies unchanged.
/**
* Options for opening a database (v9). Accepted anywhere a mode number
* could appear in the `Database` constructor and in `sqlite3.open`'s
* second argument.
*
* @typedef {object} OpenOptions
* @property {number} [mode] open flags, e.g. `sqlite3.OPEN_READWRITE`.
* @property {boolean} [untrusted] harden the connection for an
* attacker-supplied database file: defensive mode, untrusted schema,
* writable_schema off, extension loading permanently disabled,
* conservative run-time limits and a deny-all ATTACH gate. See
* docs/security.md#untrusted-database-files.
* @since 9.0.0
*/
// Captured before the wrapper patches anything: the native halves the
// wrappers below delegate to.
const nativeConfigure = /** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (NativeDatabase.prototype.configure)
);
const nativeLoadExtension = /** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (NativeDatabase.prototype.loadExtension)
);
/**
* True for a v9 open-options object (every own key is one of the known
* option keys), used to pick the options argument out of the constructor's
* legacy positional shapes.
*
* @param {unknown} value the candidate.
* @returns {boolean} whether it is an options object.
* @private
*/
function isOpenOptions(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value);
return (
keys.length > 0 &&
keys.every(
(key) =>
key === 'mode' ||
key === 'untrusted' ||
// Internal: the node:sqlite compat shim's synchronous open
// (lib/compat.js); not part of the public surface.
key === 'syncOpen',
)
);
}
/**
* A connection to a SQLite database — the v9 wrapper around the native
* class. Adds the permission-model checks on every open path, the
* {@link OpenOptions} forms, the `extensionPolicy`/`attachPaths`
* configure options and the guarded `loadExtension`/`backup`; everything
* else, including all pre-v9 positional constructor forms, behaves
* exactly as before.
*
* @since 9.0.0
*/
class DatabaseClass extends NativeDatabase {
/**
* Opens a database connection. The open itself is asynchronous; the
* callback fires (or the `'open'` event emits) once it completes.
*
* Under Node's permission model (`--permission`), the target is
* checked against the process's fs allowances before anything is
* opened: a read-only open needs `fs.read` for the file; a writable
* open additionally needs `fs.write` for the file **and its
* directory** (SQLite writes `-journal`/`-wal`/`-shm` files beside
* it). A refusal names the path and the flag that permits it.
*
* @param {string} filename path to the database file, `:memory:`, `''`
* or (with `OPEN_URI`) a `file:` URI.
* @param {number | OpenOptions | ((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void)} [a]
* open flags, an options object, or the callback.
* @param {((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void) | OpenOptions} [b]
* the callback (after a mode), or the options object.
* @throws {Error} ERR_ACCESS_DENIED under the permission model when
* the target is not permitted, naming the path and the remedy.
* @throws {TypeError} when the arguments are malformed.
*/
constructor(filename, a, b) {
/** @type {number | undefined} */
let mode;
/** @type {((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void) | undefined} */
let callback;
let untrusted = false;
if (typeof a === 'number' && Number.isInteger(a)) {
mode = a;
} else if (typeof a === 'function') {
callback = a;
} else if (isOpenOptions(a)) {
const opts = /** @type {OpenOptions} */ (a);
if (opts.mode !== undefined) {
if (typeof opts.mode !== 'number') {
throw new TypeError(
"open option 'mode' must be a number (an OPEN_* flag set)",
);
}
mode = opts.mode;
}
if (opts.untrusted !== undefined) {
if (typeof opts.untrusted !== 'boolean') {
throw new TypeError(
"open option 'untrusted' must be a boolean",
);
}
untrusted = opts.untrusted;
}
} else if (a !== undefined && a !== null) {
throw new TypeError(
'Database expects a mode number, an options object or a callback as its second argument',
);
}
let syncOpen = false;
if (
isOpenOptions(a) &&
/** @type {{ syncOpen?: unknown }} */ (/** @type {unknown} */ (a))
.syncOpen !== undefined
) {
const flag = /** @type {{ syncOpen?: unknown }} */ (
/** @type {unknown} */ (a)
).syncOpen;
if (typeof flag !== 'boolean') {
throw new TypeError("open option 'syncOpen' must be a boolean");
}
syncOpen = flag;
}
if (b !== undefined && b !== null) {
if (typeof b === 'function') {
callback = b;
} else if (isOpenOptions(b)) {
const opts =
/** @type {OpenOptions & { syncOpen?: boolean }} */ (b);
if (opts.mode !== undefined && mode === undefined) {
mode = opts.mode;
}
if (opts.untrusted === true) untrusted = true;
if (opts.syncOpen === true) syncOpen = true;
}
}
assertOpenPermitted(filename, mode);
super(
filename,
...(mode !== undefined ? [mode] : []),
...(callback !== undefined ? [callback] : []),
...(syncOpen ? [true] : []),
);
// diagnostics_channel (Phase 6): tracked so a subscriber arriving
// after the connection was opened can arm query-span publication
// on it; armed eagerly here when one is already listening.
trackConnection(this);
if (diagnosticsChannelSubscribers.size > 0) {
armDiagnosticsFor(this);
}
if (untrusted) {
applyUntrustedHardening(this);
} else if (permissionModelActive()) {
// The ATTACH gate closes the SQL-level path to the filesystem
// (ATTACH and VACUUM INTO); the deny-all default is opened up
// only through configure('attachPaths', ...).
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._setAttachGate)
).call(this, true, []);
}
}
}
// The namespace binding is a Proxy around the class for one reason: a
// native node-addon-api class and a JavaScript class word their
// call-without-new TypeError differently ("Class constructors cannot be
// invoked…" vs "Class constructor Database cannot be invoked…"), and the
// exact pre-v9 message is pinned by tests and matched by user code. The
// named export is the class itself; `import { Database }` callers get the
// plain subclass.
sqlite3.Database = /** @type {sqlite3['Database']} */ (
/** @type {unknown} */ (
new Proxy(DatabaseClass, {
/**
* Reproduces the native class's exact call-without-new
* TypeError.
*
* @returns {never}
*/
apply() {
throw new TypeError(
"Class constructors cannot be invoked without 'new'",
);
},
})
)
);
// The rest of this file (and the promise layer) patches the class
// prototype; this alias keeps those assignments unchanged.
const Database = DatabaseClass;
/**
* Configures the connection: the pre-v9 native options plus the v9
* security policies.
*
* - `configure('extensionPolicy', { allow: [...] } | { deny: true })` —
* see {@link Database#loadExtension}.
* - `configure('attachPaths', [...] | null)` — the ATTACH-gate allowlist
* (or null to disarm a manually-armed gate).
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} option the configuration option.
* @param {...unknown} rest the option's arguments.
* @returns {any} this database, for chaining.
*/
Database.prototype.configure = function (option, ...rest) {
if (option === 'extensionPolicy') {
applyExtensionPolicy(this, rest[0]);
return this;
}
if (option === 'attachPaths') {
applyAttachPaths(this, rest[0]);
return this;
}
return /** @type {any} */ (nativeConfigure.call(this, option, ...rest));
};
/**
* Loads a SQLite extension — arbitrary native code in a shared library,
* gated by policy.
*
* Under Node's permission model every load is refused unless the exact
* path was declared with `configure('extensionPolicy', { allow: [...] })`
* and is fs.read-permitted. `configure('extensionPolicy', { deny: true })`
* disables loading permanently for the connection. On untrusted
* connections (`{ untrusted: true }`) loading is permanently disabled from
* the start.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} filename the extension file.
* @param {...unknown} rest optionally a callback.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core).
* @throws {Error} when the policy refuses the load, naming the path and
* the remedy.
*/
Database.prototype.loadExtension = function (filename, ...rest) {
assertExtensionAllowed(this, filename);
return /** @type {any} */ (
nativeLoadExtension.call(this, filename, ...rest)
);
};
/**
* Creates a backup. The filename side (destination in the short form,
* source when `filenameIsDest` is false) is opened by the native Backup
* layer directly, so under the permission model it is checked like any
* other open — read and write on the file, write on its directory.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {...unknown} args filename and optional callback, or the full
* filename/source/dest/direction/callback form.
* @returns {import('./sqlite3-binding.js').Backup} the created backup.
* @throws {Error} ERR_ACCESS_DENIED under the permission model when the
* filename side is not permitted.
*/
// Database#backup(filename, [callback])
// Database#backup(filename, destName, sourceName, filenameIsDest, [callback])
Database.prototype.backup = function (...args) {
if (permissionModelActive() && typeof args[0] === 'string') {
const filenameIsDest =
args.length <= 2 ||
args[3] === undefined ||
/** @type {boolean} */ (args[3]);
const who = filenameIsDest ? 'Backing up into' : 'Backing up from';
checkDatabaseFileOpen(path.resolve(args[0]), who);
}
/** @type {import('./sqlite3-binding.js').Backup} */
let backup;
if (args.length <= 2) {
backup = new Backup(
this,
/** @type {string} */ (args[0]),
'main',
'main',
true,
/** @type {((err: Error | null) => void) | undefined} */ (args[1]),
);
} else {
backup = new Backup(
this,
/** @type {string} */ (args[0]),
/** @type {string} */ (args[1]),
/** @type {string} */ (args[2]),
/** @type {boolean} */ (args[3]),
/** @type {((err: Error | null) => void) | undefined} */ (args[4]),
);
}
// Per the sqlite docs, exclude the following errors as non-fatal by default.
backup.retryErrors = [sqlite3.BUSY, sqlite3.LOCKED];
return backup;
};
/**
* Pops a trailing error-first callback off `args`, wrapped so it is
* only invoked for a truthy error — the `err === null` success call is
* the caller's business, not the extractErrBack user's.
*
* @param {unknown[]} args the call arguments.
* @returns {((err: import('./native.js').SqliteError | null) => void) | undefined} the wrapped callback, or undefined.
* @private
*/
function extractErrBack(args) {
const last = args[args.length - 1];
if (args.length > 0 && typeof last === 'function') {
const callback =
/** @type {(err: import('./native.js').SqliteError) => void} */ (
last
);
/**
* @param {import('./native.js').SqliteError | null} err
*/
function rethrow(err) {
if (err) callback(err);
}
return rethrow;
}
return undefined;
}
/**
* Splits a call's trailing callback across the two native slots that can
* both observe one failed prepare: the statement's error-only prepare
* errback and the queued statement method's completion callback (which
* also carries the call's results). When the prepare fails, both slots
* fire with the same error; the shared token here keeps the user's
* callback to a single invocation, whatever route delivered first.
* Success calls flow through untouched.
*
* Mutates `args`, replacing the trailing callback with the guarded
* completion wrapper.
*
* @param {unknown[]} args the call arguments, ending in a callback.
* @returns {{
* errback: (this: import('./sqlite3-binding.js').Statement, err: import('./native.js').SqliteError | null) => void,
* completion: (this: import('./sqlite3-binding.js').Statement, ...call: unknown[]) => void,
* } | null} the two slot handlers, or null with no trailing callback.
* @private
*/
function splitPrepareCallback(args) {
const errBack = extractErrBack(args);
if (errBack === undefined) return null;
const userCallback = /** @type {(...call: unknown[]) => void} */ (
args[args.length - 1]
);
let delivered = false;
/**
* @param {import('./native.js').SqliteError | null} err
* @this {import('./sqlite3-binding.js').Statement}
*/
const errback = function (err) {
if (!err || delivered) return;
delivered = true;
errBack.call(this, err);
};
/**
* @param {...unknown} call
* @this {import('./sqlite3-binding.js').Statement}
*/
const completion = function (...call) {
if (call[0] && delivered) return;
if (call[0]) delivered = true;
userCallback.apply(this, call);
};
args[args.length - 1] = completion;
return { errback, completion };
}
// Captured before the promise API wraps Statement#bind: prepare()'s bind
// path must keep its synchronous statement return, and a dual-mode bind
// would hand back a promise instead.
/** @type {(...args: any[]) => any} */
const nativeStatementBind = Statement.prototype.bind;
// Internal fire-and-forget finalizes must not allocate a promise per call:
// they sit on the hot path of every uncached run/get/all/each/map.
/** @type {(...args: any[]) => any} */
const nativeStatementFinalize = Statement.prototype.finalize;
// --- Sessions, changesets, serialization and blob I/O (Deliverable 08) -----
//
// The native halves run through the same queues as statement work; these
// wrappers add option parsing and the promise layer (lib/promises.js)
// rewraps the cores for dual-mode use.
// sqlite3_deserialize flags, composed by deserializeFromBytes().
const DESERIALIZE_RESIZEABLE = 2;
const DESERIALIZE_READONLY = 4;
/**
* Creates a session that records changes made through this connection.
*
* The returned session records every INSERT, UPDATE and DELETE on the
* attached tables (only tables with a primary key are recordable); call
* {@link Session#changeset} to harvest the recorded changes as a
* `Uint8Array`, and `close()` when done. A session left open is closed
* by `db.close()`.
*
* A connection has a single preupdate hook, shared between sessions and
* the `'preupdate'` event: creating a session while a `'preupdate'`
* listener is registered throws, and registering the listener while a
* session is open fails the registration — one would silently stop the
* other.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./native.js').SessionOptions | ((err: import('./native.js').SqliteError | null) => void)} [options]
* the options object, or the ready callback.
* @param {(this: import('./sqlite3-binding.js').Session, err: import('./native.js').SqliteError | null) => void} [callback]
* called once the session is recording (create errors surface here or
* on the session's `'error'` event).
* @returns {import('./sqlite3-binding.js').Session} the session (it starts recording asynchronously).
* @throws {TypeError} when the options are malformed or a 'preupdate'
* listener is registered on this connection.
* @since 9.0.0
* @example
* const session = db.session({ table: 'users' });
* await db.run('UPDATE users SET name = ? WHERE id = ?', 'x', 1);
* const changeset = await session.changeset();
* session.close();
*/
Database.prototype.session = function (options, callback) {
/** @type {string} */
let dbName = 'main';
/** @type {string} */
let table = '';
let indirect = false;
if (typeof options === 'function') {
callback = options;
} else if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('session() options must be an object');
}
const known = new Set(['db', 'table', 'indirect']);
for (const key of Object.keys(options)) {
if (!known.has(key)) {
throw new TypeError(
`session() received unknown option '${key}'`,
);
}
}
if (options.db !== undefined) {
if (typeof options.db !== 'string') {
throw new TypeError("session() option 'db' must be a string");
}
dbName = options.db;
}
if (options.table !== undefined) {
if (typeof options.table !== 'string') {
throw new TypeError(
"session() option 'table' must be a string",
);
}
table = options.table;
}
if (options.indirect !== undefined) {
if (typeof options.indirect !== 'boolean') {
throw new TypeError(
"session() option 'indirect' must be a boolean",
);
}
indirect = options.indirect;
}
}
return new Session(this, dbName, table, indirect, callback);
};
/**
* Applies a changeset (or patchset) to this connection inside one
* savepoint: either every change lands or the apply is rolled back.
*
* `options.conflict` decides what happens when a change cannot be
* applied cleanly: `'abort'` (the default) rolls the whole apply back,
* `'omit'` skips the conflicting change, `'replace'` overwrites the
* conflicting row (legal for `'data'` and `'conflict'` conflicts only).
* A function is the fully general form — it receives the conflict
* description and returns one of those decisions; it runs as a blocking
* round trip from the applying thread, so it must not use the
* synchronous methods on this connection. `options.filter` receives
* each affected table name and returns false to skip it.
*
* In callback mode returns this database; the promise layer rewraps the
* core.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {Uint8Array | ArrayBuffer | DataView} changeset the changeset bytes.
* @param {import('./native.js').ApplyChangesetOptions | ((this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void)} [options]
* the conflict policy and optional table filter, or the callback.
* @param {(this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null) => void} [callback]
* called once the apply completed or rolled back.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core).
* @throws {TypeError} when the bytes or options are malformed.
* @since 9.0.0
* @example
* await db.applyChangeset(changeset, { conflict: 'replace' });
*/
Database.prototype.applyChangeset = function (changeset, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
/** @type {number | undefined} */
let decision;
/** @type {import('./native.js').ApplyChangesetOptions['conflict']} */
let onConflict;
/** @type {((table: string) => boolean) | null | undefined} */
let onFilter;
let wantRebase = false;
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('applyChangeset() options must be an object');
}
const known = new Set(['conflict', 'onConflict', 'filter', 'rebase']);
for (const key of Object.keys(options)) {
if (!known.has(key)) {
throw new TypeError(
`applyChangeset() received unknown option '${key}'`,
);
}
}
onConflict = options.conflict ?? options.onConflict;
onFilter = options.filter;
if (
options.rebase !== undefined &&
typeof options.rebase !== 'boolean'
) {
throw new TypeError(
"applyChangeset() option 'rebase' must be a boolean",
);
}
wantRebase = options.rebase === true;
}
if (onConflict === undefined || onConflict === null) {
decision = sqlite3.CHANGESET_ABORT;
} else if (typeof onConflict === 'function') {
decision = sqlite3.CHANGESET_ABORT;
} else if (onConflict === 'abort') {
decision = sqlite3.CHANGESET_ABORT;
} else if (onConflict === 'omit') {
decision = sqlite3.CHANGESET_OMIT;
} else if (onConflict === 'replace') {
decision = sqlite3.CHANGESET_REPLACE;
} else {
throw new TypeError(
"applyChangeset() conflict must be 'abort', 'omit', 'replace' or a function",
);
}
if (
onFilter !== undefined &&
onFilter !== null &&
typeof onFilter !== 'function'
) {
throw new TypeError('applyChangeset() filter must be a function');
}
// A string policy travels in `decision`; only a function is a
// handler (a bare string must not reach the native handler slot).
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._applyChangeset)
)(
changeset,
decision,
typeof onConflict === 'function' ? onConflict : null,
typeof onFilter === 'function' ? onFilter : null,
callback,
wantRebase,
);
return this;
};
/**
* Serializes the whole database (or one attached schema) to a
* `Uint8Array` — an in-memory snapshot of the exact bytes a file copy
* would contain. Exclusive on the connection: it waits for in-flight
* work so the snapshot cannot interleave with writes. Feed the result to
* {@link sqlite3.deserializeFromBytes}.
*
* The snapshot carries every committed transaction: serialization reads
* each page through the pager, and the pager reads through the WAL, so
* frames not yet checkpointed are included. The returned bytes are
* rewritten to rollback-journal format — a WAL-format image would demand
* WAL recovery that a deserialized copy cannot perform (it has no `-wal`
* file) — so the output is always valid input to
* `deserializeFromBytes()`. The live database's journal mode is
* untouched.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string | ((err: import('./native.js').SqliteError | null, bytes: Uint8Array) => void)} [dbName]
* the attached database name (default `'main'`), or the callback.
* @param {(this: import('./sqlite3-binding.js').Database, err: import('./native.js').SqliteError | null, bytes: Uint8Array) => void} [callback]
* receives the bytes.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core to resolve the bytes).
* @since 9.0.0
* @example
* const bytes = await db.serializeToBytes();
*/
Database.prototype.serializeToBytes = function (dbName, callback) {
if (typeof dbName === 'function') {
callback = dbName;
dbName = 'main';
}
if (dbName !== undefined && dbName !== null && typeof dbName !== 'string') {
throw new TypeError(
'serializeToBytes() database name must be a string',
);
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._serializeToBytes)
)(dbName ?? 'main', callback);
return this;
};
/**
* Opens an incremental blob handle for streaming reads and writes of one
* row's blob column ({@link Blob#read}, {@link Blob#write},
* `blob.createReadStream()`), instead of materialising the whole value
* as one `Buffer`. Any write to the row invalidates open handles
* (`SQLITE_ABORT`); `blob.reopen(rowid)` re-aims a handle after that.
*
* The handle is closed by `blob.close()` or by `db.close()`.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./native.js').OpenBlobOptions} options what to open.
* @param {(this: import('./sqlite3-binding.js').Blob, err: import('./native.js').SqliteError | null) => void} [callback]
* called once the handle is open (open errors surface here or on the
* blob's `'error'` event).
* @returns {import('./sqlite3-binding.js').Blob} the blob handle (it opens asynchronously).
* @throws {TypeError} when the options are malformed.
* @since 9.0.0
* @example
* const blob = await db.openBlob({ table: 'files', column: 'data', rowid: 1 });
* const chunk = new Uint8Array(65536);
* const n = await blob.read(chunk, 0);
* await blob.close();
*/
Database.prototype.openBlob = function (options, callback) {
if (
options === null ||
typeof options !== 'object' ||
Array.isArray(options)
) {
throw new TypeError('openBlob() requires an options object');
}
const { table, column, rowid } = options;
if (typeof table !== 'string' || table.length === 0) {
throw new TypeError(
"openBlob() option 'table' must be a non-empty string",
);
}
if (typeof column !== 'string' || column.length === 0) {
throw new TypeError(
"openBlob() option 'column' must be a non-empty string",
);
}
if (typeof rowid !== 'number' || !Number.isInteger(rowid)) {
throw new TypeError("openBlob() option 'rowid' must be an integer");
}
const db = options.db ?? 'main';
if (typeof db !== 'string') {
throw new TypeError("openBlob() option 'db' must be a string");
}
const readOnly = options.readOnly ?? false;
if (typeof readOnly !== 'boolean') {
throw new TypeError("openBlob() option 'readOnly' must be a boolean");
}
return new Blob(this, db, table, column, rowid, readOnly, callback);
};
/**
* Builds a database from serialized bytes (from
* {@link Database#serializeToBytes} or a database file read into memory):
* opens a fresh in-memory connection and installs the bytes as its
* `main` schema.
*
* The bytes are **copied** into SQLite-owned memory — a `Uint8Array`'s
* backing store cannot be handed to SQLite directly without a
* use-after-free risk — so a large snapshot costs the copy's time and
* memory once. Corrupt input rejects with `SQLITE_NOTADB`.
*
* @param {Uint8Array | ArrayBuffer | DataView} bytes the serialized database.
* @param {import('./native.js').DeserializeOptions} [options] `readOnly`
* makes the result read-only; `resizable` lets it grow on write.
* @returns {Promise<import('./sqlite3-binding.js').Database>} the opened database.
* @throws {TypeError} when the bytes or options are malformed; rejects
* with `SQLITE_NOTADB` on corrupt input.
* @since 9.0.0
* @example
* const db = await sqlite3.deserializeFromBytes(bytes, { resizable: true });
*/
sqlite3.deserializeFromBytes = async function deserializeFromBytes(
bytes,
options,
) {
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(
'deserializeFromBytes() options must be an object',
);
}
const known = new Set(['readOnly', 'resizable']);
for (const key of Object.keys(options)) {
if (!known.has(key)) {
throw new TypeError(
`deserializeFromBytes() received unknown option '${key}'`,
);
}
}
for (const key of known) {
const value = /** @type {Record<string, unknown>} */ (options)[key];
if (value !== undefined && typeof value !== 'boolean') {
throw new TypeError(
`deserializeFromBytes() option '${key}' must be a boolean`,
);
}
}
}
const flags =
(options?.readOnly === true ? DESERIALIZE_READONLY : 0) |
(options?.resizable === true ? DESERIALIZE_RESIZEABLE : 0);
return new Promise((resolve, reject) => {
try {
// The callback sits in the mode slot at runtime (the
// constructor skips a non-number there); the cast keeps the
// variadic constructor shape honest for the type checker,
// the same trick sqlite3.open uses.
const OpenCtor =
/** @type {new (filename: string, ...rest: unknown[]) => import('./sqlite3-binding.js').Database} */ (
/** @type {unknown} */ (Database)
);
/**
* @param {Error | null} openErr
*/
const onOpen = (openErr) => {
if (openErr) {
reject(openErr);
return;
}
/**
* @param {import('./native.js').SqliteError | null} err
*/
const onDeserialized = (err) => {
if (err) {
db.close(() => reject(err));
return;
}
resolve(db);
};
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._deserialize)
)(bytes, flags, onDeserialized);
};
const db = new OpenCtor(':memory:', onOpen);
} catch (err) {
reject(err);
}
});
};
// Database#prepare stays uncached: the caller owns the returned statement.
//
// The no-callback form returns a thenable wrapper around the statement.
// Awaiting it settles only once the worker has completed the prepare (and
// any bind), so the introspection accessors — `columns`, `parameterCount`,
// `parameterNames`, `readonly` — are populated at the first read after the
// await. The wrapper forwards every statement member, so pre-await
// chaining (`db.prepare(sql).run(...)`, `.finalize()`) keeps the
// historical synchronous surface; after the await, callers hold the
// statement itself. The callback form is unchanged: it returns the
// statement synchronously and a prepare error surfaces through its
// error-only errback rather than as a rejection.
/**
* Wraps a statement whose prepare (and optional bind) is still queued:
* awaiting the wrapper settles only once that work has landed, while every
* statement member stays reachable for pre-await chaining. After the
* await, callers hold the statement itself, not the wrapper.
*
* @param {import('./sqlite3-binding.js').Statement} statement the statement.
* @param {Promise<import('./sqlite3-binding.js').Statement>} ready the completion promise.
* @param {() => void} addAwaiter called for every read of `then`, so the
* failure routing can tell awaited wrappers from fire-and-forget ones.
* @returns {import('./sqlite3-binding.js').Statement & Promise<import('./sqlite3-binding.js').Statement>} the thenable wrapper.
* @private
*/
function prepareThenableWrapper(statement, ready, addAwaiter) {
/** @type {import('./sqlite3-binding.js').Statement & Promise<import('./sqlite3-binding.js').Statement>} */
let wrapper;
wrapper =
/** @type {import('./sqlite3-binding.js').Statement & Promise<import('./sqlite3-binding.js').Statement>} */ (
/** @type {unknown} */ (
new Proxy(statement, {
/**
* @param {import('./sqlite3-binding.js').Statement} target
* @param {string | symbol} prop
*/
get(target, prop) {
if (prop === 'then') {
addAwaiter();
return /** @type {(onFulfilled?: (value: unknown) => unknown, onRejected?: (err: Error) => unknown) => unknown} */ (
resolve,
reject,
) =>
ready.then(
/**
* @param {import('./sqlite3-binding.js').Statement} value
*/
(value) =>
/** @type {(value: unknown) => unknown} */ (
resolve
)(value),
/**
* @param {Error} err
*/
(err) =>
/** @type {(err: Error) => unknown} */ (
reject
)(err),
);
}
if (prop === 'catch') {
return (
/** @param {(err: Error) => unknown} onRejected */
(onRejected) => ready.catch(onRejected)
);
}
if (prop === 'finally') {
return (
/** @param {() => void} onFinally */
(onFinally) => ready.finally(onFinally)
);
}
const value = Reflect.get(target, prop, target);
if (typeof value !== 'function') return value;
// Native methods must run with the statement as
// the receiver: a proxy receiver cannot be
// unwrapped back to the ObjectWrap.
return /** @type {(...args: unknown[]) => unknown} */ (
...args
) => {
const result = value.apply(target, args);
// Methods that return `this` (the cores
// chain on it) hand back the wrapper, so
// pre-await chaining keeps one identity.
return result === target ? wrapper : result;
};
},
})
)
);
return wrapper;
}
/**
* The no-callback form of {@link Database#prepare}: schedules the prepare
* (and any bind) and returns the statement wrapped so awaiting it settles
* only once that work has landed and the introspection metadata is
* published.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {string} sql the SQL statement to prepare.
* @param {unknown[]} bindArgs the bind parameters (no trailing callback).
* @returns {import('./sqlite3-binding.js').Statement & Promise<import('./sqlite3-binding.js').Statement>} the statement, awaiting it gates on completion.
* @private
*/
/**
* The no-callback form of {@link Database#prepare}: schedules the prepare
* (and any bind) and returns the statement wrapped so awaiting it settles
* only once that work has landed and the introspection metadata is
* published.
*
* Failure routing keeps both documented surfaces alive: a statement whose
* `'error'` event has a listener hears the failure there (so callback-style
* code holding the wrapper keeps working), an awaiter gets the rejection
* alone, and code doing neither keeps the historical loudness — an
* `'error'` event with no listener still throws.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {string} sql the SQL statement to prepare.
* @param {unknown[]} bindArgs the bind parameters (no trailing callback).
* @returns {import('./sqlite3-binding.js').Statement & Promise<import('./sqlite3-binding.js').Statement>} the statement, awaiting it gates on completion.
* @private
*/
function prepareAsync(db, sql, bindArgs) {
let settled = false;
let awaiters = 0;
/** @type {import('./sqlite3-binding.js').Statement | undefined} */
let statement;
/** @type {(value: import('./sqlite3-binding.js').Statement) => void} */
let resolveReady;
/** @type {(err: Error) => void} */
let rejectReady;
/** @type {Promise<import('./sqlite3-binding.js').Statement>} */
const ready = new Promise((resolve, reject) => {
resolveReady = resolve;
rejectReady = reject;
});
// A rejection nobody awaits must not surface as an unhandled
// rejection: the wrapper's `then` re-exposes it to actual awaiters,
// and the statement's 'error' event stays the other surface.
ready.catch(() => {
// Deliberately empty; see above.
});
/**
* @param {Error} err
*/
const fail = (err) => {
if (settled) return;
settled = true;
if (
statement !== undefined &&
(statement.listenerCount('error') > 0 || awaiters === 0)
) {
statement.emit('error', err);
}
rejectReady(err);
};
const succeed = () => {
if (settled) return;
settled = true;
resolveReady(
/** @type {import('./sqlite3-binding.js').Statement} */ (statement),
);
};
statement = associateStatement(
db,
new Statement(
db,
sql,
/**
* @param {import('./native.js').SqliteError | null} err
*/
(err) => {
if (err) fail(err);
else if (bindArgs.length === 0) succeed();
},
),
);
// A trailing { integerMode } bag is a prepare option, not a bind
// argument; applied to the statement before any work is scheduled.
applyPrepareOptions(statement, bindArgs);
if (bindArgs.length > 0) {
try {
const bindVariadic =
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (nativeStatementBind)
);
// The native bind runs behind the prepare (the statement
// queues it), so its callback fires once both have completed.
bindVariadic.call(
statement,
...bindArgs,
/**
* @param {import('./native.js').SqliteError | null} err
*/
(err) => {
if (err) fail(err);
else succeed();
},
);
} catch (err) {
// Strict binding throws synchronously (the historical
// contract); the freshly prepared statement is orphaned, so
// finalize it — close() could otherwise end up SQLITE_BUSY.
nativeStatementFinalize.call(statement);
throw err;
}
}
return prepareThenableWrapper(statement, ready, () => {
awaiters++;
});
}
/**
* Prepares a statement for the caller to own.
*
* The no-callback form returns the statement wrapped in a thenable:
* `await db.prepare(sql)` (with or without bind parameters) resolves only
* once the worker has completed the prepare and the introspection
* accessors — `columns`, `parameterCount`, `parameterNames`, `readonly` —
* are populated, and yields the statement itself. The wrapper still
* forwards every statement member, so pre-await chaining
* (`db.prepare(sql).run(...)`) keeps the synchronous surface. A prepare
* failure rejects the await (and is reported on the statement's `'error'`
* event when one is registered); a bind failure throws synchronously
* after finalizing the orphan.
*
* The callback form is unchanged: it returns the statement synchronously
* and the trailing callback is an error-only errback, so a prepare
* failure surfaces there rather than as a rejection.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the SQL statement to prepare.
* @param {...unknown} args bind parameters, then optionally a callback.
* @returns {any} the statement — wrapped in a completion gate in
* promise mode; the precise overload set lives in lib/augment.d.ts.
*/
Database.prototype.prepare = function (sql, ...args) {
// No trailing function: promise mode. A function is never a legal bind
// value, so the trailing-argument test is the same one the dual-mode
// methods use to detect callback mode.
if (args.length === 0 || typeof args[args.length - 1] !== 'function') {
return prepareAsync(this, sql, args);
}
// The trailing callback sits in two native slots — the prepare
// errback and the queued bind's completion — so a failed prepare
// would otherwise reach it twice. The split guards that.
const split = splitPrepareCallback(args);
const statement = new Statement(
this,
sql,
split === null ? undefined : split.errback,
);
associateStatement(this, statement);
try {
// A trailing { integerMode } bag is a prepare option, not a bind
// argument.
applyPrepareOptions(statement, args);
const bindVariadic =
/** @type {(...args: unknown[]) => import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (nativeStatementBind)
);
return bindVariadic.apply(statement, args);
} catch (err) {
// Bind TypeErrors leave the freshly prepared statement orphaned;
// finalize it so close() cannot end up with SQLITE_BUSY.
nativeStatementFinalize.call(statement);
throw err;
}
};
// run/get/all/each/map reuse prepared statements when the caller enabled
// the cache with cacheStatements(). Under serialize() the cached path is
// bypassed: statement operations do not pass through the database queue,
// so strict FIFO ordering would be lost.
/**
* The inner body of a cached Database method: run the work against a
* (possibly cached) statement and finalize it unless it stays cached.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement the statement to drive.
* @param {unknown[]} params the caller's arguments after the SQL.
* @param {boolean} cached whether the statement came from the cache.
* @returns {unknown} whatever the method returns.
* @private
*/
// Database#run(sql, [bind1, bind2, ...], [callback])
Database.prototype.run = cachedMethod(
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement
* @param {unknown[]} params
* @param {boolean} cached
*/
function (statement, params, cached) {
/** @type {(...args: unknown[]) => unknown} */ (statement.run)(
...params,
);
if (!cached) nativeStatementFinalize.call(statement);
return this;
},
);
// Database#get(sql, [bind1, bind2, ...], [callback])
Database.prototype.get = cachedMethod(
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement
* @param {unknown[]} params
* @param {boolean} cached
*/
function (statement, params, cached) {
// A Database-level get is an independent call, not a cursor step
// (that is stmt.fetch()'s job), so a cached statement re-run with
// no bind parameters must start from its first row again. Left
// alone it re-stepped the previous call's cursor: the second
// db.get(sql) returned undefined, and the unreset statement held
// the connection's WAL read snapshot open. An empty array marks
// "bindings supplied" without binding anything, which forces the
// reset — legal only when the statement takes no parameters
// (parameterCount is undefined while the first prepare is still
// in flight; that call needs no reset anyway).
if (cached && params.length <= 1 && statement.parameterCount === 0) {
/** @type {(...args: unknown[]) => unknown} */ (statement.get)(
[],
...params,
);
} else {
/** @type {(...args: unknown[]) => unknown} */ (statement.get)(
...params,
);
}
if (!cached) nativeStatementFinalize.call(statement);
return this;
},
);
// Database#all(sql, [bind1, bind2, ...], [callback])
Database.prototype.all = cachedMethod(
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement
* @param {unknown[]} params
* @param {boolean} cached
*/
function (statement, params, cached) {
/** @type {(...args: unknown[]) => unknown} */ (statement.all)(
...params,
);
if (!cached) nativeStatementFinalize.call(statement);
return this;
},
);
// Database#each(sql, [bind1, bind2, ...], [callback], [complete])
Database.prototype.each = cachedMethod(
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement
* @param {unknown[]} params
* @param {boolean} cached
*/
function (statement, params, cached) {
/** @type {(...args: unknown[]) => unknown} */ (statement.each)(
...params,
);
if (!cached) nativeStatementFinalize.call(statement);
return this;
},
);
Database.prototype.map = cachedMethod(
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./sqlite3-binding.js').Statement} statement
* @param {unknown[]} params
* @param {boolean} cached
*/
function (statement, params, cached) {
/** @type {(...args: unknown[]) => unknown} */ (statement.map)(
...params,
);
if (!cached) nativeStatementFinalize.call(statement);
return this;
},
);
/**
* Builds a Database method around `fn` that resolves its SQL to a
* statement — from the cache when one is enabled, otherwise a fresh
* (and afterwards finalized) one.
*
* A cache hit skips the prepare, and statement operations never pass
* through the database queue — so while an exclusive operation
* (exec/close/wait/loadExtension) is running or waiting, the cached
* path would overtake it and run concurrently with it. It falls back to
* the uncached path there: its prepare goes through Database::Schedule
* and lands in the queue behind that operation.
*
* @param {(statement: import('./sqlite3-binding.js').Statement, params: unknown[], cached: boolean) => unknown} fn the method body.
* @returns {(this: import('./sqlite3-binding.js').Database, sql: string, ...args: any[]) => any} the assembled method.
* @private
*/
function cachedMethod(fn) {
return function (sql, ...args) {
const errBack = extractErrBack(args);
// In the two paths below a fresh statement is prepared, so the
// trailing callback sits in two native slots (prepare errback and
// the queued method's completion) and a failed prepare would
// reach it twice. The hit path has no prepare and pays nothing.
const split = splitPrepareCallback(args);
const prepareErrback = split?.errback ?? errBack;
const cache = this._stmtCache;
// Native state, read per field: while serialized, closing, or with
// anything queued/in flight on the database queue, the cached
// path would overtake that work (statement operations bypass
// Database::Schedule), so fall back to the uncached path whose
// prepare lands in the queue behind it. db.state is the same
// information as one frozen object, but constructing it per call
// measured +46% on the sync hot path (bench, Deliverable 05).
if (
cache &&
!this.serialized &&
!this.closing &&
this.queued === 0 &&
!(this.locked && this.pending > 0)
) {
let statement = cache.get(sql);
if (statement !== undefined) {
// Most recently used.
cache.delete(sql);
cache.set(sql, statement);
} else {
/**
* @param {import('./native.js').SqliteError | null} err
*/
const onPrepareError = function (err) {
if (!err) return;
// Failed to prepare: drop it so the next call retries.
cache.delete(sql);
if (split) split.errback.call(fresh, err);
else fresh.emit('error', err);
};
const fresh = new Statement(this, sql, onPrepareError);
statement = fresh;
associateStatement(this, fresh);
cache.set(sql, fresh);
if (cache.size > /** @type {number} */ (this._stmtCacheMax)) {
const oldestSql = /** @type {string} */ (
/** @type {unknown} */ (cache.keys().next().value)
);
const oldest =
/** @type {import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (cache.get(oldestSql))
);
cache.delete(oldestSql);
nativeStatementFinalize.call(oldest);
}
}
/** @type {import('./sqlite3-binding.js').Statement} */
const ready =
/** @type {import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (statement)
);
try {
return fn.call(this, ready, args, true);
} catch (err) {
// A synchronous bind TypeError: the statement is cached
// but nothing will ever drive it, so drop it rather than
// keeping a dead entry (and a pending prepare) around.
cache.delete(sql);
nativeStatementFinalize.call(ready);
throw err;
}
}
const statement = new Statement(this, sql, prepareErrback);
associateStatement(this, statement);
try {
return fn.call(this, statement, args, false);
} catch (err) {
// Same as above, uncached shape: finalize the orphaned
// statement so close() cannot end up with SQLITE_BUSY.
nativeStatementFinalize.call(statement);
throw err;
}
};
}
/**
* Enables the opt-in LRU cache of prepared statements for
* run/get/all/each/map, keyed on the SQL string.
*
* Defaults to 64 entries. Cached statements are finalized by close().
* Under serialize() the cache is bypassed to preserve strict FIFO
* ordering.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {number} [maxEntries] cache capacity; a positive integer.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
*/
Database.prototype.cacheStatements = function (maxEntries) {
if (!this._stmtCache) {
this._stmtCache = new Map();
this._stmtCacheMax = 64;
}
/** @type {Map<string, import('./sqlite3-binding.js').Statement>} */
const cache = this._stmtCache;
const max = Number.parseInt(
/** @type {string} */ (/** @type {unknown} */ (maxEntries)),
10,
);
if (max > 0) this._stmtCacheMax = max;
while (cache.size > /** @type {number} */ (this._stmtCacheMax)) {
const oldestSql = /** @type {string} */ (
/** @type {unknown} */ (cache.keys().next().value)
);
const oldest = /** @type {import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (cache.get(oldestSql))
);
cache.delete(oldestSql);
nativeStatementFinalize.call(oldest);
}
return this;
};
/**
* True for a trailing `{ integerMode }` prepare-options bag. Like the
* rowMode bag: named bind keys carry a sigil, so a plain object owning
* only `integerMode` could never have been a legal bind argument.
*
* @param {unknown} value the candidate.
* @returns {boolean} whether it is the options bag.
* @private
*/
function isPrepareOptions(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value);
return keys.length > 0 && keys.every((key) => key === 'integerMode');
}
/**
* Applies a prepare-options bag (if the last bind argument is one) to the
* statement. Returns true when a bag was consumed.
*
* @param {import('./sqlite3-binding.js').Statement} statement the fresh statement.
* @param {unknown[]} bindArgs the bind arguments; a bag is popped in place.
* @returns {boolean} whether an options bag was applied.
* @throws {TypeError} for a bad integerMode value (the native setter's).
* @private
*/
function applyPrepareOptions(statement, bindArgs) {
const last = bindArgs[bindArgs.length - 1];
if (bindArgs.length === 0 || !isPrepareOptions(last)) return false;
bindArgs.pop();
const mode =
/** @type {{ integerMode?: 'number' | 'bigint' | 'mixed' }} */ (last)
.integerMode;
if (mode !== undefined) {
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (statement._setIntegerMode)
)(mode);
}
return true;
}
/**
* Prepares a statement synchronously on the main thread.
*
* Throws when the database is not fully idle. The returned statement
* also supports the getSync/runSync/allSync fast path.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the SQL statement to prepare.
* @param {{ integerMode?: 'number' | 'bigint' | 'mixed' }} [options] a
* per-statement integer-mode override (node:sqlite's `readBigInts`,
* better-sqlite3's `safeIntegers`, as a one-shot option).
* @returns {import('./sqlite3-binding.js').Statement} the prepared statement.
* @throws {Error} When the database is not fully idle.
*/
Database.prototype.prepareSync = function (sql, options) {
const statement = new Statement(this, sql, undefined, true);
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('prepareSync() options must be an object');
}
for (const key of Object.keys(options)) {
if (key !== 'integerMode') {
throw new TypeError(
`prepareSync() received unknown option '${key}'`,
);
}
}
if (options.integerMode !== undefined) {
applyPrepareOptions(statement, [options]);
}
}
return statement;
};
/**
* True for the trailing `{ rowMode: ... }` options bag the sync read
* paths accept. The native side re-validates; this only has to be a
* cheap discriminator so the zero-parameter reset below still applies
* when an options bag is the only other argument.
*
* @param {unknown} value the candidate last argument.
* @returns {boolean} whether it is a rowMode options bag.
* @private
*/
function isSyncReadOptions(value) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
/** @type {Record<string, unknown>} */ (value).rowMode !== undefined
);
}
/**
* Executes `SELECT ... ` synchronously, consulting (and filling) the
* statement cache when enabled.
*
* @this {import('./sqlite3-binding.js').Database}
* @template T
* @param {string} sql the query.
* @param {...unknown} params bind parameters, optionally followed by a
* `{ rowMode: 'object' | 'array' }` options bag.
* @returns {T | undefined} the first row, or undefined.
* @throws {Error} When the database is not fully idle or binding fails.
*/
Database.prototype.getSync = function (sql, ...params) {
const statement = this._statementForSync(sql);
// A trailing options bag is not a bind parameter: pull it out so the
// zero-parameter reset below still sees the true parameter count.
let options;
if (params.length > 0 && isSyncReadOptions(params[params.length - 1])) {
options = params.pop();
}
// Same rule as the cached async get(): a Database-level get is an
// independent first-row query, not a cursor step, so a cached
// statement re-run without bind parameters starts from its first row
// again (the sync statement's re-stepping otherwise returns undefined
// from the second call on).
if (params.length === 0 && statement.parameterCount === 0) {
params = [[]];
}
return /** @type {T | undefined} */ (
/** @type {(...args: unknown[]) => unknown} */ (statement.getSync)(
...params,
...(options ? [options] : []),
)
);
};
/**
* Executes a statement synchronously; returns `{ lastID, changes }`.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the statement.
* @param {...unknown} params bind parameters.
* @returns {{ lastID: number, changes: number }} the run result.
* @throws {Error} When the database is not fully idle or binding fails.
*/
Database.prototype.runSync = function (sql, ...params) {
const statement = this._statementForSync(sql);
/** @type {(...args: unknown[]) => unknown} */ (statement.runSync)(
...params,
);
return {
lastID: /** @type {number} */ (statement.lastID),
changes: /** @type {number} */ (statement.changes),
};
};
/**
* Executes a query synchronously, returning every row.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the query.
* @param {...unknown} params bind parameters, optionally followed by a
* `{ rowMode: 'object' | 'array' }` options bag.
* @template T
* @returns {T[]} every result row.
* @throws {Error} When the database is not fully idle or binding fails.
*/
Database.prototype.allSync = function (sql, ...params) {
const statement = this._statementForSync(sql);
return /** @type {T[]} */ (
/** @type {(...args: unknown[]) => unknown} */ (statement.allSync)(
...params,
)
);
};
/**
* Resolves `sql` to a statement for the sync methods.
*
* Always cached, so the statement outlives the call and the caller never
* finalizes it; both caches are emptied by close() and by every
* user-function registration.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the SQL to prepare or reuse.
* @returns {import('./sqlite3-binding.js').Statement} the prepared statement.
* @private
*/
Database.prototype._statementForSync = function (sql) {
const cache = this._stmtCache;
// No closing check is needed here: a close in flight means either the
// sync prepare throws (its gate requires a fully idle connection) or,
// on a cache hit, the sync call itself does — and close() drains the
// cache, so a post-close hit is impossible.
if (cache) {
const statement = cache.get(sql);
if (statement !== undefined) {
cache.delete(sql);
cache.set(sql, statement);
return statement;
}
const fresh = new Statement(this, sql, undefined, true);
associateStatement(this, fresh);
cache.set(sql, fresh);
if (cache.size > /** @type {number} */ (this._stmtCacheMax)) {
const oldestSql = /** @type {string} */ (
/** @type {unknown} */ (cache.keys().next().value)
);
const oldest =
/** @type {import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (cache.get(oldestSql))
);
cache.delete(oldestSql);
nativeStatementFinalize.call(oldest);
}
return fresh;
}
// No user-enabled cache: the sync paths still keep one of their own.
//
// Preparing and finalizing a statement per call costs far more than the
// query it wraps — measured at ~5.6us against ~0.75us for the same
// getSync against a cached statement, so the convenience form was ~7x
// slower than the identical work through Database#prepare. That is the
// opposite of what a call named getSync should do.
//
// This cache is deliberately separate from `_stmtCache`: enabling that
// one would also change how the *asynchronous* calls behave on the same
// connection, which is the user's choice to make via cacheStatements().
// Both are emptied by _drainStatementCache, so close() and every
// user-function registration invalidate them together.
let syncCache = this._syncStmtCache;
if (!syncCache) {
syncCache = new Map();
this._syncStmtCache = syncCache;
}
const cached = syncCache.get(sql);
if (cached !== undefined) {
// Refresh recency: Map preserves insertion order, so delete+set
// moves the entry to the end and keys().next() stays the oldest.
syncCache.delete(sql);
syncCache.set(sql, cached);
return cached;
}
const prepared = associateStatement(
this,
new Statement(this, sql, undefined, true),
);
syncCache.set(sql, prepared);
if (syncCache.size > SYNC_STMT_CACHE_MAX) {
const oldestSql = /** @type {string} */ (
/** @type {unknown} */ (syncCache.keys().next().value)
);
const oldest = /** @type {import('./sqlite3-binding.js').Statement} */ (
/** @type {unknown} */ (syncCache.get(oldestSql))
);
syncCache.delete(oldestSql);
nativeStatementFinalize.call(oldest);
}
return prepared;
};
// Database#close flushes the statement cache first: sqlite3_close fails
// with SQLITE_BUSY while prepared statements are outstanding.
/** @type {(...args: unknown[]) => unknown} */
const nativeClose = Database.prototype.close;
/**
* Refuses an operation that would disturb the statement SQLite is
* stepping. `db._inSyncCall` is true exactly while a JavaScript callback
* invoked re-entrantly by a synchronous method (`getSync`/`runSync`/
* `allSync`, a virtual table's generator, a user function or aggregate)
* is on the stack — the JS thread is inside `sqlite3_step`. Registrations
* flush the statement cache, which finalizes that very statement, so they
* must be refused *before* touching anything: the native entry points
* refuse too, but by then the flush has already freed the live VM.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {string} what the refused operation, as a sentence opener.
* @returns {void}
* @throws {Error} when a synchronous method's callback is on the stack.
* @private
*/
function assertNotInSyncCallback(db, what) {
if (
!(
/** @type {{ _inSyncCall?: boolean }} */ (
/** @type {unknown} */ (db)
)._inSyncCall
)
) {
return;
}
throw new Error(
`${what} from inside a JavaScript callback invoked by a ` +
'synchronous method on this connection: SQLite is mid-step on ' +
'this connection and the statement cache holds the executing ' +
'statement. Do it before or after the query',
);
}
/**
* Finalizes every statement in the statement cache, emptying it.
*
* Used by close() (sqlite3_close fails with SQLITE_BUSY while prepared
* statements are outstanding) and by every user-function registration or
* removal: an existing prepared statement keeps invoking the function
* implementation it was compiled against, so the cache must not hand one
* back after the registration changed.
*
* @this {import('./sqlite3-binding.js').Database}
* @returns {void}
* @private
*/
Database.prototype._drainStatementCache = function () {
// Never from inside a callback a synchronous method invoked: the
// stepping statement is in the sync cache, and finalizing it would
// free the VM sqlite is executing. Every caller checks first (see
// assertNotInSyncCallback); this is the backstop.
assertNotInSyncCallback(this, 'the statement cache cannot be flushed');
// The implicit sync cache is drained on exactly the same events as the
// opt-in one: close(), and every user-function registration or removal
// (a prepared statement keeps invoking the implementation it was
// compiled against).
const syncCache = this._syncStmtCache;
if (syncCache && syncCache.size > 0) {
for (const [sql, statement] of syncCache) {
syncCache.delete(sql);
nativeStatementFinalize.call(statement);
}
}
const cache = this._stmtCache;
if (cache && cache.size > 0) {
// The drain is synchronous and the internal finalize carries no
// user callback, so no JS can run inside it and repopulate the
// cache behind our backs. If a cached statement is busy, its
// finalize queues behind that work; an exclusive native call
// (close, registration) therefore lands after it either way.
for (const [sql, statement] of cache) {
cache.delete(sql);
nativeStatementFinalize.call(statement);
}
}
};
/**
* Closes the connection. In callback mode (a trailing function) returns
* this database; otherwise returns a promise resolving once closed.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {...any} args optionally a callback.
* @returns {any}
*/
Database.prototype.close = function (...args) {
assertNotInSyncCallback(this, 'the connection cannot be closed');
this._drainStatementCache();
// See liveConnections (diagnostics_channel).
untrackConnection(this);
// Deliberately not deferred. close() is scheduled exclusively and
// Work_BeginClose requires pending == 0, so the native queue already
// makes it wait for the finalizes above (each either completes inline
// when its statement is idle, or queues behind that statement's
// in-flight work). Deferring the native call to a promise instead would
// let operations issued after close() run before the close is even
// requested.
return nativeClose.apply(this, args);
};
// --- User-defined functions, aggregates, window functions, collations ----
//
// The native halves (_registerFunction & co.) run through the exclusive
// queue: they touch the connection under the same mutex a worker blocked
// in a JS round trip holds, so they must wait until nothing is in flight.
// The wrappers add option validation, arity computation and the statement
// cache flush (a cached statement keeps the implementation it was compiled
// against until re-prepared).
// sqlite3_create_function_v2 text-encoding OR-flags; not exported by the
// native binding because they are only meaningful at registration.
const SQLITE_DETERMINISTIC = 0x000000800;
const SQLITE_DIRECTONLY = 0x0000080000;
const SQLITE_INNOCUOUS = 0x000200000;
// sqlite3_limit(SQLITE_LIMIT_FUNCTION_ARG) default; names over 255 bytes
// are likewise rejected by sqlite3CreateFunc.
const MAX_FUNCTION_ARG = 127;
const MAX_FUNCTION_NAME = 255;
/**
* Registers a scalar SQL function backed by a JavaScript callback.
*
* The callback runs on the JS thread while the worker thread that is
* stepping the statement blocks, at a measured cost of a few microseconds
* per call — see the README's "JavaScript functions and performance"
* section before filtering large tables with one.
*
* A registered function cannot be invoked from the synchronous methods
* (`getSync`/`runSync`/`allSync`): the JS thread is the one blocked inside
* SQLite there, so the call fails with an explicit error instead of
* deadlocking. Redefining an existing name replaces it (in-flight work
* completes on the old implementation first; the statement cache is
* flushed). Registration and removal are refused by SQLite with
* `SQLITE_BUSY`, reported on the connection's `'error'` event, while a
* cursor is suspended mid-query.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the SQL name (1..255 bytes).
* @param {import('./native.js').FunctionOptions | ((...args: unknown[]) => unknown)} [options]
* the options object, or the implementation directly.
* @param {(this: undefined, ...args: unknown[]) => unknown} [fn] the
* implementation; bind values and return values use exactly the bind
* marshalling rules (BigInt for large integers, Buffer for blobs; an
* unsupported return type is an error, never a coerced string).
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name or implementation is missing or an
* option key is unknown.
* @since 9.0.0
* @example
* db.function('regexp', { deterministic: true },
* (pattern, value) => (new RegExp(pattern).test(value) ? 1 : 0));
* db.all("SELECT name FROM t WHERE name REGEXP '^a'");
*/
Database.prototype.function = function (name, options, fn) {
assertNotInSyncCallback(this, 'a user function cannot be registered');
if (typeof options === 'function') {
fn = options;
options = undefined;
}
const { nArg, flags } = parseFunctionOptions(
name,
options,
typeof fn === 'function' ? fn.length : -1,
'function()',
);
if (typeof fn !== 'function') {
throw new TypeError('function() requires an implementation function');
}
// Flush the cache BEFORE registering: a cached statement suspended
// mid-cursor counts as an active VM, and sqlite refuses to replace a
// registration (SQLITE_BUSY) until every VM has halted. Finalizing
// the cache first is what makes redefinition work in the common case.
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (this._registerFunction)(
name,
nArg,
flags,
fn,
);
return this;
};
/**
* Registers an aggregate SQL function backed by JavaScript: `start()`
* creates an accumulator, `step(acc, ...args)` folds one row into it and
* `result(acc)` produces the final value. Providing `inverse` registers a
* window function instead (`OVER (...)` windows), where `inverse` removes
* a row that left the frame.
*
* Every step is one JS round trip, so an aggregate over N rows costs N
* calls; an empty group evaluates `start()` then `result()` with no step.
*
* Note: window functions (aggregates with `inverse`) are registered
* through `sqlite3_create_window_function`, which has no flag slot —
* `deterministic`/`directOnly`/`innocuous` cannot be applied to them and
* are ignored.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the SQL name (1..255 bytes).
* @param {import('./native.js').AggregateDefinition} spec the
* implementation.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when `start`, `step` or `result` is missing or not a
* function.
* @since 9.0.0
* @example
* db.aggregate('median', {
* start: () => [],
* step: (acc, v) => { acc.push(v); return acc; },
* result: (acc) => {
* acc.sort((a, b) => a - b);
* return acc.length ? acc[acc.length >> 1] : null;
* },
* });
* db.get('SELECT median(salary) FROM employees');
*/
Database.prototype.aggregate = function (name, spec) {
assertNotInSyncCallback(this, 'an aggregate cannot be registered');
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
throw new TypeError(
'aggregate() requires an implementation object with start, step and result functions',
);
}
const { start, step, result } = spec;
if (typeof start !== 'function') {
throw new TypeError("aggregate() requires a 'start' function");
}
if (typeof step !== 'function') {
throw new TypeError("aggregate() requires a 'step' function");
}
if (typeof result !== 'function') {
throw new TypeError("aggregate() requires a 'result' function");
}
const inverse = spec.inverse;
if (inverse !== undefined && typeof inverse !== 'function') {
throw new TypeError(
"aggregate() 'inverse', when given, must be a function",
);
}
const { nArg, flags } = parseFunctionOptions(
name,
spec,
Math.max(step.length - 1, 0),
'aggregate()',
['start', 'step', 'result', 'inverse'],
);
// See function() for why the cache is flushed before registering.
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._registerAggregate)
)(name, nArg, flags, start, step, result, inverse);
return this;
};
/**
* Registers a collation under the given name, usable in `ORDER BY`,
* `CREATE INDEX` and `COLLATE`: `ORDER BY name COLLATE mycoll`.
*
* The comparator receives two strings and returns a number with the
* `Array#sort`/`localeCompare` sign convention. Each comparison is a JS
* round trip on the JS thread — sorting N rows costs O(N log N) calls, so
* for anything but small or one-off sorts, sorting in JS after `all()` is
* faster.
*
* While a JavaScript collation is registered, the synchronous methods
* (`getSync`/`runSync`/`allSync`) refuse to run: a comparison would need
* the JS thread that is blocked inside SQLite, and unlike functions a
* collation cannot report an error mid-comparison. {@link Database#withCollation}
* scopes a registration to an awaited block, removing it again afterwards.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the collation name (1..255 bytes).
* @param {(a: string, b: string) => number} fn the comparator.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name or comparator is missing.
* @since 9.0.0
* @example
* db.collation('locale', (a, b) => a.localeCompare(b, 'de'));
* db.all('SELECT name FROM t ORDER BY name COLLATE locale');
*/
Database.prototype.collation = function (name, fn) {
assertNotInSyncCallback(this, 'a collation cannot be registered');
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('collation() requires a non-empty name string');
}
if (Buffer.byteLength(name, 'utf8') > MAX_FUNCTION_NAME) {
throw new TypeError(
`collation() name exceeds SQLite's ${MAX_FUNCTION_NAME}-byte limit`,
);
}
if (typeof fn !== 'function') {
throw new TypeError('collation() requires a comparator function');
}
// See function() for why the cache is flushed before registering.
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (this._registerCollation)(
name,
fn,
);
return this;
};
/**
* Removes every function and aggregate registered under `name`. In-flight
* queries complete on the old implementation; the statement cache is
* flushed so nothing re-uses a statement compiled against it. Removing an
* unknown name is a no-op.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the function name to remove.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name is not a non-empty string.
* @since 9.0.0
*/
Database.prototype.removeFunction = function (name) {
assertNotInSyncCallback(this, 'a user function cannot be removed');
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError(
'removeFunction() requires a non-empty name string',
);
}
// Flush the cache before removing: see function() — a suspended
// cached statement keeps sqlite refusing the change with SQLITE_BUSY.
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (this._removeFunction)(name);
return this;
};
/**
* Removes the collation registered under `name`. Removing an unknown name
* is a no-op; afterwards the synchronous methods work again.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the collation name to remove.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name is not a non-empty string.
* @since 9.0.0
*/
Database.prototype.removeCollation = function (name) {
assertNotInSyncCallback(this, 'a collation cannot be removed');
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError(
'removeCollation() requires a non-empty name string',
);
}
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (this._removeCollation)(
name,
);
return this;
};
/**
* Runs `fn` with a JavaScript collation registered, removing it again
* afterwards — the blast radius of the registration is the awaited block,
* not the connection's lifetime. Equivalent to
* `db.collation(name, cmp)` / `try { ... } finally { db.removeCollation(name) }`.
*
* While the collation is registered the synchronous methods refuse to run
* (see {@link Database#collation}); inside the block use the asynchronous
* API. An error thrown by `fn` still removes the collation before the
* rejection propagates. Interleaved or nested `withCollation` calls for
* the *same* name are last-wins — use distinct names for concurrent scopes.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the collation name (1..255 bytes).
* @param {(a: string, b: string) => number} cmp the comparator.
* @param {(db: import('./sqlite3-binding.js').Database) => unknown} fn the
* body to run with the collation registered; awaited before removal.
* @returns {Promise<unknown>} whatever `fn` resolves to.
* @throws {TypeError} when the name, comparator or body is missing.
* @since 9.0.2
* @example
* const rows = await db.withCollation('locale', (a, b) => a.localeCompare(b, 'de'),
* () => db.all('SELECT name FROM t ORDER BY name COLLATE locale'));
* // here the collation is removed and the sync methods work again
*/
Database.prototype.withCollation = function (name, cmp, fn) {
if (typeof fn !== 'function') {
throw new TypeError('withCollation() requires a body function');
}
// collation() validates the name and comparator synchronously.
this.collation(name, cmp);
return (async () => {
try {
return await fn(this);
} finally {
this.removeCollation(name);
}
})();
};
// --- Ergonomics parity (Phase 1) and virtual tables (Phase 4) ---------------
//
// pragma()/explain()/batch()/dump() are thin, promise-native wrappers over
// the async paths; table()/values() wrap the native vtab registration with
// definition validation. The introspection helpers (status/limits/location)
// map friendly names onto the native entry points.
// Leading whitespace and SQL comments (line and block), so the keyword
// test below sees the statement's first real token.
const SQL_LEADING_NOISE_RE =
/^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?(?:\*\/|$))+/;
// Statements whose result is rows rather than a change count.
const BATCH_READ_RE = /^(select|pragma|explain|with|values)\b/i;
// ... and the modifying statements that also return rows: RETURNING makes
// an INSERT/UPDATE/DELETE a read as far as the caller is concerned, and
// running it through run() would throw the rows away.
const BATCH_RETURNING_RE = /\bRETURNING\b/i;
/**
* Whether a batch entry's rows must be collected rather than its change
* count.
*
* @param {string} sql one statement.
* @returns {boolean} true when the statement yields rows.
* @private
*/
function batchStatementReadsRows(sql) {
const bare = sql.replace(SQL_LEADING_NOISE_RE, '');
return BATCH_READ_RE.test(bare) || BATCH_RETURNING_RE.test(bare);
}
/**
* @typedef {object} PragmaOptions
* @property {boolean} [simple] return only the first column of the first
* row (the scalar form better-sqlite3 popularised).
* @since 9.1.0
*/
/**
* Runs a `PRAGMA` and resolves its parsed result rows — the recommended
* way to run pragmas (they are statements, and this keeps them on the
* async queue like every other statement). `PRAGMA ${source}` is
* prepared as-is, so argument forms work too: `db.pragma('table_info(t)')`.
*
* With `{ simple: true }` resolves the first column of the first row
* (`db.pragma('user_version', { simple: true })` → the number), or
* undefined when the pragma returned no rows.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} source the pragma source (without the `PRAGMA` keyword).
* @param {PragmaOptions} [options]
* @returns {Promise<Record<string, unknown>[] | unknown>} the rows, or the
* scalar with `{ simple: true }`.
* @throws {TypeError} when the source or options are malformed.
* @since 9.1.0
* @example
* await db.pragma('journal_mode = WAL');
* const version = await db.pragma('user_version', { simple: true });
*/
Database.prototype.pragma = async function pragma(source, options) {
if (typeof source !== 'string' || source.length === 0) {
throw new TypeError('pragma() requires a non-empty source string');
}
let simple = false;
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('pragma() options must be an object');
}
for (const key of Object.keys(options)) {
if (key !== 'simple') {
throw new TypeError(
`pragma() received unknown option '${key}'`,
);
}
}
if (
options.simple !== undefined &&
typeof options.simple !== 'boolean'
) {
throw new TypeError("pragma() option 'simple' must be a boolean");
}
simple = options.simple === true;
}
const rows = /** @type {Record<string, unknown>[]} */ (
await this.all(`PRAGMA ${source}`)
);
if (!simple) return rows;
if (rows.length === 0) return undefined;
const first = rows[0];
const key = Object.keys(first)[0];
return key === undefined ? undefined : first[key];
};
/**
* Resolves the `EXPLAIN QUERY PLAN` rows for a statement — the query
* planner's own account of what it will do (index choices, scan order),
* without executing it. Parameters may be left unbound: a plan does not
* run the statement. Pass `{ full: true }` for the raw VDBE program
* (`EXPLAIN`), the low-level opcode listing.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} sql the statement to explain.
* @param {{ full?: boolean }} [options]
* @returns {Promise<Record<string, unknown>[]>} the plan rows.
* @throws {TypeError} when the sql or options are malformed.
* @since 9.1.0
* @example
* const plan = await db.explain('SELECT * FROM t WHERE id = ?');
*/
Database.prototype.explain = async function explain(sql, options) {
if (typeof sql !== 'string' || sql.length === 0) {
throw new TypeError('explain() requires a non-empty SQL string');
}
let full = false;
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('explain() options must be an object');
}
for (const key of Object.keys(options)) {
if (key !== 'full') {
throw new TypeError(
`explain() received unknown option '${key}'`,
);
}
}
if (options.full !== undefined && typeof options.full !== 'boolean') {
throw new TypeError("explain() option 'full' must be a boolean");
}
full = options.full === true;
}
return /** @type {Promise<Record<string, unknown>[]>} */ (
this.all(`${full ? 'EXPLAIN' : 'EXPLAIN QUERY PLAN'} ${sql}`)
);
};
/**
* @typedef {object} BatchStatement
* @property {string} sql one statement.
* @property {unknown} [args] bind parameters: an array of positional
* values, an object of named parameters, or a single positional value.
* The parameters may also follow the SQL in an array entry
* (`[sql, ...params]`).
* @since 9.1.0
*/
/**
* Runs an array of statements atomically, inside one transaction: either
* every statement lands or the whole batch rolls back. Entries are SQL
* strings, `[sql, ...params]` arrays or `{ sql, args }` objects;
* row-returning statements (`SELECT`/`PRAGMA`/`WITH`/`VALUES`/`EXPLAIN`,
* and anything carrying a `RETURNING` clause) resolve their rows into the
* results array, everything else resolves its `{ lastID, changes }`.
*
* `options.mode` maps the libsql batch modes onto BEGIN forms:
* `'write'` (the default) → `BEGIN IMMEDIATE`, `'read'`/`'deferred'` →
* `BEGIN DEFERRED`, `'exclusive'` → `BEGIN EXCLUSIVE`.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {(string | BatchStatement | [string, ...unknown[]])[]} statements
* @param {{ mode?: 'write' | 'read' | 'deferred' | 'exclusive' }} [options]
* @returns {Promise<unknown[]>} one result per statement.
* @throws {TypeError} when the statements or mode are malformed.
* @since 9.1.0
* @example
* await db.batch([
* 'CREATE TABLE t (a)',
* { sql: 'INSERT INTO t VALUES (?)', args: 1 },
* ]);
*/
Database.prototype.batch = async function batch(statements, options) {
if (!Array.isArray(statements)) {
throw new TypeError('batch() requires an array of statements');
}
const mode = options?.mode ?? 'write';
const begin =
mode === 'write'
? 'immediate'
: mode === 'read' || mode === 'deferred'
? 'deferred'
: mode === 'exclusive'
? 'exclusive'
: null;
if (begin === null) {
throw new TypeError(
"batch() mode must be 'write', 'read', 'deferred' or 'exclusive'",
);
}
/** @type {{ sql: string, args?: unknown }[]} */
const entries = statements.map((entry, i) => {
if (typeof entry === 'string') return { sql: entry };
if (Array.isArray(entry)) {
if (entry.length === 0 || typeof entry[0] !== 'string') {
throw new TypeError(
`batch()[${i}] must be a SQL string or [sql, ...params]`,
);
}
return { sql: entry[0], args: entry.slice(1) };
}
if (
entry === null ||
typeof entry !== 'object' ||
typeof entry.sql !== 'string'
) {
throw new TypeError(
`batch()[${i}] must be a SQL string, [sql, ...params] or { sql, args }`,
);
}
return { sql: entry.sql, args: entry.args };
});
return this.transaction(
async (tx) => {
/** @type {unknown[]} */
const results = [];
for (const {
sql,
args,
} of /** @type {{ sql: string, args?: unknown }[]} */ (entries)) {
// args is a positional array, a named-parameter object
// or a single positional value; all three are documented,
// and spreading the last two used to throw "Spread
// syntax requires ...iterable".
const bindArgs =
args === undefined || args === null
? []
: Array.isArray(args)
? /** @type {any[]} */ (args)
: [args];
if (batchStatementReadsRows(sql)) {
results.push(await tx.all(sql, ...bindArgs));
} else {
results.push(await tx.run(sql, ...bindArgs));
}
}
return results;
},
{ mode: begin },
);
};
/**
* Serializes one SQL literal for {@link sqlite3.iterdump}: the inverse of
* the bind marshalling (strings quoted, blobs as X'…' hex, BigInt exact).
*
* @param {unknown} value the value to serialize.
* @returns {string} the SQL literal.
* @private
*/
function dumpLiteral(value) {
if (value === null || value === undefined) return 'NULL';
if (typeof value === 'number') {
// `Infinity`/`NaN` are JavaScript spellings, not SQL: restoring
// them fails with "no such column: Infinity". SQLite's own shell
// writes the overflowing literal, which reloads as ±inf; NaN
// cannot be stored by SQLite at all (it becomes NULL), so this
// arm only matters for the value a REAL column can hold.
if (Number.isNaN(value)) return 'NULL';
if (value === Number.POSITIVE_INFINITY) return '9.0e+999';
if (value === Number.NEGATIVE_INFINITY) return '-9.0e+999';
return String(value);
}
if (typeof value === 'bigint') return value.toString();
if (typeof value === 'boolean') return value ? '1' : '0';
if (typeof value === 'string') {
return `'${value.replaceAll("'", "''")}'`;
}
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
return `X'${Buffer.from(
value.buffer,
value.byteOffset,
value.byteLength,
).toString('hex')}'`;
}
throw new TypeError(
`iterdump cannot serialize a ${typeof value} column value`,
);
}
/**
* Quotes one identifier (schema, table or column name) for dump SQL.
*
* @param {string} name the identifier.
* @returns {string} the quoted identifier.
* @private
*/
function dumpIdent(name) {
return `"${name.replaceAll('"', '""')}"`;
}
/**
* The columns of one table that a restore can INSERT into: everything
* except generated columns (`PRAGMA table_xinfo` reports those as hidden
* 2/3), which SQLite computes and refuses as INSERT targets.
*
* @param {import('./native.js').Database} db the connection.
* @param {string} table the table name.
* @returns {Promise<string[]>} the insertable column names, in order.
* @private
*/
async function dumpInsertableColumns(db, table) {
const info = /** @type {{ name: string, hidden: number }[]} */ (
await db.all(`PRAGMA table_xinfo(${dumpIdent(table)})`)
);
return info
.filter((column) => Number(column.hidden) === 0)
.map((column) => column.name);
}
/**
* Streams the database as `.dump`-style SQL: the schema (tables first,
* then their rows, then indexes/views/triggers), the AUTOINCREMENT
* counters and `user_version`, framed by the `BEGIN`/`COMMIT` a restore
* needs. Python `sqlite3`'s `iterdump()` equivalent; nothing else in Node
* has it.
*
* Rows are streamed (`iterate`), so dumping a table larger than memory is
* fine, and the reads run inside a deferred transaction — unless the
* caller already has one open — so the dump is a point-in-time snapshot.
* Abandoning the iterator early rolls that transaction back.
*
* Virtual tables keep their content: the `CREATE VIRTUAL TABLE` statement
* is emitted (which recreates the empty shadow tables), and each shadow
* table's rows follow as `DELETE` + `INSERT` against a
* `CREATE TABLE IF NOT EXISTS`, so an FTS index is restored exactly as it
* was instead of being silently dropped. No `writable_schema` games, so
* the output restores into a defensive-mode connection too.
*
* @param {import('./native.js').Database} db the connection.
* @returns {AsyncGenerator<string, void, void>} the SQL statements.
* @since 9.1.0
* @example
* for await (const statement of sqlite3.iterdump(db)) fs.write(statement);
*/
async function* iterdump(db) {
// A snapshot needs one read transaction for the whole walk. When the
// caller already has a transaction open, theirs is the snapshot.
const ownTransaction = !db.inTransaction;
if (ownTransaction) await db.exec('BEGIN DEFERRED');
let committed = false;
try {
yield 'PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n';
// sqlite_schema's rowid order is creation order — tables before
// the indexes/triggers created for them, which is the restore-safe
// order.
const schema =
/** @type {{ name: string, type: string, sql: string }[]} */ (
await db.all(
'SELECT name, type, sql FROM sqlite_schema ' +
"WHERE sql NOT NULL AND name NOT LIKE 'sqlite_%' " +
'ORDER BY rowid',
)
);
// Virtual tables keep their content, which lives in the shadow
// tables CREATE VIRTUAL TABLE itself creates. Those are dumped
// with IF NOT EXISTS + DELETE before their rows, so the restore
// works whether the DDL made them or not — which also makes a
// misidentified `<vtab>_*` user table harmless.
const virtualTables = [
...new Set(
schema
.filter(
(row) =>
row.type === 'table' &&
/^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(row.sql),
)
.map((row) => row.name),
),
];
/** @param {string} name @returns {boolean} */
const isShadowTable = (name) =>
virtualTables.some((vtab) => name.startsWith(`${vtab}_`));
/** @type {{ name: string, sql: string }[]} */
const deferred = [];
for (const row of schema) {
if (row.type !== 'table') {
deferred.push(row);
continue;
}
const shadow = isShadowTable(row.name);
const target = dumpIdent(row.name);
if (shadow && !/\bIF\s+NOT\s+EXISTS\b/i.test(row.sql)) {
yield `${row.sql.replace(
/^\s*CREATE\s+TABLE\s+/i,
'CREATE TABLE IF NOT EXISTS ',
)};\n`;
} else {
yield `${row.sql};\n`;
}
if (virtualTables.includes(row.name)) continue;
if (shadow) yield `DELETE FROM ${target};\n`;
const names = await dumpInsertableColumns(db, row.name);
if (names.length === 0) continue;
const columns = names.map(dumpIdent).join(',');
for await (const data of db.iterate(
`SELECT ${columns} FROM ${target}`,
)) {
const values = names
.map((name) =>
dumpLiteral(
/** @type {Record<string, unknown>} */ (data)[name],
),
)
.join(',');
yield `INSERT INTO ${target}(${columns}) VALUES(${values});\n`;
}
}
// AUTOINCREMENT high-water marks: without these a restored
// database reuses rowids the original had already handed out.
const sequences = /** @type {{ name: string, seq: unknown }[]} */ (
await db
.all(
'SELECT name, seq FROM sqlite_schema JOIN sqlite_sequence USING (name) ' +
"WHERE sqlite_schema.type = 'table' ORDER BY name",
)
.catch(() => [])
);
if (sequences.length > 0) {
yield 'DELETE FROM sqlite_sequence;\n';
for (const row of sequences) {
yield 'INSERT INTO sqlite_sequence(name,seq) VALUES' +
`(${dumpLiteral(row.name)},${dumpLiteral(row.seq)});\n`;
}
}
for (const row of deferred) {
yield `${row.sql};\n`;
}
// user_version carries the schema position sqlite3.migrate() and
// most migration tools key on; a dump that dropped it would rewind
// the restored database's migration state.
const userVersion = Number(
await db.pragma('user_version', { simple: true }),
);
if (Number.isInteger(userVersion) && userVersion !== 0) {
yield `PRAGMA user_version = ${userVersion};\n`;
}
yield 'COMMIT;\n';
if (ownTransaction) {
await db.exec('COMMIT');
committed = true;
}
} finally {
// Abandoned early (break/throw): the read transaction must not
// stay open on the connection.
if (ownTransaction && !committed) {
try {
await db.exec('ROLLBACK');
} catch {
// Already resolved by an outer failure; nothing to undo.
}
}
}
}
/**
* Serializes the whole database to `.dump`-style SQL text (see
* {@link sqlite3.iterdump} for the streaming form).
*
* @this {import('./sqlite3-binding.js').Database}
* @returns {Promise<string>} the SQL text of the dump.
* @since 9.1.0
* @example
* fs.writeFileSync('backup.sql', await db.dump());
*/
Database.prototype.dump = async function dump() {
let out = '';
for await (const statement of iterdump(this)) {
out += statement;
}
return out;
};
sqlite3.iterdump = iterdump;
sqlite3.migrate = migrate;
// Friendly names for the run-time limits db.limits reports, over the
// LIMIT_* constants the binding exports.
const LIMIT_NAMES = /** @type {const} */ ([
['length', 'LIMIT_LENGTH'],
['sqlLength', 'LIMIT_SQL_LENGTH'],
['column', 'LIMIT_COLUMN'],
['exprDepth', 'LIMIT_EXPR_DEPTH'],
['compoundSelect', 'LIMIT_COMPOUND_SELECT'],
['vdbeOp', 'LIMIT_VDBE_OP'],
['functionArg', 'LIMIT_FUNCTION_ARG'],
['attached', 'LIMIT_ATTACHED'],
['likePatternLength', 'LIMIT_LIKE_PATTERN_LENGTH'],
['variableNumber', 'LIMIT_VARIABLE_NUMBER'],
['triggerDepth', 'LIMIT_TRIGGER_DEPTH'],
['workerThreads', 'LIMIT_WORKER_THREADS'],
]);
// Friendly names for the db.status() counters, over the DBSTATUS_*
// constants the binding exports.
const DB_STATUS_NAMES = /** @type {const} */ ([
['lookasideUsed', 'DBSTATUS_LOOKASIDE_USED'],
['cacheUsed', 'DBSTATUS_CACHE_USED'],
['schemaUsed', 'DBSTATUS_SCHEMA_USED'],
['stmtUsed', 'DBSTATUS_STMT_USED'],
['lookasideHit', 'DBSTATUS_LOOKASIDE_HIT'],
['lookasideMissSize', 'DBSTATUS_LOOKASIDE_MISS_SIZE'],
['lookasideMissFull', 'DBSTATUS_LOOKASIDE_MISS_FULL'],
['cacheHit', 'DBSTATUS_CACHE_HIT'],
['cacheMiss', 'DBSTATUS_CACHE_MISS'],
['cacheWrite', 'DBSTATUS_CACHE_WRITE'],
['cacheSpill', 'DBSTATUS_CACHE_SPILL'],
['deferredFks', 'DBSTATUS_DEFERRED_FKS'],
]);
/**
* Reads one `sqlite3_db_status` counter for the connection — cache
* hits/misses, schema memory, and friends. Pass the counter by friendly
* name (`'cacheHit'`) or by `sqlite3.DBSTATUS_*` constant. Resolves
* `{ current, highwater }`; `{ reset: true }` zeroes the counters after
* reading.
*
* Note: this build compiles with `SQLITE_DEFAULT_MEMSTATUS=0`, so the
* process-wide memory counters report zero; the cache and schema
* counters are the useful ones.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string | number} op the counter name or constant.
* @param {{ reset?: boolean }} [options]
* @returns {{ current: number, highwater: number }} the counter values.
* @throws {TypeError} when the name is unknown.
* @since 9.1.0
* @example
* const { current } = db.status('cacheHit');
*/
Database.prototype.status = function status(op, options) {
const reset = options?.reset === true;
if (typeof op === 'string') {
const entry = DB_STATUS_NAMES.find(([name]) => name === op);
if (entry === undefined) {
throw new TypeError(`db.status() received unknown counter '${op}'`);
}
return this._dbStatus(/** @type {any} */ (sqlite3)[entry[1]], reset);
}
return this._dbStatus(op, reset);
};
/**
* Releases non-essential memory held by this connection (page cache
* beyond the working set) — the pool-pressure lever. Returns the number
* of bytes freed (`sqlite3_db_release_memory`).
*
* @this {import('./sqlite3-binding.js').Database}
* @returns {number} bytes freed.
* @since 9.1.0
*/
Database.prototype.releaseMemory = function releaseMemory() {
return /** @type {number} */ (this._releaseMemory());
};
/**
* The current run-time limits, by friendly name — the read form of
* `configure('limit', sqlite3.LIMIT_*, value)`.
*
* @this {import('./sqlite3-binding.js').Database}
* @name Database#limits
* @type {Record<string, number>}
* @since 9.1.0
*/
Object.defineProperty(Database.prototype, 'limits', {
get() {
/** @type {Record<string, number>} */
const out = {};
for (const [name, constant] of LIMIT_NAMES) {
out[name] = /** @type {number} */ (
this._getLimit(/** @type {any} */ (sqlite3)[constant])
);
}
return out;
},
configurable: true,
});
/**
* Resolves the filesystem path of an attached database (`''` for
* in-memory or temporary schemas) — `sqlite3_db_filename`.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} [dbName] the attached name (default `'main'`).
* @returns {string} the path.
* @since 9.1.0
*/
Database.prototype.location = function location(dbName) {
return /** @type {string} */ (this._dbLocation(dbName ?? 'main'));
};
// --- Tagged-template queries (Phase 5) ----------------------------------------
// The fragment brand. Composition helpers (raw/identifier/identifierPath/
// join/empty) mint fragments carrying this symbol, and only a value
// carrying it is spliced into the SQL as text. Structural detection ("has
// a `text` property") would turn any attacker-shaped object arriving in a
// template hole — `JSON.parse('{"text":"1 OR 1=1","params":[]}')` — into
// raw SQL. Everything unbranded is a bind parameter, always.
const SQL_FRAGMENT = Symbol('@appthreat/sqlite3.sqlFragment');
/**
* One composed piece of SQL for the tag store: literal text plus bind
* parameters. `sql.raw`/`identifier`/`join` build these; a plain value in
* a template hole becomes a bind parameter instead.
*
* @typedef {object} SqlFragment
* @property {string} text the SQL text.
* @property {unknown[]} params the bind parameters, in text order.
* @private
*/
/**
* Builds a fragment.
*
* @param {string} text the SQL text.
* @param {unknown[]} params the bind parameters.
* @returns {SqlFragment} the fragment.
* @private
*/
function fragment(text, params) {
return Object.freeze({ text, params, [SQL_FRAGMENT]: true });
}
/**
* True for a value minted by one of this store's composition helpers.
*
* @param {unknown} value the candidate.
* @returns {boolean} whether it is a branded fragment.
* @private
*/
function isFragment(value) {
return (
value !== null &&
typeof value === 'object' &&
/** @type {{ [SQL_FRAGMENT]?: unknown }} */ (value)[SQL_FRAGMENT] ===
true
);
}
/**
* A SQL identifier quoted for safe interpolation — the Kysely/Sequelize
* helper names Bun notably lacks. Accepts plain identifiers and quoted
* forms (already-`"quoted"` strings pass through).
*
* @param {string} name the identifier.
* @returns {SqlFragment} the quoted fragment.
* @throws {TypeError} for anything that is not a plain identifier.
* @since 9.1.0
*/
function sqlIdentifier(name) {
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('sql.identifier() requires a non-empty string');
}
if (/^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) {
return fragment(`"${name}"`, []);
}
// Schema-qualified or already-quoted forms pass through. The pattern
// is what makes that safe: every part is wrapped in double quotes and
// may not contain one, so nothing can close the quoting early. (The
// previous guard also rejected every name containing a quote, which
// made this branch unreachable.)
if (!/[\\\0]/.test(name) && /^"[^"]+"(\."[^"]+")*$/.test(name)) {
return fragment(name, []);
}
throw new TypeError(
`sql.identifier() received ${JSON.stringify(name)}, which is not ` +
'a plain identifier (quote each part yourself for exotic names)',
);
}
/**
* A dotted identifier path quoted part by part: `identifierPath('main.t.x')`.
*
* @param {string} dotted the dot-separated path.
* @returns {SqlFragment} the quoted fragment.
* @throws {TypeError} for empty or malformed parts.
* @since 9.1.0
*/
function sqlIdentifierPath(dotted) {
if (typeof dotted !== 'string' || dotted.length === 0) {
throw new TypeError('sql.identifierPath() requires a non-empty string');
}
// Dots inside a "quoted part" belong to the name, not to the path:
// identifierPath('"a.b".c') is two parts, not three.
/** @type {string[]} */
const parts = [];
let current = '';
let quoted = false;
for (const ch of dotted) {
if (ch === '"') {
quoted = !quoted;
current += ch;
} else if (ch === '.' && !quoted) {
parts.push(current);
current = '';
} else {
current += ch;
}
}
parts.push(current);
if (quoted) {
throw new TypeError(
'sql.identifierPath() received an unterminated quoted part',
);
}
return fragment(
parts
.map((part) => {
if (part.length === 0) {
throw new TypeError(
'sql.identifierPath() received an empty part',
);
}
return sqlIdentifier(part).text;
})
.join('.'),
[],
);
}
/**
* Interpolates one template-hole value into the SQL under construction.
*
* @param {string[]} out the accumulated SQL chunks.
* @param {unknown[]} params the accumulated bind parameters.
* @param {unknown} value the hole's value.
* @returns {void}
* @private
*/
function appendHole(out, params, value) {
if (isFragment(value)) {
const frag = /** @type {SqlFragment} */ (value);
out.push(frag.text);
params.push(...frag.params);
return;
}
out.push('?');
params.push(value);
}
/**
* Builds the {@link TagStore} — tagged-template queries driven by an LRU
* of composed SQL (`store.all\`SELECT … WHERE id = ${id}\``), the
* node:sqlite `createTagStore` shape plus the composition helpers ORMs
* need: `store.raw`, `store.join`, `store.identifier`,
* `store.identifierPath` and `store.empty`. Interpolated values become
* positional `?` parameters (only a fragment from those helpers is
* spliced in as SQL text); the joined SQL is the cache key.
*
* Statement reuse is the connection's: creating a store enables the
* connection statement cache ({@link Database#cacheStatements}) if it is
* not already on (an existing one is left at its own size), since
* composing the same SQL repeatedly is the whole point of a tag store.
* The store's own LRU holds the composed SQL keys (never the bound
* values, which would pin every buffer the first call bound), and
* `clear()` empties both.
*
* Promise-native (an improvement on node's sync-only store).
*
* @this {import('./sqlite3-binding.js').Database}
* @param {number} [maxSize=200] the cache capacity.
* @returns {import('./augment.js').TagStore} the store.
* @throws {TypeError} for a non-positive capacity.
* @since 9.1.0
* @example
* const store = db.createTagStore();
* const rows = await store.all`SELECT * FROM t WHERE id = ${id}`;
*/
Database.prototype.createTagStore = function createTagStore(maxSize = 200) {
if (
typeof maxSize !== 'number' ||
!Number.isInteger(maxSize) ||
maxSize < 1
) {
throw new TypeError(
'createTagStore() maxSize must be a positive integer',
);
}
const db = this;
// The composed-SQL keys this store has seen, most recently used last
// (Map insertion order). Values are never retained: the statements
// themselves live in the connection's cache.
/** @type {Set<string>} */
const cache = new Set();
if (!db._stmtCache) db.cacheStatements();
/**
* Builds (or reuses) the statement arguments for one template call.
*
* @param {TemplateStringsArray} templates the template strings.
* @param {unknown[]} values the interpolated values.
* @returns {{ sql: string, params: unknown[] }} the statement and bind values.
*/
const build = (templates, values) => {
/** @type {string[]} */
const chunks = [];
/** @type {unknown[]} */
const params = [];
chunks.push(templates.raw[0]);
for (let i = 0; i < values.length; i++) {
appendHole(chunks, params, values[i]);
chunks.push(templates.raw[i + 1]);
}
const sql = chunks.join('');
// Most recently used last; the oldest key is evicted at capacity.
cache.delete(sql);
cache.add(sql);
if (cache.size > maxSize) {
const oldest = cache.values().next().value;
if (oldest !== undefined) cache.delete(oldest);
}
return { sql, params };
};
/** @type {import('./augment.js').TagStore} */
const store = /** @type {import('./augment.js').TagStore} */ (
/** @type {unknown} */ ({
get db() {
return db;
},
get size() {
return cache.size;
},
get capacity() {
return maxSize;
},
clear() {
// The statements themselves live in the connection's
// statement cache (keyed by the same SQL strings); clear
// that too or clear() frees nothing. Refuse first, before
// either half is touched: flushing the cache from inside a
// sync-invoked callback would finalize the statement
// sqlite is stepping.
assertNotInSyncCallback(db, 'the tag store cannot be cleared');
cache.clear();
db._drainStatementCache();
},
})
);
/**
* @param {string} method
* @param {TemplateStringsArray} templates
* @param {unknown[]} values
* @returns {any}
*/
const tag = (method, templates, values) => {
// (typed by the JSDoc above)
if (!Array.isArray(templates) || !Array.isArray(templates.raw)) {
throw new TypeError(
'the tag store is used as a template tag: store.' +
`${method}\`SELECT …\``,
);
}
const { sql, params } = build(templates, values);
return /** @type {any} */ (db)[method](sql, ...params);
};
store.get = (templates, ...values) => tag('get', templates, values);
store.all = (templates, ...values) => tag('all', templates, values);
store.iterate = (templates, ...values) => tag('iterate', templates, values);
store.run = (templates, ...values) => tag('run', templates, values);
store.raw = (text) => {
if (typeof text !== 'string') {
throw new TypeError('sql.raw() requires a string');
}
return fragment(text, []);
};
store.join = (items, separator = ', ') => {
if (!Array.isArray(items) || items.length === 0) {
throw new TypeError('sql.join() requires a non-empty array');
}
if (typeof separator !== 'string') {
throw new TypeError('sql.join() separator must be a string');
}
/** @type {string[]} */
const parts = [];
/** @type {unknown[]} */
const params = [];
for (const item of items) {
// Fragments compose as text; anything else binds. An IN-list
// of plain values is the common case (`join(ids)`), and it
// must not require the caller to reach for raw() — which on
// user data would be an injection.
if (isFragment(item)) {
const frag = /** @type {SqlFragment} */ (item);
parts.push(frag.text);
params.push(...frag.params);
} else {
parts.push('?');
params.push(item);
}
}
return fragment(parts.join(separator), params);
};
store.identifier = sqlIdentifier;
store.identifierPath = sqlIdentifierPath;
store.empty = () => fragment('', []);
return store;
};
// --- JavaScript virtual tables (Phase 4) -------------------------------------
/**
* Normalizes one column spec: `'name'`, `'name TYPE'` or
* `{ name, type }` into the bare column name.
*
* @param {unknown} spec the column spec.
* @param {string} who the call site, for error messages.
* @returns {string} the column name.
* @throws {TypeError} when the spec is malformed.
* @private
*/
function vtabColumnName(spec, who) {
if (typeof spec === 'string') {
const name = spec.split(/\s+/)[0];
if (name.length > 0) return name;
} else if (
spec !== null &&
typeof spec === 'object' &&
typeof (/** @type {{ name?: unknown }} */ (spec).name) === 'string' &&
/** @type {{ name?: unknown }} */ (spec).name !== undefined &&
/** @type {string} */ (/** @type {{ name?: unknown }} */ (spec).name)
.length > 0
) {
return /** @type {{ name: string }} */ (spec).name;
}
throw new TypeError(`${who} column specs must be strings or { name }`);
}
/**
* @typedef {object} VtabDefinition
* @property {(this: undefined, ...args: unknown[]) => Iterable<unknown[] | Record<string, unknown>>} rows
* the row generator: invoked once per query with the table-function
* parameter values; yields arrays (in column order) or objects keyed by
* column name.
* @property {(string | { name: string, type?: string })[]} columns the
* result columns (a `'name TYPE'` string or `{ name, type }`; the type
* is documentation — SQLite virtual tables are typeless).
* @property {string[]} [parameters] a subset of `columns` to declare
* HIDDEN — the table-valued function's arguments
* (`SELECT * FROM name(arg)` passes `arg` to `rows`).
*
* A parameter is a real (hidden) column, and `name(arg)` is the
* predicate `WHERE param = arg`, which SQLite re-checks against every
* row the generator yields — the generator is not trusted to have
* applied it. A row therefore either leaves that column NULL (it is
* filled with the argument) or echoes the argument *as received*: the
* column has no affinity, so an echoed `String(5)` is text and no
* longer equals the integer `5`. A row reporting anything else there
* contradicts the predicate and is filtered out.
* @since 9.1.0
*/
/**
* Registers a read-only virtual table computed by a JavaScript generator
* — better-sqlite3's marquee feature, none of the other JS drivers have
* it, and here it works from both the async paths (one worker round trip
* per query — the whole generator output is materialised at filter time)
* and the sync methods (a direct re-entrant call).
*
* The object form registers an **eponymous-only** module: the table
* exists immediately under `name` (no `CREATE VIRTUAL TABLE`). The
* factory-function form (`db.table(name, (arg, ...) => definition)`)
* registers a named module instantiated per
* `CREATE VIRTUAL TABLE ... USING name(args)`; the arguments arrive as
* the SQL literal strings from the DDL.
*
* Registration is asynchronous but ordered: a query issued right after
* `db.table()` queues behind the registration and sees the table. The
* statement cache is flushed (a cached statement cannot gain the table).
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the module/table name (1..255 bytes).
* @param {VtabDefinition | ((...args: string[]) => unknown)} definition
* the definition object, or a factory function for a named module.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name or definition is malformed.
* @since 9.1.0
* @example
* db.table('sequence', {
* columns: ['value', 'count'],
* parameters: ['count'], // HIDDEN: sequence(5) passes 5 to rows()
* rows: function* (count) {
* for (let i = 0; i < count; i++) yield [i];
* },
* });
* const rows = await db.all('SELECT value FROM sequence(5)');
*/
Database.prototype.table = function table(name, definition) {
assertNotInSyncCallback(this, 'a virtual table cannot be registered');
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('table() requires a non-empty name string');
}
if (Buffer.byteLength(name, 'utf8') > 255) {
throw new TypeError(
"table() name exceeds SQLite's 255-byte module-name limit",
);
}
const isFactory = typeof definition === 'function';
if (
definition === null ||
(typeof definition !== 'object' && typeof definition !== 'function')
) {
throw new TypeError(
'table() requires a definition object or a factory function',
);
}
/**
* Extracts the shape from the definition (the factory's own columns
* are declared up front by the module).
*
* @param {VtabDefinition | ((...args: string[]) => VtabDefinition)} def
* @param {boolean} allowMissingRows
* @returns {{ columns: string[], params: string[], rows: unknown }}
*/
const parse = (def, allowMissingRows) => {
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
throw new TypeError(
'table() definition must be an object with columns and rows',
);
}
const known = new Set(['rows', 'columns', 'parameters']);
for (const key of Object.keys(def)) {
if (!known.has(key)) {
throw new TypeError(
`table() definition received unknown option '${key}'`,
);
}
}
if (!Array.isArray(def.columns)) {
throw new TypeError(
"table() definition requires a 'columns' array",
);
}
const columns = def.columns.map((spec) =>
vtabColumnName(spec, 'table()'),
);
const parameters = def.parameters ?? [];
if (!Array.isArray(parameters)) {
throw new TypeError(
"table() option 'parameters' must be an array of column names",
);
}
for (const param of parameters) {
if (typeof param !== 'string' || param.length === 0) {
throw new TypeError(
"table() 'parameters' entries must be non-empty strings",
);
}
if (!columns.includes(param)) {
throw new TypeError(
`table() parameter '${param}' is not one of the columns; ` +
'parameters are the subset of columns declared HIDDEN',
);
}
}
const rows = /** @type {unknown} */ (def.rows);
if (typeof rows !== 'function' && !allowMissingRows) {
throw new TypeError(
"table() definition requires a 'rows' generator function",
);
}
return { columns, params: parameters, rows };
};
let columns;
let params;
let factory = null;
let rows = null;
if (isFactory) {
// A factory's instances all share the module's declared shape, so
// the factory itself carries the columns (same spec shapes as a
// definition's); the definitions it returns supply only rows.
factory = definition;
const fn = /** @type {{ columns?: unknown, parameters?: unknown }} */ (
/** @type {unknown} */ (definition)
);
if (!Array.isArray(fn.columns)) {
throw new TypeError(
'a table() factory must declare its columns as factory ' +
".columns (same shape as a definition's), so the module " +
'can declare them for every instance',
);
}
columns = fn.columns.map((spec) => vtabColumnName(spec, 'table()'));
const fparams = fn.parameters ?? [];
if (!Array.isArray(fparams)) {
throw new TypeError(
'factory.parameters must be an array of column names',
);
}
for (const param of fparams) {
if (typeof param !== 'string' || !columns.includes(param)) {
throw new TypeError(
`factory parameter '${String(param)}' is not one of the columns`,
);
}
}
params = fparams;
} else {
const parsed = parse(definition, false);
columns = parsed.columns;
params = parsed.params;
rows = parsed.rows;
}
// See function(): a cached statement keeps the schema it was compiled
// against, so the cache must not hand one back across a registration.
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._registerVtab)
)(name, columns, params, factory, rows);
return this;
};
/**
* Removes a virtual table module registered with {@link Database#table}.
* In-flight queries complete; a later query against the name fails
* loudly ("this virtual table module was removed").
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} name the module name.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the name is malformed.
* @since 9.1.0
*/
Database.prototype.removeTable = function removeTable(name) {
assertNotInSyncCallback(this, 'a virtual table cannot be removed');
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('removeTable() requires a non-empty name string');
}
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._removeVtab)
)(name);
return this;
};
// The auto-incrementing suffix for db.values() table names.
let valuesTableCounter = 0;
/**
* The per-connection registry of db.values() tables: registration order
* (for the cap) and the drop handles.
*
* @typedef {object} ValuesRegistry
* @property {string[]} order the table names, oldest registration first.
* @property {Map<string, { name: string, drop: () => void }>} byName the handles.
*/
/** @type {WeakMap<object, ValuesRegistry>} */
const valuesTables = new WeakMap();
const VALUES_TABLES_MAX = 32;
/**
* Registers one JS array (or any iterable) as a queryable table — the
* rusqlite `rarray()` ergonomics no JS driver has: `WHERE id IN (SELECT
* value FROM v)` and `JOIN` against in-memory data, no string-building.
*
* Returns `{ name, drop() }`: `name` is the (unquoted) table name to use
* in SQL, keyed by position (`key`, 0-based) and element (`value`).
* `drop()` removes one explicitly, and the whole connection is cleaned up
* at close.
*
* Anonymous registrations are capped at 32 per connection: the 33rd drops
* the oldest, whose handle then refers to a table that no longer exists
* (a query against it fails with "this virtual table module was removed").
* The cap exists so a forgotten `drop()` cannot grow without bound —
* `drop()` each handle when you are done with it, or pass an explicit
* `{ name }`, which opts out of the cap entirely.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {Iterable<unknown>} iterable the array/iterable to expose.
* @param {{ name?: string }} [options] an explicit table name (then no
* LRU applies — drop() it yourself).
* @returns {{ name: string, drop: () => void }} the table handle.
* @throws {TypeError} when the values or options are malformed.
* @since 9.1.0
* @example
* const ids = db.values([4, 8, 15]);
* const rows = await db.all(
* `SELECT * FROM users JOIN ${ids.name} v ON users.id = v.value`,
* );
*/
Database.prototype.values = function values(iterable, options) {
assertNotInSyncCallback(this, 'a values table cannot be registered');
if (iterable === null || typeof iterable[Symbol.iterator] !== 'function') {
throw new TypeError('values() requires an iterable');
}
let explicitName;
if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('values() options must be an object');
}
for (const key of Object.keys(options)) {
if (key !== 'name') {
throw new TypeError(
`values() received unknown option '${key}'`,
);
}
}
if (options.name !== undefined) {
if (typeof options.name !== 'string' || options.name.length === 0) {
throw new TypeError(
"values() option 'name' must be a non-empty string",
);
}
explicitName = options.name;
}
}
let registry = valuesTables.get(this);
if (registry === undefined) {
registry = { order: [], byName: new Map() };
valuesTables.set(this, registry);
}
const name = explicitName ?? `sqlite_values_${++valuesTableCounter}`;
this.table(name, {
columns: ['key', 'value'],
rows: function* () {
let key = 0;
for (const value of iterable) {
yield [key++, value];
}
},
});
/** @type {() => void} */
let drop = () => {
this.removeTable(name);
registry?.byName.delete(name);
const at = registry?.order.indexOf(name);
if (at !== undefined && at >= 0) registry?.order.splice(at, 1);
// Dropped once; a second call is a no-op rather than a second
// removeTable (which would refuse or drop a re-registered name).
drop = () => undefined;
};
const existing = registry.byName.get(name);
if (existing !== undefined) existing.drop();
registry.byName.set(name, { name, drop: () => drop() });
if (!explicitName) {
registry.order.push(name);
while (registry.order.length > VALUES_TABLES_MAX) {
const oldest = registry.order.shift();
const entry =
oldest !== undefined ? registry.byName.get(oldest) : undefined;
if (entry !== undefined) entry.drop();
}
}
return { name, drop: () => drop() };
};
// --- Hooks, authorizer, progress, WAL and introspection (Deliverable 07) --
//
// The commit/rollback/wal hooks are installed by the on()/removeListener()
// overrides above: the native sqlite hook exists only while a listener is
// registered, so an installed-but-unused hook costs nothing.
/**
* Installs (or removes) a declarative authorizer on the connection.
*
* The policy is evaluated inside SQLite itself, in C++ — there is no
* JavaScript callback on the prepare path, so it is fast and safe from any
* thread that prepares a statement. This is the supported way to sandbox
* user-supplied SQL: with `{ default: 'deny' }` everything is refused
* unless a rule explicitly allows it.
*
* `deny` rules are evaluated before `allow` rules, and a denied action
* fails the statement with `SQLITE_AUTH` ("not authorized"). The statement
* cache is flushed on every change: a cached statement was compiled while
* the old policy was in force and would bypass the new one entirely.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./native.js').AuthorizerPolicy | null} [policy] the
* policy to install, or null/undefined to remove the authorizer.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the policy or one of its rules is malformed.
* @since 9.0.0
* @example
* db.authorizer({
* default: 'deny',
* allow: [
* { action: sqlite3.SELECT },
* { action: sqlite3.READ, table: 'users' },
* ],
* });
*/
Database.prototype.authorizer = function (policy) {
assertNotInSyncCallback(this, 'the authorizer policy cannot be changed');
if (policy === null || policy === undefined) {
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._setAuthorizer)
)();
return this;
}
if (typeof policy !== 'object' || Array.isArray(policy)) {
throw new TypeError('authorizer() policy must be an object or null');
}
const known = new Set(['default', 'allow', 'deny', 'ignore']);
for (const key of Object.keys(policy)) {
if (!known.has(key)) {
throw new TypeError(
`authorizer() received unknown option '${key}'`,
);
}
}
const decisions = new Set(['allow', 'deny', 'ignore']);
const decisionOf = /** @type {Record<string, number>} */ ({
allow: sqlite3.OK,
deny: sqlite3.DENY,
ignore: sqlite3.IGNORE,
});
const fallback = policy.default === undefined ? 'allow' : policy.default;
if (!decisions.has(fallback)) {
throw new TypeError(
"authorizer() default must be 'allow', 'deny' or 'ignore'",
);
}
/**
* Normalizes one rule list into native rows
* [action, verdict, arg1, arg2, database, trigger].
*
* @param {unknown} rules the raw rule list.
* @param {string} verdict the list's decision name ('allow' etc).
* @param {string} who the list's name in the policy, for error messages.
* @returns {unknown[][]} the native rule rows.
*/
const normalize = (rules, verdict, who) => {
if (rules === undefined || rules === null) return [];
if (!Array.isArray(rules)) {
throw new TypeError(
`authorizer() '${who}' must be an array of rules`,
);
}
return rules.map((rule, i) => {
if (
rule === null ||
typeof rule !== 'object' ||
Array.isArray(rule)
) {
throw new TypeError(
`authorizer() ${who}[${i}] must be a rule object`,
);
}
const where = `authorizer() ${who}[${i}]`;
// null = match anything; an explicit '' targets an empty
// argument (previously unexpressible — D08 closes the D07
// finding).
const row = /** @type {(number | string | null)[]} */ ([
-1,
decisionOf[verdict],
null,
null,
null,
null,
]);
if (rule.action !== undefined) {
if (
typeof rule.action !== 'number' ||
!Number.isInteger(rule.action)
) {
throw new TypeError(
`${where} action must be an integer constant`,
);
}
row[0] = rule.action;
}
const arg1 = rule.arg1 !== undefined ? rule.arg1 : rule.table;
const arg2 = rule.arg2 !== undefined ? rule.arg2 : rule.column;
const parts = [
[arg1, 'arg1'],
[arg2, 'arg2'],
[rule.database, 'database'],
[rule.trigger, 'trigger'],
];
parts.forEach((part, j) => {
const value = part[0];
const name = /** @type {string} */ (part[1]);
if (value === undefined || value === null) return;
if (typeof value !== 'string') {
throw new TypeError(`${where} ${name} must be a string`);
}
row[2 + j] = value;
});
return row;
});
};
// Deny first: the sandbox reading — a deny must not be rescuable by a
// later allow, whatever the array order.
const rows = [
...normalize(policy.deny, 'deny', 'deny'),
...normalize(policy.ignore, 'ignore', 'ignore'),
...normalize(policy.allow, 'allow', 'allow'),
];
this._drainStatementCache();
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._setAuthorizer)
)(decisionOf[fallback], rows);
return this;
};
/**
* Installs a progress handler. Two forms:
*
* - `db.progress(period, callback)` — a JavaScript callback invoked every
* `period` VM instructions; returning truthy aborts the running
* statement with `SQLITE_INTERRUPT`. Each invocation is a blocking
* round trip to the JS thread from whatever thread is executing SQL,
* so it is the expensive form: fine for progress bars over a handful
* of long queries, wrong for anything per-row. While it is installed,
* the synchronous methods (`getSync`/`runSync`/`allSync` and
* `prepareSync`) refuse to run — the callback could fire on the thread
* that would have to service it.
* - `db.cancellationToken()` — the recommended form; see there.
*
* Calling `db.progress()` with no callback removes the handler.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {number | (() => unknown)} [period] VM instructions between
* invocations (default 1000), or the callback directly.
* @param {() => unknown} [callback] called with no arguments; a truthy
* return aborts the statement.
* @returns {import('./sqlite3-binding.js').Database} this database, for chaining.
* @throws {TypeError} when the period or callback has the wrong type.
* @since 9.0.0
* @example
* db.progress(10000, () => shouldStop);
*/
Database.prototype.progress = function (period, callback) {
if (typeof period === 'function' && callback === undefined) {
callback = period;
period = 1000;
}
if (callback === undefined || callback === null) {
// Also the documented removal form: db.progress().
progressOwner.delete(this);
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._progressCallback)
)();
return this;
}
if (typeof period !== 'number' || !Number.isInteger(period) || period < 1) {
throw new TypeError('progress() period must be a positive integer');
}
if (typeof callback !== 'function') {
throw new TypeError('progress() callback must be a function');
}
// The callback form takes the slot from any token that held it.
progressOwner.delete(this);
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._progressCallback)
)(period, callback);
return this;
};
// SQLite has exactly one progress-handler slot per connection, but three
// things claim it: cancellationToken(), progress(fn) and progress()'s
// removal form. Whoever claims it last owns it, and only the owner may
// release it — otherwise a stale token's destroy() silently disarms
// whatever replaced it, and a cancel() that should abort a runaway query
// does nothing (leaving the connection wedged on it).
/** @type {WeakMap<object, object>} */
const progressOwner = new WeakMap();
/**
* Creates a {@link CancellationToken} for this connection. The flag lives
* in a `SharedArrayBuffer`, so `cancel()` works from any thread — hand
* the token's `signal` or the buffer itself to a `worker_threads` Worker
* and it can stop a query running on the main connection.
*
* The handler is installed until `token.destroy()` or the connection
* closes; while it is installed every query pays one relaxed atomic load
* per `period` VM instructions (measured in bench: within noise for the
* default period of 1000).
*
* A connection has one progress slot: creating a second token, or
* calling {@link Database#progress}, replaces the first. `destroy()` on
* a token that no longer owns the slot only clears its own flag.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {{ period?: number }} [options] `period`: VM instructions
* between flag checks (default 1000; lower aborts sooner and costs
* proportionally more).
* @returns {import('./native.js').CancellationToken} the token.
* @throws {TypeError} when the period is not a positive integer.
* @since 9.0.0
* @example
* const token = db.cancellationToken();
* db.all('WITH RECURSIVE ...', () => {}).catch(() => {});
* token.cancel();
*/
Database.prototype.cancellationToken = function (options) {
const period = options?.period ?? 1000;
if (typeof period !== 'number' || !Number.isInteger(period) || period < 1) {
throw new TypeError(
'cancellationToken() period must be a positive integer',
);
}
const sab = new SharedArrayBuffer(4);
const flag = new Int32Array(sab);
const controller = new AbortController();
/** @type {import('./sqlite3-binding.js').Database} */
const db = this;
/** @type {boolean} */
let cancelled = false;
/** @type {import('./native.js').CancellationToken} */
const token = {
get cancelled() {
return Atomics.load(flag, 0) !== 0;
},
get signal() {
return controller.signal;
},
get buffer() {
return sab;
},
cancel(reason) {
if (cancelled) return;
cancelled = true;
Atomics.store(flag, 0, 1);
controller.abort(reason);
},
reset() {
cancelled = false;
Atomics.store(flag, 0, 0);
},
destroy() {
Atomics.store(flag, 0, 0);
// Only the current owner may release the slot; see
// progressOwner above.
if (progressOwner.get(db) !== token) return;
progressOwner.delete(db);
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (db._progressFlag)
)();
},
};
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._progressFlag)
)(flag, period);
progressOwner.set(this, token);
return token;
};
/**
* Runs a WAL checkpoint on this connection.
*
* The result reports `busy` (another connection's reader or writer
* prevented the checkpoint), `logFrames` (frames in the WAL) and
* `checkpointedFrames` (frames copied back into the database). This is
* the lever for keeping a WAL file bounded under sustained writes; see
* also the `'wal'` event for a per-commit notification.
*
* In callback mode returns this database; otherwise returns a promise
* resolving the result.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {import('./native.js').CheckpointOptions | import('./native.js').CheckpointMode | string | ((err: import('./native.js').SqliteError | null, result: import('./native.js').CheckpointResult) => void)} [options] the
* options object, or just the mode, or just the attached database
* name, or just the callback.
* @param {(err: import('./native.js').SqliteError | null, result: import('./native.js').CheckpointResult) => void} [callback]
* called with the checkpoint result.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core).
* @throws {TypeError} when the mode is unknown.
* @since 9.0.0
* @example
* await db.checkpoint({ mode: 'truncate' });
*/
Database.prototype.checkpoint = function (options, callback) {
/** @type {string} */
let dbName = 'main';
/** @type {string} */
let mode = 'passive';
if (typeof options === 'function') {
callback = options;
options = undefined;
} else if (typeof options === 'string') {
mode = options;
} else if (options !== undefined && options !== null) {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('checkpoint() options must be an object');
}
if (options.db !== undefined) {
if (typeof options.db !== 'string') {
throw new TypeError(
"checkpoint() option 'db' must be a string",
);
}
dbName = options.db;
}
if (options.mode !== undefined) {
mode = options.mode;
}
}
const modes = /** @type {Record<string, number>} */ ({
passive: sqlite3.CHECKPOINT_PASSIVE,
full: sqlite3.CHECKPOINT_FULL,
restart: sqlite3.CHECKPOINT_RESTART,
truncate: sqlite3.CHECKPOINT_TRUNCATE,
});
const modeInt = modes[mode];
if (modeInt === undefined) {
throw new TypeError(
"checkpoint() mode must be 'passive', 'full', 'restart' or 'truncate'",
);
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._checkpoint)
)(dbName, modeInt, callback);
return this;
};
/**
* Reads one table's column metadata (`PRAGMA table_info` enriched with
* `sqlite3_table_column_metadata`). Runs a `PRAGMA` on the connection, so
* an installed deny-by-default authorizer must allow `sqlite3.PRAGMA`.
*
* In callback mode returns this database; the promise layer rewraps the
* core so promise mode resolves the column array. An empty array means
* the table has no columns or does not exist.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {string} table the table name.
* @param {string | ((err: import('./native.js').SqliteError | null, columns: import('./native.js').TableColumnInfo[]) => void)} [dbName]
* the attached database (default `'main'`), or the callback.
* @param {(err: import('./native.js').SqliteError | null, columns: import('./native.js').TableColumnInfo[]) => void} [callback]
* called with the column array.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core).
* @throws {TypeError} when the table name is missing.
* @since 9.0.0
* @example
* const columns = await db.tableInfo('users');
*/
Database.prototype.tableInfo = function (table, dbName, callback) {
if (typeof table !== 'string' || table.length === 0) {
throw new TypeError('tableInfo() requires a non-empty table name');
}
if (typeof dbName === 'function') {
callback = dbName;
dbName = 'main';
}
if (dbName !== undefined && dbName !== null && typeof dbName !== 'string') {
throw new TypeError('tableInfo() database name must be a string');
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._tableInfo)
)(dbName ?? 'main', table, callback);
return this;
};
/**
* Reads or changes one of the safe `sqlite3_db_config` switches:
* `sqlite3.DBCONFIG_ENABLE_FKEY`, `_ENABLE_TRIGGER`, `_ENABLE_VIEW`,
* `_ENABLE_LOAD_EXTENSION`, `_DEFENSIVE`, `_WRITABLE_SCHEMA` and
* `_TRUSTED_SCHEMA`.
*
* Passing `true`/`false` (or 1/0) sets the switch and the promise resolves
* its previous value; passing `-1` (or omitting the value) only reads it.
*
* In callback mode returns this database; the promise layer rewraps the
* core so promise mode resolves the resulting boolean.
*
* @this {import('./sqlite3-binding.js').Database}
* @param {number} op one of the `DBCONFIG_*` constants.
* @param {boolean | number | ((err: import('./native.js').SqliteError | null, value: boolean) => void)} [value]
* true/false to set, -1 to query, or the callback.
* @param {(err: import('./native.js').SqliteError | null, value: boolean) => void} [callback]
* called with the previous value.
* @returns {any} this database in callback mode (the promise layer
* rewraps the core).
* @throws {TypeError} when the op is not a known DBCONFIG constant or the
* value is invalid.
* @since 9.0.0
* @example
* const was = await db.dbConfig(sqlite3.DBCONFIG_DEFENSIVE, true);
*/
Database.prototype.dbConfig = function (op, value, callback) {
if (typeof value === 'function') {
callback = value;
value = -1;
}
/** @type {Set<number>} */
const known = new Set([
sqlite3.DBCONFIG_ENABLE_FKEY,
sqlite3.DBCONFIG_ENABLE_TRIGGER,
sqlite3.DBCONFIG_ENABLE_VIEW,
sqlite3.DBCONFIG_ENABLE_LOAD_EXTENSION,
sqlite3.DBCONFIG_DEFENSIVE,
sqlite3.DBCONFIG_WRITABLE_SCHEMA,
sqlite3.DBCONFIG_TRUSTED_SCHEMA,
]);
if (typeof op !== 'number' || !known.has(op)) {
throw new TypeError(
'dbConfig() op must be one of the DBCONFIG_* constants',
);
}
/** @type {number} */
let valueInt;
if (value === undefined || value === null || value === -1) {
valueInt = -1;
} else if (value === true) {
valueInt = 1;
} else if (value === false) {
valueInt = 0;
} else if (typeof value === 'number' && (value === 0 || value === 1)) {
valueInt = value;
} else {
throw new TypeError('dbConfig() value must be a boolean or -1');
}
/** @type {(...args: unknown[]) => unknown} */ (
/** @type {unknown} */ (this._dbConfig)
)(op, valueInt, callback);
return this;
};
/**
* Validates the shared function/aggregate options and computes the SQLite
* arity and flag word.
*
* @param {string} name the requested SQL name.
* @param {unknown} options the raw options object (or undefined).
* @param {number} defaultArity the arity derived from the implementation.
* @param {string} who the calling method, for error messages.
* @param {string[]} [extraKeys] option keys owned by the caller (the
* aggregate implementation functions).
* @returns {{ nArg: number, flags: number }} the arity and flag word.
* @throws {TypeError} on an invalid name or option.
* @private
*/
function parseFunctionOptions(name, options, defaultArity, who, extraKeys) {
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError(`${who} requires a non-empty name string`);
}
if (Buffer.byteLength(name, 'utf8') > MAX_FUNCTION_NAME) {
throw new TypeError(
`${who} name exceeds SQLite's ${MAX_FUNCTION_NAME}-byte limit`,
);
}
const opts = options === undefined || options === null ? {} : options;
if (typeof opts !== 'object' || Array.isArray(opts)) {
throw new TypeError(`${who} options must be an object`);
}
const known = new Set([
'deterministic',
'directOnly',
'innocuous',
'varargs',
...(extraKeys ?? []),
]);
for (const key of Object.keys(opts)) {
if (!known.has(key)) {
throw new TypeError(`${who} received unknown option '${key}'`);
}
}
// Only the flag keys are boolean-typed; the aggregate implementation
// keys are functions validated by the caller.
const flagKeys = /** @type {string[]} */ ([
'deterministic',
'directOnly',
'innocuous',
'varargs',
]);
for (const key of flagKeys) {
const value = /** @type {Record<string, unknown>} */ (opts)[key];
if (value !== undefined && typeof value !== 'boolean') {
throw new TypeError(`${who} option '${key}' must be a boolean`);
}
}
/**
* @param {string} key
*/
const get = (key) =>
/** @type {Record<string, unknown>} */ (opts)[key] === true;
let nArg;
if (get('varargs')) {
nArg = -1;
} else {
nArg = defaultArity;
if (nArg > MAX_FUNCTION_ARG) {
throw new TypeError(
`${who} arity ${nArg} exceeds SQLite's ` +
`${MAX_FUNCTION_ARG}-argument limit; use { varargs: true }`,
);
}
}
// directOnly defaults to true: the security posture that keeps a JS
// callback from being invoked through a trigger, view or CHECK
// constraint in attacker-supplied schema SQL. Opting out is explicit.
const flags =
(get('deterministic') ? SQLITE_DETERMINISTIC : 0) |
(opts &&
/** @type {Record<string, unknown>} */ (opts).directOnly === false
? 0
: SQLITE_DIRECTONLY) |
(get('innocuous') ? SQLITE_INNOCUOUS : 0);
return { nArg, flags };
}
sqlite3.cached = {
/**
* Opens a connection, or reuses the one already open for the
* resolved path.
*
* @param {string} file the database filename.
* @param {number | ((this: import('./sqlite3-binding.js').Database, err: Error | null) => void)} [a] open mode, or the callback.
* @param {(this: import('./sqlite3-binding.js').Database, err: Error | null) => void} [b] the callback when a mode was given.
* @returns {import('./sqlite3-binding.js').Database} the connection.
*/
Database: function (file, a, b) {
/** @type {any} */
const modeOrCallback = a;
/** @type {any} */
const callback = b;
if (file === '' || file === ':memory:') {
// Don't cache special databases.
return new Database(file, modeOrCallback, callback);
}
/** @type {import('./sqlite3-binding.js').Database} */
let db;
file = path.resolve(file);
if (!sqlite3.cached.objects[file]) {
db = sqlite3.cached.objects[file] = new Database(
file,
modeOrCallback,
callback,
);
} else {
// Make sure the callback is called.
db = sqlite3.cached.objects[file];
const callback = typeof a === 'number' ? b : a;
if (typeof callback === 'function') {
const cb = () => callback.call(db, null);
if (db.open) process.nextTick(cb);
else db.once('open', cb);
}
}
return db;
},
objects: {},
};
// Database#backup (the guarded definition) lives with the other
// Deliverable 11 wrappers above, before installPromiseApi runs.
/**
* Maps rows by their first column via `all`, then reshapes the result.
*
* With two columns the value is the second column; with any other count
* (including a single column) the value is the whole row — the
* single-column case used to yield `undefined` for every entry
* (REVIEW-LOG, D03).
*
* @this {import('./sqlite3-binding.js').Statement}
* @param {...any} params bind parameters, then the callback.
* @returns {any} this statement in callback mode, a promise otherwise.
*/
Statement.prototype.map = function (...params) {
const popped = params.pop();
const callback =
/** @type {(err: Error | null, map?: Record<string, unknown>) => void} */ (
/** @type {unknown} */ (popped)
);
/**
* @param {Error | null} err
* @param {Record<string, unknown>[] | null} rows
*/
const reshape = (err, rows) => {
// An error means there are no rows to reshape: hand the caller the
// error alone, exactly as the callback-mode contract has always
// done (a second argument here would be a fake empty result).
if (err) return callback(err, undefined);
/** @type {Record<string, unknown>} */
const result = {};
if (rows?.length) {
const keys = Object.keys(rows[0]);
const key = keys[0];
if (keys.length > 2) {
// Value is an object
for (let i = 0; i < rows.length; i++) {
result[/** @type {string} */ (rows[i][key])] = rows[i];
}
} else if (keys.length === 2) {
const value = keys[1];
// Value is a plain value
for (let i = 0; i < rows.length; i++) {
result[/** @type {string} */ (rows[i][key])] =
rows[i][value];
}
} else {
// Single column: the key column is the only data there is,
// so the value is the whole row (same rule as >2). Used to
// return `undefined` for every entry (REVIEW-LOG, D03).
for (let i = 0; i < rows.length; i++) {
result[/** @type {string} */ (rows[i][key])] = rows[i];
}
}
}
callback(err, result);
};
params.push(reshape);
return /** @type {(...args: any[]) => any} */ (this.all)(...params);
};
let isVerbose = false;
const supportedEvents = new Set([
'trace',
'profile',
'change',
'commit',
'rollback',
'wal',
// Deliverable 08. Shares SQLite's single preupdate hook with the
// session extension; the registration fails loudly while a session
// is open, and db.session() throws while a listener is registered.
'preupdate',
]);
// --- diagnostics_channel (Phase 6) -------------------------------------------
//
// Node's own node:sqlite publishes finished-statement spans on the
// 'sqlite.db.query' channel when it has subscribers; APM tooling can
// subscribe to that name. This package publishes the same payload shape
// there AND on '@appthreat/sqlite3.query' (the named channel is this
// package's documented surface; the sqlite.db.query mirror exists for
// tool compatibility). The SQLITE_TRACE_PROFILE machinery is armed only
// while a subscriber exists, so the cost when nobody listens is nothing
// at all: no hook, no per-query work.
const DIAGNOSTIC_OWN_CHANNEL = '@appthreat/sqlite3.query';
const DIAGNOSTIC_NODE_CHANNEL = 'sqlite.db.query';
// Every open connection, so a subscriber arriving late can arm tracing on
// connections that already exist. Maintained by the Database wrapper
// (constructor adds, close removes) — and held *weakly*: a connection
// dropped without close() must still be collectable, or this registry
// would pin every Database (and its sqlite handle and file descriptor)
// for the life of the process. Dead references are pruned on iteration.
/** @type {Set<WeakRef<import('./sqlite3-binding.js').Database>>} */
const liveConnections = new Set();
/**
* Registers a connection with the diagnostics registry.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {void}
* @private
*/
function trackConnection(db) {
liveConnections.add(new WeakRef(db));
}
/**
* Drops a connection from the diagnostics registry (close()), pruning
* collected entries while it walks.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {void}
* @private
*/
function untrackConnection(db) {
for (const ref of liveConnections) {
const live = ref.deref();
if (live === undefined || live === db) liveConnections.delete(ref);
}
}
/**
* The live connections, pruning collected entries as it goes.
*
* @returns {import('./sqlite3-binding.js').Database[]} the connections.
* @private
*/
function trackedConnections() {
/** @type {import('./sqlite3-binding.js').Database[]} */
const live = [];
for (const ref of liveConnections) {
const db = ref.deref();
if (db === undefined) liveConnections.delete(ref);
else live.push(db);
}
return live;
}
/**
* One finished-statement span, published on the diagnostics channels.
*
* @typedef {object} QuerySpan
* @property {string} sql the expanded SQL text.
* @property {import('./sqlite3-binding.js').Database} database the connection.
* @property {bigint} duration the measured duration in nanoseconds.
* @property {number} durationMs the measured duration in milliseconds.
* @since 9.1.0
*/
// The sqlite3.subscribeQueries() consumers (Phase 6): non-empty means the
// profile tracing is armed.
/** @type {Set<(message: QuerySpan) => void>} */
const diagnosticsChannelSubscribers = new Set();
/**
* The internal profile listener forwarding one connection's finished
* statements into the channels. Kept as one shared function so it can be
* removed again on unsubscribe.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @param {string} sql the expanded SQL text.
* @param {number} ms the measured duration in milliseconds.
*/
function publishQuerySpan(db, sql, ms) {
const message = {
sql,
database: db,
duration: BigInt(Math.round(ms * 1e6)),
durationMs: ms,
};
for (const onMessage of diagnosticsChannelSubscribers) {
try {
onMessage(/** @type {QuerySpan} */ (message));
} catch {
// A throwing consumer must not break the query pipeline.
}
}
diagnostics_channel.channel(DIAGNOSTIC_OWN_CHANNEL).publish(message);
diagnostics_channel.channel(DIAGNOSTIC_NODE_CHANNEL).publish(message);
}
// The listener this module installed on each armed connection, so arming
// and disarming touch exactly that one: a user's own 'profile' listener
// must neither block publication (the old `listenerCount === 0` test made
// subscribeQueries silently inert next to one) nor be removed by an
// unsubscribe (removeAllListeners took them with it).
/** @type {WeakMap<object, (sql: string, ms: number) => void>} */
const armedProfileListeners = new WeakMap();
/**
* Arms span publication on one connection (the per-connection half of
* {@link armDiagnostics}).
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {void}
* @private
*/
function armDiagnosticsFor(db) {
if (armedProfileListeners.has(db)) return;
/** @type {(sql: string, ms: number) => void} */
const listener = (sql, ms) => publishQuerySpan(db, sql, ms);
armedProfileListeners.set(db, listener);
// The user may already have a 'profile' listener (and therefore
// tracing already on); adding ours is additive, and configure() is
// idempotent.
db.on('profile', listener);
db.configure('profile', true);
}
/**
* Disarms span publication on one connection, leaving any listener the
* user registered — and the tracing it needs — in place.
*
* @param {import('./sqlite3-binding.js').Database} db the connection.
* @returns {void}
* @private
*/
function disarmDiagnosticsFor(db) {
const listener = armedProfileListeners.get(db);
if (listener === undefined) return;
armedProfileListeners.delete(db);
db.removeListener('profile', listener);
// Only stop tracing when nobody else is listening for it.
if (db.listenerCount('profile') === 0) db.configure('profile', false);
}
/**
* Arms or disarms span publication on every live connection.
*
* @param {boolean} arm true to arm, false to disarm.
* @returns {void}
* @private
*/
function armDiagnostics(arm) {
for (const db of trackedConnections()) {
if (arm) armDiagnosticsFor(db);
else disarmDiagnosticsFor(db);
}
}
/**
* Delivers the spans already recorded but not yet dispatched, to every
* current subscriber, synchronously.
*
* SQLite reports a finished statement's timing on the thread that ran it
* — before that statement's own completion reaches JS — and the timing
* crosses to the JS thread through a queue drained on a later loop turn,
* so it arrives after the query it belongs to has resolved. Draining is
* how a caller reads it at a known moment.
*
* @returns {void}
* @private
*/
function flushQuerySpansNow() {
for (const db of trackedConnections()) {
if (!armedProfileListeners.has(db)) continue;
try {
/** @type {any} */ (db)._flushProfile();
} catch {
// A connection closing underneath the drain is not a failure
// worth propagating out of a flush.
}
}
}
/**
* Subscribes to query spans: every finished statement is published as
* `{ sql, database, duration (bigint ns), durationMs }` on the
* `@appthreat/sqlite3.query` diagnostics channel (and mirrored onto
* node:sqlite's `sqlite.db.query` channel name, for APM-tool
* compatibility). The underlying SQLITE_TRACE_PROFILE tracing is armed by
* the first subscriber and disarmed when the last one goes — nothing runs
* while nobody listens.
*
* **Delivery is asynchronous.** SQLite reports a statement's timing on
* the thread that executed it, and the span reaches the JS thread through
* a queue drained on a later event-loop turn — so immediately after
* `await db.all(sql)` the span for that query has usually not been
* delivered yet. Await a macrotask (`setImmediate`), or call
* {@link sqlite3.flushQuerySpans} to deliver what is pending right now,
* before asserting on or flushing collected spans. The unsubscribe
* function returned here drains first, so nothing recorded before it is
* lost.
*
* Returns the unsubscribe function. Connections opened while a
* subscription is active publish their spans too.
*
* @param {(message: { sql: string, database: import('./sqlite3-binding.js').Database, duration: bigint, durationMs: number }) => void} onMessage
* the span consumer.
* @returns {() => void} the unsubscribe function.
* @since 9.1.0
* @example
* const unsubscribe = sqlite3.subscribeQueries(({ sql, durationMs }) =>
* console.log(sql, durationMs.toFixed(3)));
* await db.all('SELECT 1');
* sqlite3.flushQuerySpans(); // the span for that query is delivered now
* unsubscribe();
*/
sqlite3.subscribeQueries = function subscribeQueries(onMessage) {
if (typeof onMessage !== 'function') {
throw new TypeError('subscribeQueries() requires a listener function');
}
diagnosticsChannelSubscribers.add(onMessage);
armDiagnostics(true);
return () => {
// Drain before dropping the subscriber: the spans of queries it
// already awaited are sitting in the queue, and disarming would
// otherwise deliver them after it stopped listening.
flushQuerySpansNow();
diagnosticsChannelSubscribers.delete(onMessage);
if (diagnosticsChannelSubscribers.size === 0) {
armDiagnostics(false);
}
};
};
/**
* Delivers every query span recorded but not yet dispatched,
* synchronously, to the current subscribers and channels.
*
* Spans are inherently asynchronous (see {@link sqlite3.subscribeQueries}):
* this is the drain point for code that must read them at a known moment
* — a test asserting on the span of a query it just awaited, or a
* shutdown path flushing to an APM sink. A no-op when nothing is
* subscribed.
*
* @returns {void}
* @since 9.1.0
* @example
* const spans = [];
* const unsubscribe = sqlite3.subscribeQueries((s) => spans.push(s.sql));
* await db.all('SELECT 1');
* sqlite3.flushQuerySpans();
* // spans now contains 'SELECT 1'
* unsubscribe();
*/
sqlite3.flushQuerySpans = function flushQuerySpans() {
flushQuerySpansNow();
};
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {string} type
* @param {...any} args
* @returns {any}
*/
Database.prototype.addListener = Database.prototype.on = function (
type,
...args
) {
const val = /** @type {(...callArgs: any[]) => any} */ (
EventEmitter.prototype.addListener
).call(this, type, ...args);
if (supportedEvents.has(type)) {
this.configure(
/** @type {'trace' | 'profile' | 'change' | 'commit' | 'rollback' | 'wal'} */ (
type
),
true,
);
}
return val;
};
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {string} type
* @param {...any} args
* @returns {any}
*/
Database.prototype.removeListener = function (type, ...args) {
const val = /** @type {(...callArgs: any[]) => any} */ (
EventEmitter.prototype.removeListener
).call(this, type, ...args);
if (supportedEvents.has(type) && !this.listenerCount(type)) {
this.configure(
/** @type {'trace' | 'profile' | 'change' | 'commit' | 'rollback' | 'wal'} */ (
type
),
false,
);
}
return val;
};
/**
* @this {import('./sqlite3-binding.js').Database}
* @param {string} type
* @param {...any} args
* @returns {any}
*/
Database.prototype.removeAllListeners = function (type, ...args) {
const val = /** @type {(...callArgs: any[]) => any} */ (
EventEmitter.prototype.removeAllListeners
).call(this, type, ...args);
if (supportedEvents.has(type)) {
this.configure(
/** @type {'trace' | 'profile' | 'change' | 'commit' | 'rollback' | 'wal'} */ (
type
),
false,
);
}
return val;
};
/**
* Enables long stack traces for every method: errors delivered to
* callbacks (and promise rejections) carry the call site's stack,
* filtered of driver frames.
*
* Irreversible for the process — once on, always on — and global: it
* wraps the method cores, so every connection created afterwards is
* traced too.
*
* @returns {sqlite3} the same namespace, for chaining.
*/
sqlite3.verbose = function () {
if (!isVerbose) {
// Dual-mode methods are traced at their callback cores and the
// promise wrappers are then rebuilt around the traced cores, so a
// promise rejection carries the same augmented stack as a callback
// error. (Wrapping the dual-mode wrapper itself would see no
// trailing function in promise mode and capture nothing.)
retracePromiseApi(extendTrace);
// prepare keeps its synchronous, non-dual contract; trace it on the
// prototype as before.
extendTrace(
/** @type {Record<string, import('./trace.js').Traceable>} */ (
/** @type {unknown} */ (Database.prototype)
),
'prepare',
);
isVerbose = true;
}
return sqlite3;
};
// The worker-thread pool (Deliverable 09): opt-in, promise-only, and
// documented in lib/pool.js and docs/concurrency.md. A single Database
// remains the primary object; the pool is for moving all SQLite work
// off the main thread.
sqlite3.pool = pool;
// Promise API, async iteration, transactions and disposal — installed after
// every callback-mode method above is final.
installPromiseApi(sqlite3);
export default sqlite3;
export { Backup, Blob, Session, Statement } from './sqlite3-binding.js';
// Database is the v9 wrapper (lib/sqlite3.js) — a real subclass of the
// native class carrying the permission-model checks; the other classes
// pass through unchanged.
export { DatabaseClass as Database };