/** * SQLite Adapter * * Thin wrapper over Node's built-in `node:sqlite` (`DatabaseSync`), exposed * through a small better-sqlite3-shaped interface so the rest of the codebase * is storage-agnostic. * * CodeGraph ships with a bundled Node runtime, so `node:sqlite` (real SQLite, * with WAL + FTS5) is always available — there is no native build step and no * wasm fallback. When run from source instead, it requires Node >= 22.5. */ export interface SqliteStatement { run(...params: any[]): { changes: number; lastInsertRowid: number | bigint }; get(...params: any[]): any; all(...params: any[]): any[]; /** * Lazily yield result rows one at a time instead of materializing the whole * set with `all()`. Use for unbounded scans (e.g. every function/method node) * so memory stays O(1) in the row count rather than O(rows) — see #610, where * `all()`-ing every symbol on a dense project spiked the heap into an OOM. */ iterate(...params: any[]): IterableIterator; } export interface SqliteDatabase { prepare(sql: string): SqliteStatement; exec(sql: string): void; pragma(str: string, options?: { simple?: boolean }): any; transaction(fn: (...args: any[]) => T): (...args: any[]) => T; close(): void; readonly open: boolean; } /** * The active SQLite backend. Only one now (`node:sqlite`); kept as a named type * so `codegraph status` and the per-instance reporting have a stable shape. */ export type SqliteBackend = 'node-sqlite'; /** * Wraps Node's built-in `node:sqlite` (`DatabaseSync`) to match the * better-sqlite3 interface the rest of the code expects. * * node:sqlite is real SQLite compiled into Node, so it supports WAL, FTS5, * mmap, and `@named` params natively — the only shims needed are the * better-sqlite3 conveniences node:sqlite omits: a `.pragma()` helper, a * `.transaction()` helper, and `open` (node:sqlite exposes `isOpen`). */ class NodeSqliteAdapter implements SqliteDatabase { private _db: any; private _txDepth = 0; constructor(dbPath: string, opts?: { readOnly?: boolean }) { // eslint-disable-next-line @typescript-eslint/no-require-imports const { DatabaseSync } = require('node:sqlite'); this._db = opts?.readOnly ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath); } get open(): boolean { return this._db.isOpen; } prepare(sql: string): SqliteStatement { // node:sqlite matches better-sqlite3's calling convention (variadic // positional args, or a single object for @named params), so params forward // through unchanged. const stmt = this._db.prepare(sql); return { run(...params: any[]) { const r = stmt.run(...params); return { changes: Number(r?.changes ?? 0), lastInsertRowid: r?.lastInsertRowid ?? 0, }; }, get(...params: any[]) { return stmt.get(...params); }, all(...params: any[]) { return stmt.all(...params); }, iterate(...params: any[]) { return stmt.iterate(...params); }, }; } exec(sql: string): void { this._db.exec(sql); } pragma(str: string, options?: { simple?: boolean }): any { const trimmed = str.trim(); // Write pragma ("key = value"): node:sqlite is real SQLite, so every pragma // (WAL, mmap, synchronous, …) applies as-is. if (trimmed.includes('=')) { this._db.exec(`PRAGMA ${trimmed}`); return; } // Read pragma. Default: the row object (e.g. { journal_mode: 'wal' }). // `{ simple: true }` returns just the single column value, like better-sqlite3. const row = this._db.prepare(`PRAGMA ${trimmed}`).get(); if (options?.simple) { return row && typeof row === 'object' ? Object.values(row)[0] : row; } return row; } transaction(fn: (...args: any[]) => T): (...args: any[]) => T { return (...args: any[]) => { // Nested call (a transaction()-wrapped helper invoked from inside another // transaction): run the body directly inside the enclosing transaction. // BEGIN would throw "cannot start a transaction within a transaction", // so no existing caller ever relied on nested rollback granularity — // flattening is behavior-preserving and free. if (this._txDepth > 0) { this._txDepth++; try { return fn(...args); } finally { this._txDepth--; } } this._db.exec('BEGIN'); this._txDepth = 1; try { const result = fn(...args); this._db.exec('COMMIT'); this._txDepth = 0; return result; } catch (error) { this._db.exec('ROLLBACK'); this._txDepth = 0; throw error; } }; } close(): void { // node:sqlite's DatabaseSync.close() throws if already closed; make it // idempotent to match better-sqlite3 (callers may close more than once). if (this._db.isOpen) this._db.close(); } } /** * Create a database connection backed by `node:sqlite`. * * Returns the active backend alongside the db so each `DatabaseConnection` can * report it per-instance — MCP can open multiple project DBs in one process, so * a process-global would race. */ export function createDatabase(dbPath: string, opts?: { readOnly?: boolean }): { db: SqliteDatabase; backend: SqliteBackend } { try { return { db: new NodeSqliteAdapter(dbPath, opts), backend: 'node-sqlite' }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); throw new Error( 'Failed to open SQLite via the built-in node:sqlite module.\n' + 'CodeGraph requires node:sqlite (Node.js 22.5+). Install the self-contained\n' + 'CodeGraph release (it bundles a compatible Node), or run on Node 22.5+.\n' + `Underlying error: ${msg}` ); } }