feat: initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./implementations.all.js";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user