forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsqlite-adapter.ts
More file actions
139 lines (127 loc) · 4.54 KB
/
sqlite-adapter.ts
File metadata and controls
139 lines (127 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* 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[];
}
export interface SqliteDatabase {
prepare(sql: string): SqliteStatement;
exec(sql: string): void;
pragma(str: string, options?: { simple?: boolean }): any;
transaction<T>(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;
constructor(dbPath: string) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { DatabaseSync } = require('node:sqlite');
this._db = 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);
},
};
}
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<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
return (...args: any[]) => {
this._db.exec('BEGIN');
try {
const result = fn(...args);
this._db.exec('COMMIT');
return result;
} catch (error) {
this._db.exec('ROLLBACK');
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): { db: SqliteDatabase; backend: SqliteBackend } {
try {
return { db: new NodeSqliteAdapter(dbPath), 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}`
);
}
}