You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

248 lines
6.6 KiB

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<void> {
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<void> {
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<T>(
sql: string,
params: unknown[] = [],
operation: "query" | "execute" = "query",
): Promise<SQLiteResult<T>> {
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<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
this.db.exec("BEGIN TRANSACTION");
}
protected async _commitTransaction(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
this.db.exec("COMMIT");
}
protected async _rollbackTransaction(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
this.db.exec("ROLLBACK");
}
protected async _prepareStatement<T>(
_sql: string,
): Promise<PreparedStatement<T>> {
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<void> {
// Statements are finalized when the PreparedStatement is finalized
}
async getDatabaseSize(): Promise<number> {
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;
}
}
}