feat: initial commit

This commit is contained in:
2026-06-29 23:07:14 +00:00
commit 3b9a6bc85c
152 changed files with 12558 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
import { ConflictAbodeError, NotFoundAbodeError } from "../../types/errors.js";
import type { SqlCode, SqlVar } from "../sql.js";
import Sqlite, * as sqlite from "better-sqlite3";
import pragma from "../pragma.sqlite.sql";
import type { WrappedDb, WrappedDbOptions } from "./types.js";
function getDatabase(
path: string,
options?: Omit<sqlite.Options, "nativeBinding">
): sqlite.Database {
if (!natives.sqlite) throw new Error("No natives found for better-sqlite3");
options = { ...options };
if (typeof options.timeout !== "number") delete options.timeout;
const db = new Sqlite(path, { ...options, nativeBinding: natives.sqlite });
db.exec(pragma);
return db;
}
function rethrow<R>(fn: () => R): R {
try {
return fn();
} catch (e) {
if (e instanceof sqlite.SqliteError) {
switch (e.code) {
case "SQLITE_CONSTRAINT_UNIQUE":
case "SQLITE_CONSTRAINT_PRIMARYKEY":
throw new ConflictAbodeError();
case "SQLITE_CONSTRAINT_FOREIGNKEY":
throw new NotFoundAbodeError();
}
}
throw e;
}
}
export class WrappedBetterSqlite3Db implements WrappedDb {
#db: sqlite.Database;
#statements: Map<string, sqlite.Statement>;
constructor(path: string, options: WrappedDbOptions = {}) {
this.#db = getDatabase(path, {
readonly: options.readonly,
timeout: options.timeout,
});
this.#statements = new Map();
}
get _db() {
return this.#db;
}
get readonly(): boolean {
return this.#db.readonly;
}
destroy(): void {
this.#db.close();
this.#statements.clear();
}
#stmt<R>(stmt: string): sqlite.Statement<SqlVar[], R> {
if (!this.#db.open) throw new Error("Db is closed");
let prep = this.#statements.get(stmt);
if (!prep) {
prep = this.#db.prepare<(string | Buffer)[], R>(stmt);
this.#statements.set(stmt, prep);
}
return prep as sqlite.Statement<string | Buffer, R>;
}
all<R>(stmt: SqlCode): R[] {
return this.#stmt<R>(stmt._sql).all(...stmt._vars);
}
get<R>(stmt: SqlCode): R | null {
const results = this.all<R>(stmt);
if (results.length > 1) throw new Error("Multiple results");
if (!results.length) return null;
return results[0];
}
run(stmt: SqlCode): { changes: number } {
return this.#stmt<void>(stmt._sql).run(...stmt._vars);
}
multi<R>(fn: () => R): R {
return this.#db.transaction(fn)();
}
rethrow = rethrow;
}
@@ -0,0 +1,6 @@
import { WrappedBetterSqlite3Db } from "./better-sqlite3.js";
import { WrappedNodeSqliteDb } from "./node-sqlite.js";
import type { WrappedDbConstructor } from "./types.js";
export const node: WrappedDbConstructor | null = WrappedNodeSqliteDb;
export const bs3: WrappedDbConstructor | null = WrappedBetterSqlite3Db;
@@ -0,0 +1,5 @@
import { WrappedBetterSqlite3Db } from "./better-sqlite3.js";
import type { WrappedDbConstructor } from "./types.js";
export const node: WrappedDbConstructor | null = null;
export const bs3: WrappedDbConstructor | null = WrappedBetterSqlite3Db;
@@ -0,0 +1,5 @@
import { WrappedNodeSqliteDb } from "./node-sqlite.js";
import type { WrappedDbConstructor } from "./types.js";
export const node: WrappedDbConstructor | null = WrappedNodeSqliteDb;
export const bs3: WrappedDbConstructor | null = null;
+1
View File
@@ -0,0 +1 @@
export * from "./implementations.all.js";
+20
View File
@@ -0,0 +1,20 @@
import type { WrappedDb, WrappedDbOptions } from "./types.js";
import { node, bs3 } from "./implementations.js";
export function getWrappedDb(
kind: "any" | "node" | "bs3",
path: string,
options: WrappedDbOptions
): WrappedDb {
if (kind === "node") {
if (!node) throw new Error("Requesting unavailable node backend");
return new node(path, options);
} else if (kind === "bs3") {
if (!bs3) throw new Error("Requesting unavailable better-sqlite3 backend");
return new bs3(path, options);
} else {
const impl = [node, bs3].find(Boolean);
if (!impl) throw new Error("No available backend");
return new impl(path, options);
}
}
+76
View File
@@ -0,0 +1,76 @@
import { DatabaseSync, StatementSync } from "node:sqlite";
import type { WrappedDb, WrappedDbOptions } from "./types.js";
import type { SqlCode } from "../sql.js";
import pragma from "../pragma.sqlite.sql";
export class WrappedNodeSqliteDb implements WrappedDb {
#db: DatabaseSync;
#readonly: boolean;
#statements: Map<string, StatementSync>;
constructor(path: string, options: WrappedDbOptions = {}) {
this.#readonly = !!options.readonly;
this.#db = new DatabaseSync(path, {
readOnly: options.readonly,
timeout: options.timeout,
open: true,
});
this.#db.exec(pragma);
this.#statements = new Map();
}
get _db() {
return this.#db;
}
get readonly(): boolean {
return this.#readonly;
}
destroy(): void {
this.#db.close();
this.#statements.clear();
}
#stmt(stmt: string): StatementSync {
if (!this.#db.open) throw new Error("Db is closed");
let prep = this.#statements.get(stmt);
if (!prep) {
prep = this.#db.prepare(stmt);
prep.setReadBigInts(false);
this.#statements.set(stmt, prep);
}
return prep;
}
all<R>(stmt: SqlCode): R[] {
return this.#stmt(stmt._sql).all(...stmt._vars) as R[];
}
get<R>(stmt: SqlCode): R | null {
const results = this.all<R>(stmt);
if (results.length > 1) throw new Error("Multiple results");
if (!results.length) return null;
return results[0];
}
run(stmt: SqlCode): { changes: number } {
const { changes } = this.#stmt(stmt._sql).run(...stmt._vars);
return { changes: Number(changes) };
}
multi<R>(fn: () => R): R {
if (this.#db.isTransaction)
throw new Error("Nested transactions not supported");
this.#db.exec("BEGIN");
try {
const rst = fn();
this.#db.exec("COMMIT");
return rst;
} catch (e) {
this.#db.exec("ROLLBACK");
throw e;
}
}
rethrow<R>(fn: () => R): R {
return fn();
}
}
+18
View File
@@ -0,0 +1,18 @@
import type { SqlCode } from "../sql.js";
export interface WrappedDb {
get readonly(): boolean;
destroy(): void;
all<R>(stmt: SqlCode): R[];
get<R>(stmt: SqlCode): R | null;
run(stmt: SqlCode): { changes: number };
multi<R>(fn: () => R): R;
rethrow<R>(fn: () => R): R;
}
export type WrappedDbOptions = { readonly?: boolean; timeout?: number };
export interface WrappedDbConstructor {
new (path: string, options?: WrappedDbOptions): WrappedDb;
}