Files
SnowLuma/.pnpm-store/v11/files/33/852e036343bf1399dd3abfc5471d875d57d89c9f5db1f23847702f34a071a67dbd17dd8a02d4281b0d495621231211eef41f70c5608ba846e84b594bed9568
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Skipped
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

707 lines
24 KiB
Plaintext

// The node:sqlite compatibility shim (Phase 5):
// `import { DatabaseSync } from '@appthreat/sqlite3/compat'`.
//
// A zero-dependency drop-in for code written against Node's built-in
// node:sqlite that outgrew it — when the connection is idle the mapped
// calls run on this package's synchronous fast path (parity with
// node:sqlite's speed), and the moment the application needs pools,
// sessions, streaming or cancellation it can reach through `db.native`
// for the full async surface without changing databases.
//
// What maps 1:1: open/close/isOpen/isTransaction, exec (synchronous
// multi-statement scripts), prepare + StatementSync get/all/run/iterate/
// columns/sourceSQL/expandedSQL, function/aggregate (the Phase 2
// re-entrant UDFs make these genuinely synchronous), loadExtension/
// enableLoadExtension, enableDefensive, location, prepare options
// (readBigInts, returnArrays, allowBareNamedParameters,
// allowUnknownNamedParameters, persistent — accepted, some as no-ops).
//
// Documented divergences (loud, not silent):
// - setAuthorizer() throws: this package's authorizer is a declarative
// C++ rule list by design (no JavaScript on the prepare path); reach
// through `db.native.authorizer(policy)` for the supported form.
// - createSession()/applyChangeset() keep this package's async
// signatures (they return promises) — node:sqlite's sync ones have no
// equivalent on the sync fast path.
// - serialize() returns the bytes asynchronously.
// - StatementSync.close() finalizes asynchronously under the hood; the
// statement is unusable immediately after. DatabaseSync.close() is the
// same shape: it finalizes the statements prepared through it (as
// node:sqlite does) and starts the close — the connection is unusable
// at once, the handle is released on the queue. Use `await db[Symbol.
// asyncDispose]()`, or `await db.native.close()`, when the close must
// have completed (deleting the file, reopening it on Windows). A close
// that fails anyway — a statement prepared directly on `db.native`
// holds the connection — is reported on the connection's 'error'
// event, never discarded.
// - StatementSync.iterate() materialises the rows before yielding: the
// sync fast path has no mid-cursor suspension. Use
// `db.native.iterate()` for a true streaming cursor.
// - setReadBigInts() after prepare() applies to lastInsertRowid;
// integer *columns* are read in the mode fixed at prepare time, so
// pass `{ readBigInts: true }` to prepare() (or re-prepare) to change
// how rows are read.
import sqlite3 from './sqlite3.js';
/**
* Splits one SQL script into complete statements, using sqlite3_complete
* (via the binding's `complete()`) to find statement boundaries — quotes,
// comments and trigger bodies included. The trailing remainder (an
* incomplete final statement) is kept and will fail loudly on run.
*
* @param {string} sql the script.
* @returns {string[]} the complete statements.
* @private
*/
function splitScript(sql) {
/** @type {string[]} */
const statements = [];
let current = '';
let i = 0;
while (i < sql.length) {
const ch = sql[i];
current += ch;
i++;
if (ch === ';') {
const candidate = current.trim();
if (candidate.length > 0 && sqlite3.complete(candidate)) {
statements.push(candidate);
current = '';
}
}
}
const rest = current.trim();
if (rest.length > 0) {
// An unterminated tail: run it through complete() to fail with
// sqlite's own message rather than dropping it silently.
statements.push(rest);
}
return statements;
}
/**
* A prepared statement in the node:sqlite shape, wrapping this package's
* synchronous fast path.
*
* @since 9.1.0
*/
class StatementSync {
/** @type {import('./sqlite3-binding.js').Statement} */
#stmt;
#readBigInts;
#returnArrays;
/** @type {(mode: 'number' | 'bigint') => import('./sqlite3-binding.js').Statement} */
#reprepare;
/** @type {(stmt: StatementSync) => void} */
#onClose;
/**
* @param {import('./sqlite3-binding.js').Statement} stmt the wrapped statement.
* @param {{ readBigInts?: boolean, returnArrays?: boolean }} options
* @param {(mode: 'number' | 'bigint') => import('./sqlite3-binding.js').Statement} [reprepare]
* re-prepares the same SQL under another integer mode, for
* setReadBigInts (the mode belongs to the prepared statement).
* @param {(stmt: StatementSync) => void} [onClose] tells the owning
* connection this statement no longer needs finalizing at close.
*/
constructor(stmt, options, reprepare, onClose) {
this.#stmt = stmt;
this.#readBigInts = options?.readBigInts === true;
this.#returnArrays = options?.returnArrays === true;
this.#reprepare =
reprepare ??
(() => {
throw new Error(
'setReadBigInts() cannot re-prepare this statement',
);
});
this.#onClose =
onClose ??
(() => {
// Unowned statement: nobody is tracking it for close.
});
}
/**
* True once finalized (explicitly or through dispose).
*
* @returns {boolean} finalized.
*/
get finalized() {
return this.#stmt.finalized;
}
/**
* The statement's SQL text.
*
* @returns {string} the SQL.
*/
get sourceSQL() {
return /** @type {string} */ (this.#stmt.sql);
}
/**
* The SQL with the most recent bound values substituted.
*
* @returns {string} the expanded SQL.
*/
get expandedSQL() {
return /** @type {string} */ (this.#stmt.expandedSQL);
}
/**
* The statement's result columns.
*
* @returns {{ name: string, database?: string, table?: string, column?: string, type?: string }[]}
* the column descriptors.
*/
columns() {
// node:sqlite names the origin column `column`; this package's
// native accessor calls it `origin`.
const native = /** @type {any[]} */ (this.#stmt.columns) ?? [];
return native.map(({ origin, ...rest }) =>
origin === undefined ? rest : { ...rest, column: origin },
);
}
/**
* The bind arguments for one call: node:sqlite re-executes a
* statement called again with no parameters (this package's statement
* cursor semantics otherwise continue), so an explicit empty bind
* forces the reset.
*
* @param {unknown[]} params
* @returns {unknown[]}
*/
#bindArgs(params) {
// Only a zero-parameter statement can be safely re-executed with
// an explicit empty bind (which forces the reset); a parameterful
// statement called without arguments keeps this package's
// re-bind-with-last-values semantics.
if (params.length === 0 && this.#stmt.parameterCount === 0) {
return [[]];
}
return params;
}
/**
* @param {unknown[]} params
* @returns {unknown[]}
*/
#rowModeOption(params) {
return this.#returnArrays ? [...params, { rowMode: 'array' }] : params;
}
/**
* Steps once and returns the first row (or undefined).
*
* @param {...unknown} params bind parameters.
* @returns {any} the row.
*/
get(...params) {
return this.#stmt.getSync(
...this.#rowModeOption(this.#bindArgs(params)),
);
}
/**
* Steps to completion and returns every row.
*
* @param {...unknown} params bind parameters.
* @returns {any[]} the rows.
*/
all(...params) {
return this.#stmt.allSync(
...this.#rowModeOption(this.#bindArgs(params)),
);
}
/**
* Runs the statement, returning `{ changes, lastInsertRowid }`.
*
* @param {...unknown} params bind parameters.
* @returns {{ changes: number | bigint, lastInsertRowid: number | bigint }} the run result.
*/
run(...params) {
this.#stmt.runSync(...this.#bindArgs(params));
return {
changes: this.#stmt.changes,
lastInsertRowid: this.#readBigInts
? this.#stmt.lastIDBigInt
: this.#stmt.lastID,
};
}
/**
* A synchronous iterator over the rows. Divergence: the rows are
* materialised first (the sync fast path has no mid-cursor
* suspension), so this costs the memory of the whole result — reach
* for `db.native.iterate()` when that matters.
*
* @param {...unknown} params bind parameters.
* @returns {IterableIterator<any>} the rows.
*/
*iterate(...params) {
yield* this.all(...params);
}
/**
* Sets whether integers read as BigInt (node:sqlite's toggle). The
* integer mode belongs to the prepared statement here, so this
* re-prepares the same SQL under the new mode — column values follow
* the setting, as they do in node:sqlite, rather than only
* `lastInsertRowid`.
*
* @param {boolean} value the new setting.
* @returns {void}
*/
setReadBigInts(value) {
const next = value === true;
if (next === this.#readBigInts) return;
const replacement = this.#reprepare(next ? 'bigint' : 'number');
const previous = this.#stmt;
this.#stmt = replacement;
this.#readBigInts = next;
if (!previous.finalized) {
previous.finalize(function () {
/* best effort: the replacement is live */
});
}
}
/**
* Sets whether rows are arrays (node:sqlite's toggle).
*
* @param {boolean} value the new setting.
* @returns {void}
*/
setReturnArrays(value) {
this.#returnArrays = value === true;
}
/**
* Finalizes the statement. Accepted for shape parity; the underlying
* finalize is asynchronous (queued), and the statement is unusable
* immediately.
*
* @returns {void}
*/
close() {
if (!this.#stmt.finalized) {
this.#stmt.finalize(function () {
/* best effort */
});
}
this.#onClose(this);
}
/**
* `using` support.
*
* @returns {void}
*/
[Symbol.dispose]() {
this.close();
}
}
/**
* A connection in the node:sqlite `DatabaseSync` shape, mapping onto this
* package's synchronous fast path.
*
* @since 9.1.0
* @example
* import { DatabaseSync } from '@appthreat/sqlite3/compat';
* const db = new DatabaseSync(':memory:');
* db.exec('CREATE TABLE t (a)');
* const stmt = db.prepare('SELECT * FROM t WHERE a = ?');
* const row = stmt.get(1);
* db.close();
*/
class DatabaseSync {
/** @type {import('./sqlite3-binding.js').Database} */
#db;
#allowExtension = false;
// Statements prepared through this connection and not yet closed.
// node:sqlite finalizes outstanding statements when the database
// closes, and code written against it relies on that: this package's
// close() instead fails with SQLITE_BUSY ("unable to close due to
// unfinalized statements") while one is live, so without this the
// connection would simply never close. Weakly held, because a
// statement nobody kept a reference to is finalized by GC on its own
// and pinning it here would be the leak the tracking is meant to
// avoid.
/** @type {Set<WeakRef<StatementSync>>} */
#statements = new Set();
/** @type {FinalizationRegistry<WeakRef<StatementSync>>} */
#collected = new FinalizationRegistry((ref) => {
this.#statements.delete(ref);
});
/**
* Opens a connection. Options map onto this package's opens and
* configure() calls: `readOnly`, `open` (default true),
* `enableForeignKeyConstraints` (default true, node:sqlite's own
* default), `enableDoubleQuotedStringLiterals` (refused: this
* package keeps strict SQL), `allowExtension`, `timeout`.
*
* @param {string} location the database filename or `:memory:`.
* @param {{ readOnly?: boolean, open?: boolean, enableForeignKeyConstraints?: boolean, allowExtension?: boolean, timeout?: number }} [options]
*/
constructor(location, options = {}) {
const known = new Set([
'readOnly',
'open',
'enableForeignKeyConstraints',
'enableDoubleQuotedStringLiterals',
'allowExtension',
'timeout',
]);
for (const key of Object.keys(options)) {
if (!known.has(key)) {
throw new TypeError(
`DatabaseSync received unknown option '${key}'`,
);
}
}
if (options.open === false) {
throw new TypeError(
"DatabaseSync option 'open: false' has no equivalent; " +
'open the connection when ready instead',
);
}
if (options.enableDoubleQuotedStringLiterals === true) {
throw new TypeError(
'DatabaseSync cannot enable double-quoted string ' +
'literals: this package deliberately keeps strict SQL',
);
}
const mode =
options.readOnly === true
? sqlite3.OPEN_READONLY
: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE;
// syncOpen: the native synchronous open (node:sqlite's semantics —
// the connection is usable the moment the constructor returns).
this.#db = new sqlite3.Database(location, { mode, syncOpen: true });
if (options.timeout !== undefined) {
this.#db.configure('busyTimeout', options.timeout);
}
this.#allowExtension = options.allowExtension === true;
if (options.enableForeignKeyConstraints !== false) {
this.#db.runSync('PRAGMA foreign_keys = ON');
}
}
/**
* The underlying connection, for reaching the full async surface
* (pools, sessions, streaming, cancellation) without a second open.
*
* @returns {import('./sqlite3-binding.js').Database} the connection.
*/
get native() {
return this.#db;
}
/**
* True while open.
*
* @returns {boolean} open.
*/
get open() {
return this.#db.open;
}
/**
* Alias of {@link DatabaseSync.open} (node:sqlite's name).
*
* @returns {boolean} open.
*/
get isOpen() {
return this.#db.open;
}
/**
* True inside an explicit transaction.
*
* @returns {boolean} in transaction.
*/
get isTransaction() {
return this.#db.inTransaction;
}
/**
* Runs a SQL script (possibly several statements) synchronously.
* Returns undefined, like node:sqlite's exec.
*
* @param {string} sql the script.
* @returns {undefined}
*/
exec(sql) {
if (typeof sql !== 'string') {
throw new TypeError('exec() requires a SQL string');
}
for (const statement of splitScript(sql)) {
this.#db.runSync(statement);
}
return undefined;
}
/**
* Prepares a statement synchronously.
*
* @param {string} sql the SQL.
* @param {{ readBigInts?: boolean, returnArrays?: boolean, allowBareNamedParameters?: boolean, allowUnknownNamedParameters?: boolean, persistent?: boolean }} [options]
* `readBigInts`/`returnArrays` map onto this package's per-call
* row shapes; the remaining options are accepted for shape parity
* (bare named parameters are always allowed here, unknown named
* parameters always refused — this package's strictness).
* @returns {StatementSync} the statement.
*/
prepare(sql, options = {}) {
const known = new Set([
'readBigInts',
'returnArrays',
'allowBareNamedParameters',
'allowUnknownNamedParameters',
'persistent',
]);
for (const key of Object.keys(options)) {
if (!known.has(key)) {
throw new TypeError(
`prepare() received unknown option '${key}'`,
);
}
}
/** @param {'number' | 'bigint'} [mode] */
const prepare = (mode) =>
this.#db.prepareSync(sql, { integerMode: mode });
const stmt = prepare(
options.readBigInts === true ? 'bigint' : undefined,
);
/** @type {StatementSync} */
const wrapper = new StatementSync(stmt, options, prepare, (closed) =>
this.#forget(closed),
);
const ref = new WeakRef(wrapper);
this.#statements.add(ref);
this.#collected.register(wrapper, ref, wrapper);
return wrapper;
}
/**
* Drops a statement from the close-time finalize list.
*
* @param {StatementSync} stmt the statement that closed itself.
* @returns {void}
*/
#forget(stmt) {
this.#collected.unregister(stmt);
for (const ref of this.#statements) {
const live = ref.deref();
if (live === undefined || live === stmt)
this.#statements.delete(ref);
}
}
/**
* Finalizes every statement still open on this connection, so that
* the close below is not refused with SQLITE_BUSY.
*
* @returns {void}
*/
#finalizeStatements() {
for (const ref of this.#statements) {
const stmt = ref.deref();
if (stmt !== undefined && !stmt.finalized) {
// One statement refusing to finalize must not strand the
// rest — or the close.
try {
stmt.close();
} catch {
/* keep going; close() reports what it cannot do */
}
}
}
this.#statements.clear();
}
/**
* Registers a scalar SQL function (synchronous, via the re-entrant
* direct-call path).
*
* @param {string} name the SQL name.
* @param {((...args: unknown[]) => unknown) | { deterministic?: boolean, directOnly?: boolean, varargs?: boolean }} [options]
* the options object, or the implementation directly.
* @param {(...args: unknown[]) => unknown} [fn] the implementation.
* @returns {void}
*/
function(name, options, fn) {
/** @type {any} */ (this.#db).function(name, options, fn);
}
/**
* Registers an aggregate (or window, with `inverse`) SQL function.
*
* @param {string} name the SQL name.
* @param {{ start: () => unknown, step: (acc: unknown, ...args: unknown[]) => unknown, result: (acc: unknown) => unknown, inverse?: (acc: unknown, ...args: unknown[]) => unknown, deterministic?: boolean, varargs?: boolean }} spec
* the implementation.
* @returns {void}
*/
aggregate(name, spec) {
/** @type {any} */ (this.#db).aggregate(name, spec);
}
/**
* Loads a SQLite extension.
*
* @param {string} path the extension file.
* @param {string} [entryPoint] the optional entry point.
* @returns {void}
*/
loadExtension(path, entryPoint) {
if (!this.#allowExtension) {
throw new Error(
'DatabaseSync.loadExtension() requires allowExtension: ' +
'true at construction (node:sqlite has the same gate)',
);
}
if (entryPoint !== undefined) {
throw new TypeError(
'loadExtension entryPoint is not supported; the extension ' +
'must use its default entry point',
);
}
this.#db.loadExtension(path, function () {
/* sync-shaped: completes on the queue */
});
}
/**
* No-op gate (the allowExtension constructor option is the real
* gate), kept for node:sqlite's shape.
*
* @param {boolean} allow the new setting.
* @returns {void}
*/
enableLoadExtension(allow) {
this.#allowExtension = allow === true;
}
/**
* Toggles SQLite defensive mode.
*
* @param {boolean} active the new state.
* @returns {void}
*/
enableDefensive(active) {
this.#db.dbConfig(sqlite3.DBCONFIG_DEFENSIVE, active === true);
}
/**
* The filesystem path of an attached database, or null for an
* in-memory or temporary one (node:sqlite's shape; this package's own
* `location()` returns the empty string there).
*
* @param {string} [dbName] the attached name.
* @returns {string | null} the path.
*/
location(dbName) {
const path = this.#db.location(dbName);
return path === '' ? null : path;
}
/**
* Not available: this package's authorizer is declarative (a C++ rule
* list — no JavaScript runs on the prepare path, by design). Use
* `db.native.authorizer(policy)`.
*
* @returns {never}
*/
setAuthorizer() {
throw new Error(
'DatabaseSync.setAuthorizer() is not available: this ' +
"package's authorizer is declarative (a C++ rule list, no " +
'JavaScript on the prepare path). Reach through ' +
'db.native.authorizer(policy) for the supported form',
);
}
/**
* Creates a changeset-recording session. Divergence: this package's
* sessions are asynchronous, so the returned session's methods return
* promises (node:sqlite's are synchronous).
*
* @param {{ table?: string }} [options]
* @returns {import('./sqlite3-binding.js').Session} the session.
*/
createSession(options = {}) {
return this.#db.session(options);
}
/**
* Applies a changeset (asynchronous here; see createSession).
*
* @param {Uint8Array} changeset the bytes.
* @param {import('./native.js').ApplyChangesetOptions} [options]
* @returns {Promise<void>} resolves once applied.
*/
applyChangeset(changeset, options) {
return this.#db.applyChangeset(changeset, options);
}
/**
* Serializes the database (asynchronous here).
*
* @returns {Promise<Uint8Array>} the bytes.
*/
async serialize() {
return this.#db.serializeToBytes();
}
/**
* Closes the connection, finalizing any statement prepared through it
* that is still open — node:sqlite's semantics, and required here:
* this package refuses to close a connection holding an unfinalized
* statement (`SQLITE_BUSY: unable to close due to unfinalized
* statements`).
*
* Divergence: the close is queued (this package's close is
* asynchronous), so the connection refuses further work at once but
* the file handle is released a turn later — use `await using` /
* `Symbol.asyncDispose`, or `await db.native.close()`, when a caller
* must know the file is free, e.g. before deleting or reopening it on
* Windows. A close that still fails (a statement prepared directly on
* `db.native`, say) is reported on the connection's `'error'` event
* rather than discarded; with no listener attached that surfaces as an
* uncaught error, which is the point — the connection stayed open.
*
* @returns {void}
*/
close() {
if (this.#db.open) {
this.#finalizeStatements();
const db = this.#db;
db.close((err) => {
if (err) db.emit('error', err);
});
}
}
/**
* `await using` support: finalizes outstanding statements, then waits
* for the close to complete (unlike {@link DatabaseSync.close}, which
* only starts it).
*
* @returns {Promise<void>} resolves once closed.
*/
async [Symbol.asyncDispose]() {
if (this.#db.open) {
this.#finalizeStatements();
await this.#db.close();
}
}
}
export { DatabaseSync, StatementSync };