Repository navigation
Expand file tree
/
Copy pathsqlite-driver.ts
More file actions
187 lines (166 loc) · 7.17 KB
/
Copy pathsqlite-driver.ts
File metadata and controls
187 lines (166 loc) · 7.17 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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* SQLite driver adapter for the SQLite DB provider.
*
* Selects the embedded SQLite driver by runtime so sqlite connections work
* both under Bun (standalone dev, Docker image) and under plain Node
* (npx / brew / deb installs running `node server.js`):
*
* - Bun runtime -> `bun:sqlite` (Bun built-in)
* - Node runtime -> `node:sqlite` (Node built-in: unflagged from 22.13,
* stable on the recommended Node 24 LTS; `better-sqlite3` is not used here
* because Bun refuses to load it at all and its native binding must match
* the installing runtime's ABI, while `node:sqlite` needs no native
* dependency)
*
* Set LIBREDB_SQLITE_DRIVER=bun|node to force a driver (deterministic tests).
* Both drivers load lazily via dynamic import, so neither is required unless
* a sqlite connection is actually used. This module is internal to the SQLite
* provider — other code must not depend on it.
*/
import { DatabaseConfigError } from "../../errors";
// The exact driver surface the SQLite provider uses (bun:sqlite-shaped).
export type SQLiteStatement = {
all(...params: unknown[]): unknown[];
get(...params: unknown[]): unknown;
run(...params: unknown[]): { changes: number };
};
export type SQLiteDatabase = {
exec(sql: string): void;
prepare(sql: string): SQLiteStatement;
close(): void;
};
/**
* Open options in the bun:sqlite spelling (this surface is bun-shaped; the
* node adapter below translates). `readonly` opens the database under SQLite's
* own read-only enforcement — writes are refused by the engine and a missing
* file is NOT created — which is the SQLite half of the agent execution
* profile (#328).
*/
export type SQLiteOpenOptions = { create?: boolean; readwrite?: boolean; readonly?: boolean };
export type SQLiteConstructor = new (path: string, options?: SQLiteOpenOptions) => SQLiteDatabase;
export type SQLiteDriverName = "bun" | "node";
// Minimal structural view of node:sqlite (kept local so the adapter and its
// tests never need the real module, which Bun does not implement).
type NodeStatementLike = {
all(...params: unknown[]): unknown;
get(...params: unknown[]): unknown;
run(...params: unknown[]): { changes: number | bigint };
};
export type NodeDatabaseSyncLike = {
exec(sql: string): void;
prepare(sql: string): NodeStatementLike;
close(): void;
};
/** node:sqlite's own open options — only the ones this adapter maps. */
export type NodeSQLiteOpenOptions = { readOnly?: boolean };
export type NodeSQLiteModule = {
DatabaseSync: new (path: string, options?: NodeSQLiteOpenOptions) => NodeDatabaseSyncLike;
};
const loadedDrivers = new Map<SQLiteDriverName, SQLiteConstructor>();
const driverLoadErrors = new Map<SQLiteDriverName, Error>();
/**
* Resolve which driver to use: the LIBREDB_SQLITE_DRIVER override wins,
* otherwise pick by the current runtime.
*/
export function resolveSQLiteDriverName(): SQLiteDriverName {
const override = process.env.LIBREDB_SQLITE_DRIVER;
if (override === "bun" || override === "node") {
return override;
}
return typeof Bun === "undefined" ? "node" : "bun";
}
async function loadBunDriver(): Promise<SQLiteConstructor> {
// The ignore comments keep the bundler's hands off this import: Turbopack
// would otherwise emit an externals chunk FILE named after the specifier
// ("[externals]_bun:sqlite_<hash>._.js"), and the colon makes that name
// unwritable on NTFS - the win32 standalone build fails (issue #114).
// At runtime nothing changes: Bun resolves its builtin natively, and this
// branch is only ever taken under the Bun runtime.
const sqlite = await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ "bun:sqlite");
return sqlite.Database as unknown as SQLiteConstructor;
}
/**
* Adapts node:sqlite's DatabaseSync to the bun:sqlite-shaped surface above.
* The two APIs are nearly identical (synchronous exec/prepare/all/get/run);
* the bridges below keep behaviour byte-compatible with bun:sqlite:
* - node:sqlite opens read-write and creates missing files by default,
* matching the `{ create: true, readwrite: true }` options the provider
* passes to bun:sqlite, so those two flags need no translation. The
* read-only flag DOES: node spells it `readOnly`, bun spells it `readonly`,
* and an adapter that dropped it would silently hand an agent execution
* profile a fully writable database handle (#328).
* - `get()` returns `undefined` on a miss where bun:sqlite returns `null`.
* - `run()` reports `changes` as `number | bigint`; normalize to `number`.
*
* Exported (with the injectable ctor) so the adapter semantics are unit-testable
* in-process under Bun, where node:sqlite itself cannot be imported.
*/
export function createNodeSQLiteDriver(DatabaseSyncCtor: NodeSQLiteModule["DatabaseSync"]): SQLiteConstructor {
class NodeSQLiteDatabase implements SQLiteDatabase {
private readonly db: NodeDatabaseSyncLike;
constructor(dbPath: string, options?: SQLiteOpenOptions) {
this.db = new DatabaseSyncCtor(dbPath, { readOnly: options?.readonly === true });
}
exec(sql: string): void {
this.db.exec(sql);
}
prepare(sql: string): SQLiteStatement {
const stmt = this.db.prepare(sql);
return {
all: (...params: unknown[]): unknown[] => stmt.all(...params) as unknown[],
get: (...params: unknown[]): unknown => stmt.get(...params) ?? null,
run: (...params: unknown[]): { changes: number } => {
const info = stmt.run(...params);
return { changes: Number(info.changes) };
},
};
}
close(): void {
this.db.close();
}
}
return NodeSQLiteDatabase;
}
async function importNodeSQLite(): Promise<NodeSQLiteModule> {
return (await import("node:sqlite")) as unknown as NodeSQLiteModule;
}
/**
* Load the node:sqlite-backed driver. The module import is injectable so the
* success path is unit-testable under Bun (which lacks node:sqlite); callers
* outside tests use the default importer.
*/
export async function loadNodeSQLiteDriver(
importModule: () => Promise<NodeSQLiteModule> = importNodeSQLite,
): Promise<SQLiteConstructor> {
const sqlite = await importModule();
return createNodeSQLiteDriver(sqlite.DatabaseSync);
}
/**
* Load the runtime-appropriate SQLite driver (lazily, cached per driver).
*/
export async function loadSQLiteDriver(): Promise<SQLiteConstructor> {
const name = resolveSQLiteDriverName();
const cached = loadedDrivers.get(name);
if (cached) {
return cached;
}
const cachedError = driverLoadErrors.get(name);
if (cachedError) {
throw cachedError;
}
try {
const driver = name === "bun" ? await loadBunDriver() : await loadNodeSQLiteDriver();
loadedDrivers.set(name, driver);
return driver;
} catch (error) {
const loadError = new DatabaseConfigError(
`SQLite driver "${name}" is not available in this environment: ` +
`${error instanceof Error ? error.message : String(error)}. ` +
'The "bun" driver requires the Bun runtime (bun:sqlite); ' +
'the "node" driver requires Node.js with the built-in node:sqlite module (present from Node 22.13; the supported floor is Node 24 LTS).',
"sqlite",
);
driverLoadErrors.set(name, loadError);
throw loadError;
}
}