import ffi from "./ffi.ts";import { fromFileUrl } from "../deps.ts";import { SQLITE3_OPEN_CREATE, SQLITE3_OPEN_MEMORY, SQLITE3_OPEN_READONLY, SQLITE3_OPEN_READWRITE, SQLITE_BLOB, SQLITE_FLOAT, SQLITE_INTEGER, SQLITE_NULL, SQLITE_TEXT,} from "./constants.ts";import { readCstr, toCString, unwrap } from "./util.ts";import { RestBindParameters, Statement, STATEMENTS } from "./statement.ts";import { BlobOpenOptions, SQLBlob } from "./blob.ts";
export interface DatabaseOpenOptions { readonly?: boolean; create?: boolean; flags?: number; memory?: boolean; int64?: boolean; unsafeConcurrency?: boolean;}
export type Transaction<T> = ((v: T) => void) & { default: Transaction<T>; deferred: Transaction<T>; immediate: Transaction<T>; exclusive: Transaction<T>; database: Database;};
export interface FunctionOptions { varargs?: boolean; deterministic?: boolean; directOnly?: boolean; innocuous?: boolean; subtype?: boolean;}
export interface AggregateFunctionOptions extends FunctionOptions { start: any | (() => any); step: (aggregate: any, ...args: any[]) => void; final?: (aggregate: any) => any;}
const { sqlite3_open_v2, sqlite3_close_v2, sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid, sqlite3_get_autocommit, sqlite3_exec, sqlite3_free, sqlite3_libversion, sqlite3_sourceid, sqlite3_complete, sqlite3_finalize, sqlite3_result_blob, sqlite3_result_double, sqlite3_result_error, sqlite3_result_int64, sqlite3_result_null, sqlite3_result_text, sqlite3_value_blob, sqlite3_value_bytes, sqlite3_value_double, sqlite3_value_int64, sqlite3_value_text, sqlite3_value_type, sqlite3_create_function, sqlite3_result_int, sqlite3_aggregate_context,} = ffi;
export const SQLITE_VERSION = readCstr(sqlite3_libversion());export const SQLITE_SOURCEID = readCstr(sqlite3_sourceid());
export function isComplete(statement: string): boolean { return Boolean(sqlite3_complete(toCString(statement)));}
export class Database { #path: string; #handle: Deno.PointerValue; #open = true;
int64: boolean;
unsafeConcurrency: boolean;
get open(): boolean { return this.#open; }
get unsafeHandle(): Deno.PointerValue { return this.#handle; }
get path(): string { return this.#path; }
get changes(): number { return sqlite3_changes(this.#handle); }
get totalChanges(): number { return sqlite3_total_changes(this.#handle); }
get lastInsertRowId(): number { return Number(sqlite3_last_insert_rowid(this.#handle)); }
get autocommit(): boolean { return sqlite3_get_autocommit(this.#handle) === 1; }
get inTransaction(): boolean { return this.#open && !this.autocommit; }
constructor(path: string | URL, options: DatabaseOpenOptions = {}) { this.#path = path instanceof URL ? fromFileUrl(path) : path; let flags = 0; this.int64 = options.int64 ?? false; this.unsafeConcurrency = options.unsafeConcurrency ?? false; if (options.flags !== undefined) { flags = options.flags; } else { if (options.memory) { flags |= SQLITE3_OPEN_MEMORY; }
if (options.readonly ?? false) { flags |= SQLITE3_OPEN_READONLY; } else { flags |= SQLITE3_OPEN_READWRITE; }
if (options.create ?? true) { flags |= SQLITE3_OPEN_CREATE; } }
const pHandle = new Uint32Array(2); const result = sqlite3_open_v2(toCString(this.#path), pHandle, flags, 0); this.#handle = pHandle[0] + 2 ** 32 * pHandle[1]; if (result !== 0) sqlite3_close_v2(this.#handle); unwrap(result); }
prepare(sql: string): Statement { return new Statement(this, sql); }
openBlob(options: BlobOpenOptions): SQLBlob { return new SQLBlob(this, options); }
exec(sql: string, ...params: RestBindParameters): number { if (params.length === 0) { const pErr = new Uint32Array(2); sqlite3_exec(this.#handle, toCString(sql), 0, 0, pErr); const errPtr = pErr[0] + 2 ** 32 * pErr[1]; if (errPtr !== 0) { const err = readCstr(errPtr); sqlite3_free(errPtr); throw new Error(err); } return sqlite3_changes(this.#handle); }
const stmt = this.prepare(sql); stmt.run(...params); return sqlite3_changes(this.#handle); }
run(sql: string, ...params: RestBindParameters): number { return this.exec(sql, ...params); }
transaction<T = any>( fn: (this: Transaction<T>, _: T) => unknown, ): Transaction<T> { const controller = getController(this);
const properties = { default: { value: wrapTransaction(fn, this, controller.default) }, deferred: { value: wrapTransaction(fn, this, controller.deferred) }, immediate: { value: wrapTransaction(fn, this, controller.immediate) }, exclusive: { value: wrapTransaction(fn, this, controller.exclusive) }, database: { value: this, enumerable: true }, };
Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties);
return properties.default.value as any as Transaction<T>; }
#callbacks = new Set<Deno.UnsafeCallback>();
function( name: string, fn: CallableFunction, options?: FunctionOptions, ): void { const cb = new Deno.UnsafeCallback( { parameters: ["pointer", "i32", "pointer"], result: "void", } as const, (ctx, nArgs, pArgs) => { const argptr = new Deno.UnsafePointerView(pArgs); const args: any[] = []; for (let i = 0; i < nArgs; i++) { const arg = Number(argptr.getBigUint64(i * 8)); const type = sqlite3_value_type(arg); switch (type) { case SQLITE_INTEGER: args.push(sqlite3_value_int64(arg)); break; case SQLITE_FLOAT: args.push(sqlite3_value_double(arg)); break; case SQLITE_TEXT: args.push( new TextDecoder().decode( new Uint8Array( Deno.UnsafePointerView.getArrayBuffer( sqlite3_value_text(arg), sqlite3_value_bytes(arg), ), ), ), ); break; case SQLITE_BLOB: args.push( new Uint8Array( Deno.UnsafePointerView.getArrayBuffer( sqlite3_value_blob(arg), sqlite3_value_bytes(arg), ), ), ); break; case SQLITE_NULL: args.push(null); break; default: throw new Error(`Unknown type: ${type}`); } }
let result: any; try { result = fn(...args); } catch (err) { const buf = new TextEncoder().encode(err.message); sqlite3_result_error(ctx, buf, buf.byteLength); return; }
if (result === undefined || result === null) { sqlite3_result_null(ctx); } else if (typeof result === "boolean") { sqlite3_result_int(ctx, result ? 1 : 0); } else if (typeof result === "number") { if (Number.isSafeInteger(result)) sqlite3_result_int64(ctx, result); else sqlite3_result_double(ctx, result); } else if (typeof result === "bigint") { sqlite3_result_int64(ctx, result); } else if (typeof result === "string") { const buffer = new TextEncoder().encode(result); sqlite3_result_text(ctx, buffer, buffer.byteLength, 0); } else if (result instanceof Uint8Array) { sqlite3_result_blob(ctx, result, result.length, -1); } else { const buffer = new TextEncoder().encode( `Invalid return value: ${Deno.inspect(result)}`, ); sqlite3_result_error(ctx, buffer, buffer.byteLength); } }, );
let flags = 1;
if (options?.deterministic) { flags |= 0x000000800; }
if (options?.directOnly) { flags |= 0x000080000; }
if (options?.subtype) { flags |= 0x000100000; }
if (options?.directOnly) { flags |= 0x000200000; }
const err = sqlite3_create_function( this.#handle, toCString(name), options?.varargs ? -1 : fn.length, flags, 0, cb.pointer, 0, 0, );
unwrap(err, this.#handle);
this.#callbacks.add(cb as Deno.UnsafeCallback); }
aggregate(name: string, options: AggregateFunctionOptions): void { const contexts = new Map<Deno.PointerValue, any>();
const cb = new Deno.UnsafeCallback( { parameters: ["pointer", "i32", "pointer"], result: "void", } as const, (ctx, nArgs, pArgs) => { const aggrCtx = sqlite3_aggregate_context(ctx, 8); let aggregate; if (contexts.has(aggrCtx)) { aggregate = contexts.get(aggrCtx); } else { aggregate = typeof options.start === "function" ? options.start() : options.start; contexts.set(aggrCtx, aggregate); } const argptr = new Deno.UnsafePointerView(pArgs); const args: any[] = []; for (let i = 0; i < nArgs; i++) { const arg = Number(argptr.getBigUint64(i * 8)); const type = sqlite3_value_type(arg); switch (type) { case SQLITE_INTEGER: args.push(sqlite3_value_int64(arg)); break; case SQLITE_FLOAT: args.push(sqlite3_value_double(arg)); break; case SQLITE_TEXT: args.push( new TextDecoder().decode( new Uint8Array( Deno.UnsafePointerView.getArrayBuffer( sqlite3_value_text(arg), sqlite3_value_bytes(arg), ), ), ), ); break; case SQLITE_BLOB: args.push( new Uint8Array( Deno.UnsafePointerView.getArrayBuffer( sqlite3_value_blob(arg), sqlite3_value_bytes(arg), ), ), ); break; case SQLITE_NULL: args.push(null); break; default: throw new Error(`Unknown type: ${type}`); } }
let result: any; try { result = options.step(aggregate, ...args); } catch (err) { const buf = new TextEncoder().encode(err.message); sqlite3_result_error(ctx, buf, buf.byteLength); return; }
contexts.set(aggrCtx, result); }, );
const cbFinal = new Deno.UnsafeCallback( { parameters: ["pointer"], result: "void", } as const, (ctx) => { const aggrCtx = sqlite3_aggregate_context(ctx, 0); const aggregate = contexts.get(aggrCtx); contexts.delete(aggrCtx); let result: any; try { result = options.final ? options.final(aggregate) : aggregate; } catch (err) { const buf = new TextEncoder().encode(err.message); sqlite3_result_error(ctx, buf, buf.byteLength); return; }
if (result === undefined || result === null) { sqlite3_result_null(ctx); } else if (typeof result === "boolean") { sqlite3_result_int(ctx, result ? 1 : 0); } else if (typeof result === "number") { if (Number.isSafeInteger(result)) sqlite3_result_int64(ctx, result); else sqlite3_result_double(ctx, result); } else if (typeof result === "bigint") { sqlite3_result_int64(ctx, result); } else if (typeof result === "string") { const buffer = new TextEncoder().encode(result); sqlite3_result_text(ctx, buffer, buffer.byteLength, 0); } else if (result instanceof Uint8Array) { sqlite3_result_blob(ctx, result, result.length, -1); } else { const buffer = new TextEncoder().encode( `Invalid return value: ${Deno.inspect(result)}`, ); sqlite3_result_error(ctx, buffer, buffer.byteLength); } }, );
let flags = 1;
if (options?.deterministic) { flags |= 0x000000800; }
if (options?.directOnly) { flags |= 0x000080000; }
if (options?.subtype) { flags |= 0x000100000; }
if (options?.directOnly) { flags |= 0x000200000; }
const err = sqlite3_create_function( this.#handle, toCString(name), options?.varargs ? -1 : options.step.length - 1, flags, 0, 0, cb.pointer, cbFinal.pointer, );
unwrap(err, this.#handle);
this.#callbacks.add(cb as Deno.UnsafeCallback); this.#callbacks.add(cbFinal as Deno.UnsafeCallback); }
close(): void { if (!this.#open) return; for (const [stmt, db] of STATEMENTS) { if (db === this.#handle) { sqlite3_finalize(stmt); STATEMENTS.delete(stmt); } } for (const cb of this.#callbacks) { cb.close(); } unwrap(sqlite3_close_v2(this.#handle)); this.#open = false; }
[Symbol.for("Deno.customInspect")](): string { return `SQLite3.Database { path: ${this.path} }`; }}
const controllers = new WeakMap();
const getController = (db: Database) => { let controller = controllers.get(db); if (!controller) { const shared = { commit: db.prepare("COMMIT"), rollback: db.prepare("ROLLBACK"), savepoint: db.prepare("SAVEPOINT `\t_bs3.\t`"), release: db.prepare("RELEASE `\t_bs3.\t`"), rollbackTo: db.prepare("ROLLBACK TO `\t_bs3.\t`"), };
controllers.set( db, controller = { default: Object.assign( { begin: db.prepare("BEGIN") }, shared, ), deferred: Object.assign( { begin: db.prepare("BEGIN DEFERRED") }, shared, ), immediate: Object.assign( { begin: db.prepare("BEGIN IMMEDIATE") }, shared, ), exclusive: Object.assign( { begin: db.prepare("BEGIN EXCLUSIVE") }, shared, ), }, ); } return controller;};
const wrapTransaction = ( fn: any, db: Database, { begin, commit, rollback, savepoint, release, rollbackTo }: any,) => function sqliteTransaction(): any { const { apply } = Function.prototype; let before, after, undo; if (db.inTransaction) { before = savepoint; after = release; undo = rollbackTo; } else { before = begin; after = commit; undo = rollback; } before.run(); try { const result = apply.call(fn, this, arguments); after.run(); return result; } catch (ex) { if (!db.autocommit) { undo.run(); if (undo !== rollback) after.run(); } throw ex; } };