File size: 3,543 Bytes
dcedb25 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import { createRequire } from 'node:module';
import type { Db, DbFactory, DbStatement } from './types.js';
const runtimeRequire = createRequire(import.meta.url);
type NodeSqliteRunResult = {
changes: number | bigint;
lastInsertRowid: number | bigint;
};
type NodeSqliteStatement = {
run(...params: unknown[]): NodeSqliteRunResult;
get(...params: unknown[]): unknown;
all(...params: unknown[]): unknown[];
};
type NodeSqliteDatabase = {
prepare(sql: string): NodeSqliteStatement;
exec(sql: string): void;
close(): void;
};
type NodeSqliteModule = {
DatabaseSync: new (path: string) => NodeSqliteDatabase;
};
function loadNodeSqlite(): NodeSqliteModule {
try {
return runtimeRequire('node:sqlite') as NodeSqliteModule;
} catch (cause) {
throw new Error(
'Android/Termux requires Node.js 22.13 or newer for the built-in node:sqlite database driver.',
{ cause },
);
}
}
function numberResult(value: number | bigint): number {
const n = Number(value);
if (!Number.isSafeInteger(n)) {
throw new Error(`SQLite returned an integer outside JavaScript's safe range: ${value}`);
}
return n;
}
function wrapStatement(statement: NodeSqliteStatement): DbStatement {
return {
get: (...params) => statement.get(...params),
all: (...params) => statement.all(...params),
run: (...params) => {
const result = statement.run(...params);
return {
changes: numberResult(result.changes),
lastInsertRowid: numberResult(result.lastInsertRowid),
};
},
};
}
/**
* `node:sqlite` adapter used on Android, where better-sqlite3 does not publish
* prebuilt binaries. It exposes only the small synchronous database contract
* the server uses and implements better-sqlite3-style nested transactions with
* savepoints.
*/
export const nodeSqliteFactory: DbFactory = (resolvedPath) => {
const { DatabaseSync } = loadNodeSqlite();
const raw = new DatabaseSync(resolvedPath);
let transactionDepth = 0;
let savepointSequence = 0;
const database: Db = {
name: resolvedPath,
memory: resolvedPath === ':memory:',
prepare: (sql) => wrapStatement(raw.prepare(sql)),
exec: (sql) => raw.exec(sql),
pragma: (source) => raw.prepare(`PRAGMA ${source}`).all(),
close: () => raw.close(),
transaction: <F extends (...args: any[]) => unknown>(fn: F): F => {
const wrapped = function (this: unknown, ...args: Parameters<F>): ReturnType<F> {
const outermost = transactionDepth === 0;
const savepoint = `freellmapi_tx_${++savepointSequence}`;
raw.exec(outermost ? 'BEGIN' : `SAVEPOINT ${savepoint}`);
transactionDepth += 1;
try {
const result = fn.apply(this, args) as ReturnType<F>;
if (result && typeof (result as { then?: unknown }).then === 'function') {
throw new Error('SQLite transaction callbacks must be synchronous');
}
raw.exec(outermost ? 'COMMIT' : `RELEASE SAVEPOINT ${savepoint}`);
return result;
} catch (error) {
try {
if (outermost) {
raw.exec('ROLLBACK');
} else {
raw.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
raw.exec(`RELEASE SAVEPOINT ${savepoint}`);
}
} catch {
// Preserve the original callback/commit error.
}
throw error;
} finally {
transactionDepth -= 1;
}
};
return wrapped as F;
},
};
return database;
};
|