import initSqlJs, { Database } from "@jlongster/sql.js"; import { SQLiteFS } from "absurd-sql"; import { IndexedDBBackend } from "absurd-sql/dist/indexeddb-backend"; import { BaseSQLiteService } from "./BaseSQLiteService"; import { SQLiteConfig, SQLiteResult, PreparedStatement, } from "../PlatformService"; import { logger } from "../../utils/logger"; /** * SQLite implementation using absurd-sql for web browsers. * Provides SQLite access in the browser using Web Workers and IndexedDB. */ export class AbsurdSQLService extends BaseSQLiteService { private db: Database | null = null; private worker: Worker | null = null; private config: SQLiteConfig | null = null; async initialize(config: SQLiteConfig): Promise { if (this.initialized) { return; } try { this.config = config; const SQL = await initSqlJs({ locateFile: (file) => `/sql-wasm/${file}`, }); // Initialize the virtual file system const backend = new IndexedDBBackend(this.config.name); const fs = new SQLiteFS(SQL.FS, backend); SQL.register_for_idb(fs); // Create and initialize the database this.db = new SQL.Database(this.config.name, { filename: true, }); // Configure database settings if (this.config.useWAL) { await this.execute("PRAGMA journal_mode = WAL"); this.stats.walMode = true; } if (this.config.useMMap) { const mmapSize = this.config.mmapSize ?? 30000000000; await this.execute(`PRAGMA mmap_size = ${mmapSize}`); this.stats.mmapActive = true; } // Set other pragmas for performance await this.execute("PRAGMA synchronous = NORMAL"); await this.execute("PRAGMA temp_store = MEMORY"); await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache // Start the Web Worker for async operations this.worker = new Worker(new URL("./sqlite.worker.ts", import.meta.url), { type: "module", }); this.initialized = true; await this.updateStats(); } catch (error) { logger.error("Failed to initialize Absurd SQL:", error); throw error; } } async close(): Promise { if (!this.initialized || !this.db) { return; } try { // Finalize all prepared statements for (const [_sql, stmt] of this.preparedStatements) { logger.debug("finalizing statement", _sql); await stmt.finalize(); } this.preparedStatements.clear(); // Close the database this.db.close(); this.db = null; // Terminate the worker if (this.worker) { this.worker.terminate(); this.worker = null; } this.initialized = false; } catch (error) { logger.error("Failed to close Absurd SQL connection:", error); throw error; } } protected async _executeQuery( sql: string, params: unknown[] = [], operation: "query" | "execute" = "query", ): Promise> { if (!this.db) { throw new Error("Database not initialized"); } try { let lastInsertId: number | undefined = undefined; if (operation === "query") { const stmt = this.db.prepare(sql); const rows: T[] = []; try { while (stmt.step()) { rows.push(stmt.getAsObject() as T); } } finally { stmt.free(); } // Get last insert ID safely const result = this.db.exec("SELECT last_insert_rowid() AS id"); lastInsertId = (result?.[0]?.values?.[0]?.[0] as number | undefined) ?? undefined; return { rows, rowsAffected: this.db.getRowsModified(), lastInsertId, executionTime: 0, // Will be set by base class }; } else { this.db.run(sql, params); // Get last insert ID after execute const result = this.db.exec("SELECT last_insert_rowid() AS id"); lastInsertId = (result?.[0]?.values?.[0]?.[0] as number | undefined) ?? undefined; return { rows: [], rowsAffected: this.db.getRowsModified(), lastInsertId, executionTime: 0, }; } } catch (error) { logger.error("Absurd SQL query failed:", { sql, params, error: error instanceof Error ? error.message : String(error), }); throw error; } } protected async _beginTransaction(): Promise { if (!this.db) { throw new Error("Database not initialized"); } this.db.exec("BEGIN TRANSACTION"); } protected async _commitTransaction(): Promise { if (!this.db) { throw new Error("Database not initialized"); } this.db.exec("COMMIT"); } protected async _rollbackTransaction(): Promise { if (!this.db) { throw new Error("Database not initialized"); } this.db.exec("ROLLBACK"); } protected async _prepareStatement( _sql: string, ): Promise> { if (!this.db) { throw new Error("Database not initialized"); } const stmt = this.db.prepare(_sql); return { execute: async (params: unknown[] = []) => { if (!this.db) { throw new Error("Database not initialized"); } try { const rows: T[] = []; stmt.bind(params); while (stmt.step()) { rows.push(stmt.getAsObject() as T); } // Safely extract lastInsertId const result = this.db.exec("SELECT last_insert_rowid()"); const rawId = result?.[0]?.values?.[0]?.[0]; const lastInsertId = typeof rawId === "number" ? rawId : undefined; return { rows, rowsAffected: this.db.getRowsModified(), lastInsertId, executionTime: 0, // Will be set by base class }; } finally { stmt.reset(); } }, finalize: async () => { stmt.free(); }, }; } protected async _finalizeStatement(_sql: string): Promise { // Statements are finalized when the PreparedStatement is finalized } async getDatabaseSize(): Promise { if (!this.db) { throw new Error("Database not initialized"); } try { const result = this.db.exec( "SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()", ); const rawSize = result?.[0]?.values?.[0]?.[0]; const size = typeof rawSize === "number" ? rawSize : 0; return size; } catch (error) { logger.error("Failed to get database size:", error); return 0; } } }