// 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} 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, * pool: typeof import('./pool.js').pool, * iterdump: (db: import('./sqlite3-binding.js').Database) => AsyncGenerator, * 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} allow allowed extension paths (as written). * @private */ /** @type {WeakMap} */ 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} */ (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} */ (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} 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} */ (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} 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} the thenable wrapper. * @private */ function prepareThenableWrapper(statement, ready, addAwaiter) { /** @type {import('./sqlite3-binding.js').Statement & Promise} */ let wrapper; wrapper = /** @type {import('./sqlite3-binding.js').Statement & Promise} */ ( /** @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} 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} 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} */ 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} */ 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} */ (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} 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[] | 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[]} */ ( 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[]>} 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[]>} */ ( 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} 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} 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} 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 `_*` 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} */ (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} 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} * @since 9.1.0 */ Object.defineProperty(Database.prototype, 'limits', { get() { /** @type {Record} */ 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} */ 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>} 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 void }>} byName the handles. */ /** @type {WeakMap} */ 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} 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} */ ({ 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} */ 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} */ ({ 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} */ 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} */ (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} */ (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} */ (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) => void} */ ( /** @type {unknown} */ (popped) ); /** * @param {Error | null} err * @param {Record[] | 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} */ 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>} */ 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 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} */ ( /** @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 };