providerrefactor part1

This commit is contained in:
seb
2026-09-21 03:33:55 +02:00
parent 53a20c4353
commit 4ed6260e91
57 changed files with 1662 additions and 145 deletions

View File

@@ -0,0 +1,134 @@
/**
* Provider for a caller-owned `better-sqlite3` Database instance.
*
* The driver is synchronous, but this adapter exposes the asynchronous
* provider contract used by LibDashStore.
*/
function bindParams(params) {
return Object.fromEntries(
Object.entries(params).map(([key, value]) => [
key.replace(/^[@:$]/, ''),
value === undefined ? null : value,
]),
);
}
function normalizeInsertId(value) {
if (value === undefined || value === null) return null;
if (typeof value !== 'bigint') return value;
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
}
export class BetterSqlite3Executor {
/** @param {import('better-sqlite3').Database} db */
constructor(db) {
this.db = db;
}
/** @returns {Promise<import('../provider.js').QueryResult>} */
async query(sql, params = {}) {
const statement = this.db.prepare(sql);
const bindings = bindParams(params);
if (statement.reader) {
const rows = statement.all(bindings);
return {
rows,
rowsAffected: rows.length,
insertId: normalizeInsertId(rows[0]?.id),
};
}
const result = statement.run(bindings);
return {
rows: [],
rowsAffected: result.changes,
insertId: normalizeInsertId(result.lastInsertRowid),
};
}
}
export class BetterSqlite3Provider extends BetterSqlite3Executor {
/**
* @param {import('better-sqlite3').Database} db An open, caller-owned database.
*/
constructor(db) {
if (
!db ||
typeof db.prepare !== 'function' ||
typeof db.exec !== 'function' ||
typeof db.pragma !== 'function'
) {
throw new TypeError(
'BetterSqlite3Provider requires an open better-sqlite3 Database',
);
}
super(db);
this.dialect = 'sqlite3';
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
}
#transactionTail = Promise.resolve();
/**
* Runs `fn(runner)` in an immediate write transaction. Transactions are
* serialized because better-sqlite3 has one synchronous connection.
*
* @template T
* @param {(runner: BetterSqlite3Executor) => Promise<T> | T} fn
* @returns {Promise<T>}
*/
transaction(fn) {
if (typeof fn !== 'function') {
return Promise.reject(new TypeError('transaction requires a callback'));
}
const run = async () => {
this.db.exec('BEGIN IMMEDIATE');
try {
const result = await fn(new BetterSqlite3Executor(this.db));
this.db.exec('COMMIT');
return result;
} catch (err) {
if (this.db.inTransaction) this.db.exec('ROLLBACK');
throw err;
}
};
const pending = this.#transactionTail.then(run, run);
this.#transactionTail = pending.catch(() => undefined);
return pending;
}
async exec() {
throw new Error('Stored procedures are not supported by better-sqlite3');
}
async initialize() {
// SQLite migrations are provided by the SQLite schema layer.
}
async purge() {
// SQLite schema teardown is provided by the SQLite schema layer.
}
/** Explicitly closes the caller-owned database. It is never closed implicitly. */
close() {
this.db.close();
}
}
export function isUniqueConstraintError(err) {
return (
err?.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
err?.code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
);
}
export function isForeignKeyConstraintError(err) {
return err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY';
}