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
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;
|
|
}
|
|
}
|
|
}
|
|
|