-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathsqlite.ts
More file actions
147 lines (134 loc) · 4.48 KB
/
sqlite.ts
File metadata and controls
147 lines (134 loc) · 4.48 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
/**
* SQLite adapter wrapping node:sqlite's DatabaseSync with a convenient API.
*
* This module is the single import point for all SQLite access in the
* codebase. It provides a `.query(sql).get()` / `.all()` / `.run()`
* interface and a manual `transaction()` wrapper.
*
* Uses `node:sqlite` (Node 22+) as the backing implementation. Falls back
* to `bun:sqlite` when `node:sqlite` is unavailable (Bun runtime) — this
* fallback will be removed once the test runner migrates off Bun.
*/
import { logger } from "../logger.js";
const log = logger.withTag("sqlite");
/** Valid SQLite binding value. */
export type SQLQueryBindings =
| string
| number
| bigint
| boolean
| null
| Uint8Array
| undefined;
/**
* Prepared statement wrapper exposing `.get()`, `.all()`, `.run()`.
*
* Uses a Proxy to pass through any additional driver-specific methods
* (e.g. bun:sqlite's `.values()`) while normalising `.get()` to return
* `null` (not `undefined`) for no-row results.
*/
type StatementWrapper = {
get(...params: SQLQueryBindings[]): Record<string, SQLQueryBindings> | null;
all(...params: SQLQueryBindings[]): Record<string, SQLQueryBindings>[];
run(...params: SQLQueryBindings[]): void;
/** Allow driver-specific methods (e.g. bun:sqlite `.values()`) to pass through. */
[method: string]: unknown;
};
// biome-ignore lint/suspicious/noExplicitAny: backing driver types vary
function wrapStatement(stmt: any): StatementWrapper {
return new Proxy(stmt, {
get(target, prop) {
if (prop === "get") {
return (...params: SQLQueryBindings[]) =>
// node:sqlite returns undefined for no rows; bun:sqlite returns null.
// Normalise to null so callers can rely on a single sentinel.
(target.get(...params) as Record<string, SQLQueryBindings>) ?? null;
}
const value = Reflect.get(target, prop);
if (typeof value === "function") {
return value.bind(target);
}
return value;
},
}) as StatementWrapper;
}
/**
* Resolve the underlying SQLite database constructor.
*
* Prefers `node:sqlite` (Node 22+). Falls back to `bun:sqlite` when
* `node:sqlite` is unavailable (Bun runtime). The fallback will be
* removed once the test runner migrates off Bun.
*/
function getSqliteConstructor(): new (
path: string
) => {
exec(sql: string): void;
close(): void;
} {
try {
return require("node:sqlite").DatabaseSync;
} catch (error) {
log.debug("node:sqlite unavailable, falling back to bun:sqlite", error);
return require("bun:sqlite").Database;
}
}
// biome-ignore lint/suspicious/noExplicitAny: resolved dynamically
const SqliteImpl: any = getSqliteConstructor();
/**
* SQLite database wrapper.
*
* - `exec(sql)` — execute raw SQL (DDL, multi-statement)
* - `query(sql)` — prepare a statement → `.get()` / `.all()` / `.run()`
* - `close()` — close the connection
* - `transaction(fn)` — wrap a function in BEGIN/COMMIT/ROLLBACK
*/
export class Database {
// biome-ignore lint/suspicious/noExplicitAny: backing driver resolved at runtime
private readonly db: any;
constructor(path: string) {
this.db = new SqliteImpl(path);
}
/** Execute raw SQL (DDL statements, multi-statement strings). */
exec(sql: string): void {
this.db.exec(sql);
}
/**
* Prepare a SQL statement.
* Returns a wrapper with `.get()`, `.all()`, `.run()`.
*
* Uses bun:sqlite's `.query()` (cached statements) when available,
* falling back to node:sqlite's `.prepare()`.
*/
query(sql: string): StatementWrapper {
// bun:sqlite exposes both .query() (cached) and .prepare() (fresh).
// Prefer .query() to preserve the caching semantics all consumers
// were written against. node:sqlite only has .prepare().
const prepFn = this.db.query ?? this.db.prepare;
return wrapStatement(prepFn.call(this.db, sql));
}
/** Close the database connection. */
close(): void {
this.db.close();
}
/**
* Wrap a function in a transaction. Returns a callable that executes
* the function within BEGIN/COMMIT, with ROLLBACK on error.
*/
transaction<T>(fn: () => T): () => T {
// bun:sqlite has native transaction(); node:sqlite does not
if (typeof this.db.transaction === "function") {
return this.db.transaction(fn);
}
return () => {
this.db.exec("BEGIN");
try {
const result = fn();
this.db.exec("COMMIT");
return result;
} catch (error) {
this.db.exec("ROLLBACK");
throw error;
}
};
}
}