@photostructure/sqlite 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -16
- package/README.md +5 -10
- package/binding.gyp +2 -2
- package/dist/index.cjs +314 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +346 -89
- package/dist/index.d.mts +346 -89
- package/dist/index.d.ts +346 -89
- package/dist/index.mjs +311 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +72 -63
- package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/test_extension.so +0 -0
- package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
- package/src/aggregate_function.cpp +222 -114
- package/src/aggregate_function.h +5 -6
- package/src/binding.cpp +30 -21
- package/src/enhance.ts +552 -0
- package/src/index.ts +84 -9
- package/src/shims/node_errors.h +34 -15
- package/src/shims/sqlite_errors.h +34 -8
- package/src/sql-tag-store.ts +6 -9
- package/src/sqlite_impl.cpp +1044 -394
- package/src/sqlite_impl.h +46 -7
- package/src/transaction.ts +178 -0
- package/src/types/database-sync-instance.ts +6 -40
- package/src/types/pragma-options.ts +23 -0
- package/src/types/statement-sync-instance.ts +38 -12
- package/src/types/transaction.ts +72 -0
- package/src/upstream/node_sqlite.cc +143 -43
- package/src/upstream/node_sqlite.h +15 -11
- package/src/upstream/sqlite3.c +102 -58
- package/src/upstream/sqlite3.h +5 -5
- package/src/user_function.cpp +138 -141
- package/src/user_function.h +3 -0
package/src/enhance.ts
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhancement utilities for adding better-sqlite3-style methods to any
|
|
3
|
+
* compatible database, including `node:sqlite` DatabaseSync and this package's
|
|
4
|
+
* DatabaseSync.
|
|
5
|
+
*
|
|
6
|
+
* This module provides the `enhance()` function which adds `.pragma()`,
|
|
7
|
+
* `.transaction()`, and statement modes (`.pluck()`, `.raw()`, `.expand()`)
|
|
8
|
+
* to database instances that don't have them (e.g., node:sqlite DatabaseSync).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createTransaction } from "./transaction";
|
|
12
|
+
import type { PragmaOptions } from "./types/pragma-options";
|
|
13
|
+
import type { TransactionFunction } from "./types/transaction";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal interface for a database that can be enhanced. This matches the
|
|
17
|
+
* subset of functionality needed by pragma() and transaction().
|
|
18
|
+
*/
|
|
19
|
+
export interface EnhanceableDatabaseSync {
|
|
20
|
+
/** Execute SQL without returning results */
|
|
21
|
+
exec(sql: string): void;
|
|
22
|
+
/** Prepare a statement that can return results */
|
|
23
|
+
prepare(sql: string): { all(): unknown[] };
|
|
24
|
+
/** Whether the database connection is open */
|
|
25
|
+
readonly isOpen?: boolean;
|
|
26
|
+
/** Whether a transaction is currently active */
|
|
27
|
+
readonly isTransaction: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Statement mode matching better-sqlite3's mutually exclusive modes.
|
|
32
|
+
* - "flat": Default — rows as `{ column: value }` objects
|
|
33
|
+
* - "pluck": First column value only
|
|
34
|
+
* - "raw": Rows as arrays of values
|
|
35
|
+
* - "expand": Rows namespaced by table, e.g. `{ users: { id: 1 }, posts: { title: "..." } }`
|
|
36
|
+
*/
|
|
37
|
+
type StatementMode = "flat" | "pluck" | "raw" | "expand";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A statement enhanced with better-sqlite3-style `.pluck()`, `.raw()`, and
|
|
41
|
+
* `.expand()` methods. These are mutually exclusive — enabling one disables
|
|
42
|
+
* the others.
|
|
43
|
+
*/
|
|
44
|
+
export interface EnhancedStatementMethods {
|
|
45
|
+
/**
|
|
46
|
+
* Causes the statement to return only the first column value of each row.
|
|
47
|
+
*
|
|
48
|
+
* When plucking is turned on, raw and expand modes are turned off.
|
|
49
|
+
*
|
|
50
|
+
* @param toggle Enable (true) or disable (false) pluck mode. Defaults to true.
|
|
51
|
+
* @returns The same statement for chaining.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const count = db.prepare("SELECT COUNT(*) FROM users").pluck().get();
|
|
56
|
+
* // Returns: 42 (not { "COUNT(*)": 42 })
|
|
57
|
+
*
|
|
58
|
+
* const names = db.prepare("SELECT name FROM users").pluck().all();
|
|
59
|
+
* // Returns: ["Alice", "Bob"] (not [{ name: "Alice" }, { name: "Bob" }])
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
pluck(toggle?: boolean): this;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Causes the statement to return rows as arrays of values instead of objects.
|
|
66
|
+
*
|
|
67
|
+
* When raw mode is turned on, pluck and expand modes are turned off.
|
|
68
|
+
*
|
|
69
|
+
* @param toggle Enable (true) or disable (false) raw mode. Defaults to true.
|
|
70
|
+
* @returns The same statement for chaining.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const rows = db.prepare("SELECT id, name FROM users").raw().all();
|
|
75
|
+
* // Returns: [[1, "Alice"], [2, "Bob"]] (not [{ id: 1, name: "Alice" }, ...])
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
raw(toggle?: boolean): this;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Causes the statement to return data namespaced by table. Each key in a row
|
|
82
|
+
* object will be a table name, and each corresponding value will be a nested
|
|
83
|
+
* object containing that table's columns. Columns from expressions or
|
|
84
|
+
* subqueries are placed under the special `$` namespace.
|
|
85
|
+
*
|
|
86
|
+
* When expand mode is turned on, pluck and raw modes are turned off.
|
|
87
|
+
*
|
|
88
|
+
* Requires the statement to have a `.columns()` method (available on real
|
|
89
|
+
* statements but not minimal mocks).
|
|
90
|
+
*
|
|
91
|
+
* @param toggle Enable (true) or disable (false) expand mode. Defaults to true.
|
|
92
|
+
* @returns The same statement for chaining.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const rows = db.prepare("SELECT u.id, u.name, p.title FROM users u JOIN posts p ON ...").expand().all();
|
|
97
|
+
* // Returns: [{ users: { id: 1, name: "Alice" }, posts: { title: "Hello" } }]
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
expand(toggle?: boolean): this;
|
|
101
|
+
|
|
102
|
+
/** The database instance this statement was prepared from. */
|
|
103
|
+
readonly database: EnhanceableDatabaseSync;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Interface for an enhanced database with pragma() and transaction() methods.
|
|
108
|
+
*/
|
|
109
|
+
export interface EnhancedMethods {
|
|
110
|
+
/**
|
|
111
|
+
* Executes a PRAGMA statement and returns its result.
|
|
112
|
+
*
|
|
113
|
+
* @param source The PRAGMA command (without "PRAGMA" prefix)
|
|
114
|
+
* @param options Optional configuration
|
|
115
|
+
* @returns Array of rows, or single value if `simple: true`
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* db.pragma('cache_size', { simple: true }); // -16000
|
|
120
|
+
* db.pragma('journal_mode = wal');
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
pragma(source: string, options?: PragmaOptions): unknown;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a function that always runs inside a transaction.
|
|
127
|
+
*
|
|
128
|
+
* @param fn The function to wrap in a transaction
|
|
129
|
+
* @returns A transaction function with `.deferred`, `.immediate`,
|
|
130
|
+
* `.exclusive` variants
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const insertMany = db.transaction((items) => {
|
|
135
|
+
* for (const item of items) insert.run(item);
|
|
136
|
+
* });
|
|
137
|
+
* insertMany(['a', 'b', 'c']); // All in one transaction
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
transaction<F extends (...args: any[]) => any>(fn: F): TransactionFunction<F>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A database instance that has been enhanced with pragma(), transaction(),
|
|
145
|
+
* and statement modes (pluck/raw/expand) on statements returned by prepare().
|
|
146
|
+
*/
|
|
147
|
+
export type EnhancedDatabaseSync<T extends EnhanceableDatabaseSync> = Omit<
|
|
148
|
+
T,
|
|
149
|
+
"prepare"
|
|
150
|
+
> &
|
|
151
|
+
EnhancedMethods & {
|
|
152
|
+
prepare(
|
|
153
|
+
...args: Parameters<T["prepare"]>
|
|
154
|
+
): ReturnType<T["prepare"]> & EnhancedStatementMethods;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Symbol to track whether prepare() has been wrapped */
|
|
158
|
+
const ENHANCED_PREPARE = Symbol.for("@photostructure/sqlite:enhancedPrepare");
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract the first column value from a row object or array.
|
|
162
|
+
*/
|
|
163
|
+
function extractFirstColumn(row: unknown): unknown {
|
|
164
|
+
if (row == null) return row;
|
|
165
|
+
if (Array.isArray(row)) return row[0];
|
|
166
|
+
const keys = Object.keys(row as Record<string, unknown>);
|
|
167
|
+
return keys.length > 0
|
|
168
|
+
? (row as Record<string, unknown>)[keys[0]!]
|
|
169
|
+
: undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a column-to-table mapping from statement metadata, used by expand mode.
|
|
174
|
+
* Returns an array parallel to column order: each entry is the table name
|
|
175
|
+
* (or "$" for expressions/subqueries) and the column name.
|
|
176
|
+
*/
|
|
177
|
+
function buildColumnTableMap(
|
|
178
|
+
stmt: any,
|
|
179
|
+
): Array<{ table: string; column: string }> | undefined {
|
|
180
|
+
if (typeof stmt.columns !== "function") return undefined;
|
|
181
|
+
const cols: Array<{ name: string; table: string | null }> = stmt.columns();
|
|
182
|
+
return cols.map((c) => ({
|
|
183
|
+
table: c.table ?? "$",
|
|
184
|
+
column: c.name,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Transform a row array into a table-namespaced expanded object.
|
|
190
|
+
* Uses array indices to match columns, avoiding data loss from duplicate names.
|
|
191
|
+
*/
|
|
192
|
+
function expandRowFromArray(
|
|
193
|
+
row: unknown[],
|
|
194
|
+
columnMap: Array<{ table: string; column: string }>,
|
|
195
|
+
): Record<string, Record<string, unknown>> {
|
|
196
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
197
|
+
for (let i = 0; i < columnMap.length && i < row.length; i++) {
|
|
198
|
+
const { table, column } = columnMap[i]!;
|
|
199
|
+
result[table] ??= {};
|
|
200
|
+
result[table]![column] = row[i];
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Transform a flat row object into a table-namespaced expanded object.
|
|
207
|
+
* Fallback for mocks without setReturnArrays — cannot handle duplicate column
|
|
208
|
+
* names correctly, but mocks typically don't produce them.
|
|
209
|
+
*/
|
|
210
|
+
function expandRowFromObject(
|
|
211
|
+
row: Record<string, unknown>,
|
|
212
|
+
columnMap: Array<{ table: string; column: string }>,
|
|
213
|
+
): Record<string, Record<string, unknown>> {
|
|
214
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
215
|
+
const keys = Object.keys(row);
|
|
216
|
+
for (let i = 0; i < keys.length && i < columnMap.length; i++) {
|
|
217
|
+
const { table, column } = columnMap[i]!;
|
|
218
|
+
result[table] ??= {};
|
|
219
|
+
result[table]![column] = row[keys[i]!];
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Validate a boolean toggle argument, matching better-sqlite3's pattern.
|
|
226
|
+
*/
|
|
227
|
+
function validateToggle(value: unknown): boolean {
|
|
228
|
+
const use = value === undefined ? true : value;
|
|
229
|
+
if (typeof use !== "boolean") {
|
|
230
|
+
throw new TypeError("Expected first argument to be a boolean");
|
|
231
|
+
}
|
|
232
|
+
return use;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Transform a row based on the current statement mode.
|
|
237
|
+
*/
|
|
238
|
+
function transformRow(
|
|
239
|
+
row: unknown,
|
|
240
|
+
mode: StatementMode,
|
|
241
|
+
columnMap: Array<{ table: string; column: string }> | undefined,
|
|
242
|
+
): unknown {
|
|
243
|
+
if (row == null || mode === "flat") return row;
|
|
244
|
+
if (mode === "pluck") return extractFirstColumn(row);
|
|
245
|
+
if (mode === "expand") {
|
|
246
|
+
// columnMap is guaranteed non-null here — setMode() throws if columns() is
|
|
247
|
+
// unavailable, so we'll never reach this branch with columnMap == null.
|
|
248
|
+
// Prefer array-based expand (used when setReturnArrays is available)
|
|
249
|
+
// to correctly handle duplicate column names across tables.
|
|
250
|
+
// Fall back to object-based expand for mocks without setReturnArrays.
|
|
251
|
+
if (Array.isArray(row)) {
|
|
252
|
+
return expandRowFromArray(row, columnMap!);
|
|
253
|
+
}
|
|
254
|
+
return expandRowFromObject(row as Record<string, unknown>, columnMap!);
|
|
255
|
+
}
|
|
256
|
+
// "raw" mode is handled natively by setReturnArrays(), so no transform needed
|
|
257
|
+
return row;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Add `.pluck()`, `.raw()`, and `.expand()` methods to a statement instance.
|
|
262
|
+
* These modes are mutually exclusive — enabling one disables the others,
|
|
263
|
+
* matching better-sqlite3's behavior.
|
|
264
|
+
*/
|
|
265
|
+
function enhanceStatement<S extends { all(): unknown[] }>(
|
|
266
|
+
stmt: S,
|
|
267
|
+
): S & EnhancedStatementMethods {
|
|
268
|
+
// Idempotency: if already enhanced, return as-is
|
|
269
|
+
if (typeof (stmt as any).pluck === "function") {
|
|
270
|
+
return stmt as S & EnhancedStatementMethods;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let mode: StatementMode = "flat";
|
|
274
|
+
let columnMap: Array<{ table: string; column: string }> | undefined;
|
|
275
|
+
|
|
276
|
+
// Cast to any to avoid TypeScript strictness around bound method signatures.
|
|
277
|
+
// At runtime these are native C++ methods that accept variadic bind parameters.
|
|
278
|
+
const originalGet: any =
|
|
279
|
+
typeof (stmt as any).get === "function"
|
|
280
|
+
? (stmt as any).get.bind(stmt)
|
|
281
|
+
: undefined;
|
|
282
|
+
const originalAll: any = (stmt as any).all.bind(stmt);
|
|
283
|
+
const originalIterate: any =
|
|
284
|
+
typeof (stmt as any).iterate === "function"
|
|
285
|
+
? (stmt as any).iterate.bind(stmt)
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
// Toggle helper matching better-sqlite3's mode switching:
|
|
289
|
+
// enable(true) sets to target mode; enable(false) resets to flat ONLY if
|
|
290
|
+
// currently in that mode, otherwise leaves mode unchanged.
|
|
291
|
+
function setMode(target: StatementMode, use: boolean): void {
|
|
292
|
+
if (use) {
|
|
293
|
+
mode = target;
|
|
294
|
+
// Cache column map on first expand() call
|
|
295
|
+
if (target === "expand" && columnMap == null) {
|
|
296
|
+
columnMap = buildColumnTableMap(stmt);
|
|
297
|
+
if (columnMap == null) {
|
|
298
|
+
throw new TypeError(
|
|
299
|
+
"expand() requires the statement to have a columns() method",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} else if (mode === target) {
|
|
304
|
+
mode = "flat";
|
|
305
|
+
}
|
|
306
|
+
// If use=false and mode !== target, no-op (matches better-sqlite3)
|
|
307
|
+
|
|
308
|
+
// Sync native array mode: both raw and expand need arrays from the native layer.
|
|
309
|
+
// Expand needs arrays to correctly handle duplicate column names across tables.
|
|
310
|
+
if (typeof (stmt as any).setReturnArrays === "function") {
|
|
311
|
+
(stmt as any).setReturnArrays(mode === "raw" || mode === "expand");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Object.defineProperty(stmt, "pluck", {
|
|
316
|
+
value: function pluck(toggle?: boolean): S {
|
|
317
|
+
setMode("pluck", validateToggle(toggle));
|
|
318
|
+
return stmt;
|
|
319
|
+
},
|
|
320
|
+
writable: true,
|
|
321
|
+
configurable: true,
|
|
322
|
+
enumerable: false,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
Object.defineProperty(stmt, "raw", {
|
|
326
|
+
value: function raw(toggle?: boolean): S {
|
|
327
|
+
setMode("raw", validateToggle(toggle));
|
|
328
|
+
return stmt;
|
|
329
|
+
},
|
|
330
|
+
writable: true,
|
|
331
|
+
configurable: true,
|
|
332
|
+
enumerable: false,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
Object.defineProperty(stmt, "expand", {
|
|
336
|
+
value: function expand(toggle?: boolean): S {
|
|
337
|
+
setMode("expand", validateToggle(toggle));
|
|
338
|
+
return stmt;
|
|
339
|
+
},
|
|
340
|
+
writable: true,
|
|
341
|
+
configurable: true,
|
|
342
|
+
enumerable: false,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (originalGet != null) {
|
|
346
|
+
Object.defineProperty(stmt, "get", {
|
|
347
|
+
value: (...params: any[]) => {
|
|
348
|
+
const row = originalGet(...params);
|
|
349
|
+
return transformRow(row, mode, columnMap);
|
|
350
|
+
},
|
|
351
|
+
writable: true,
|
|
352
|
+
configurable: true,
|
|
353
|
+
enumerable: false,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Object.defineProperty(stmt, "all", {
|
|
358
|
+
value: (...params: any[]) => {
|
|
359
|
+
const rows = originalAll(...params);
|
|
360
|
+
if (mode === "flat" || mode === "raw") return rows;
|
|
361
|
+
return rows.map((row: unknown) => transformRow(row, mode, columnMap));
|
|
362
|
+
},
|
|
363
|
+
writable: true,
|
|
364
|
+
configurable: true,
|
|
365
|
+
enumerable: false,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (originalIterate != null) {
|
|
369
|
+
Object.defineProperty(stmt, "iterate", {
|
|
370
|
+
value: function* (...params: any[]) {
|
|
371
|
+
const iter = originalIterate(...params);
|
|
372
|
+
for (const row of iter) {
|
|
373
|
+
yield transformRow(row, mode, columnMap);
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
writable: true,
|
|
377
|
+
configurable: true,
|
|
378
|
+
enumerable: false,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return stmt as S & EnhancedStatementMethods;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Implementation of pragma() that works on any EnhanceableDatabaseSync.
|
|
387
|
+
*/
|
|
388
|
+
function pragmaImpl(
|
|
389
|
+
this: EnhanceableDatabaseSync,
|
|
390
|
+
source: string,
|
|
391
|
+
options?: PragmaOptions,
|
|
392
|
+
): unknown {
|
|
393
|
+
if (typeof source !== "string") {
|
|
394
|
+
throw new TypeError("Expected first argument to be a string");
|
|
395
|
+
}
|
|
396
|
+
if (options != null && typeof options !== "object") {
|
|
397
|
+
throw new TypeError("Expected second argument to be an options object");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const simple = options?.simple === true;
|
|
401
|
+
|
|
402
|
+
// Validate that simple is a boolean if provided
|
|
403
|
+
if (
|
|
404
|
+
options != null &&
|
|
405
|
+
"simple" in options &&
|
|
406
|
+
typeof options.simple !== "boolean"
|
|
407
|
+
) {
|
|
408
|
+
throw new TypeError('Expected the "simple" option to be a boolean');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const stmt = this.prepare(`PRAGMA ${source}`);
|
|
412
|
+
|
|
413
|
+
if (simple) {
|
|
414
|
+
return extractFirstColumn(stmt.all()[0]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return stmt.all();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Implementation of transaction() that works on any EnhanceableDatabaseSync.
|
|
422
|
+
*/
|
|
423
|
+
function transactionImpl<F extends (...args: any[]) => any>(
|
|
424
|
+
this: EnhanceableDatabaseSync,
|
|
425
|
+
fn: F,
|
|
426
|
+
): TransactionFunction<F> {
|
|
427
|
+
// createTransaction expects DatabaseSyncInstance but only uses the subset
|
|
428
|
+
// defined in EnhanceableDatabaseSync, so this cast is safe
|
|
429
|
+
return createTransaction(this as any, fn);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Checks if a database instance already has the enhanced methods.
|
|
434
|
+
*/
|
|
435
|
+
function hasEnhancedMethods(
|
|
436
|
+
db: EnhanceableDatabaseSync,
|
|
437
|
+
): db is EnhanceableDatabaseSync & EnhancedMethods {
|
|
438
|
+
return (
|
|
439
|
+
typeof (db as any).pragma === "function" &&
|
|
440
|
+
typeof (db as any).transaction === "function"
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Ensures that `.pragma()`, `.transaction()`, and statement modes
|
|
446
|
+
* (`.pluck()`, `.raw()`, `.expand()`) are available on the given database.
|
|
447
|
+
*
|
|
448
|
+
* This function can enhance:
|
|
449
|
+
* - `node:sqlite` DatabaseSync instances (adds the methods)
|
|
450
|
+
* - `@photostructure/sqlite` DatabaseSync instances (adds the methods)
|
|
451
|
+
* - Any object with compatible `exec()`, `prepare()`, and `isTransaction`
|
|
452
|
+
*
|
|
453
|
+
* The enhancement is done by adding methods directly to the instance, not the
|
|
454
|
+
* prototype, so it won't affect other instances or the original class.
|
|
455
|
+
*
|
|
456
|
+
* @param db The database instance to enhance
|
|
457
|
+
* @returns The same instance with `.pragma()`, `.transaction()`, and
|
|
458
|
+
* `.pluck()` / `.raw()` / `.expand()` (on prepared statements) guaranteed
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* import { DatabaseSync, enhance } from '@photostructure/sqlite';
|
|
463
|
+
*
|
|
464
|
+
* const db = enhance(new DatabaseSync(':memory:'));
|
|
465
|
+
*
|
|
466
|
+
* // better-sqlite3-style pragma
|
|
467
|
+
* db.pragma('journal_mode = wal');
|
|
468
|
+
*
|
|
469
|
+
* // better-sqlite3-style transactions
|
|
470
|
+
* const insertMany = db.transaction((items) => {
|
|
471
|
+
* for (const item of items) insert.run(item);
|
|
472
|
+
* });
|
|
473
|
+
*
|
|
474
|
+
* // better-sqlite3-style pluck
|
|
475
|
+
* const count = db.prepare("SELECT COUNT(*) FROM users").pluck().get();
|
|
476
|
+
* const names = db.prepare("SELECT name FROM users").pluck().all();
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
export function enhance<T extends EnhanceableDatabaseSync>(
|
|
480
|
+
db: T,
|
|
481
|
+
): EnhancedDatabaseSync<T> {
|
|
482
|
+
// Add pragma and transaction if not already present
|
|
483
|
+
if (!hasEnhancedMethods(db)) {
|
|
484
|
+
// Using Object.defineProperty to make them non-enumerable like native methods
|
|
485
|
+
Object.defineProperty(db, "pragma", {
|
|
486
|
+
value: pragmaImpl,
|
|
487
|
+
writable: true,
|
|
488
|
+
configurable: true,
|
|
489
|
+
enumerable: false,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
Object.defineProperty(db, "transaction", {
|
|
493
|
+
value: transactionImpl,
|
|
494
|
+
writable: true,
|
|
495
|
+
configurable: true,
|
|
496
|
+
enumerable: false,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Wrap prepare() to add pluck() to returned statements
|
|
501
|
+
if (!(db as any)[ENHANCED_PREPARE]) {
|
|
502
|
+
const originalPrepare: any = db.prepare.bind(db);
|
|
503
|
+
|
|
504
|
+
Object.defineProperty(db, "prepare", {
|
|
505
|
+
value: (...args: any[]) => {
|
|
506
|
+
const stmt = originalPrepare(...args);
|
|
507
|
+
enhanceStatement(stmt);
|
|
508
|
+
// Add stmt.database back-reference for better-sqlite3 compat
|
|
509
|
+
Object.defineProperty(stmt, "database", {
|
|
510
|
+
value: db,
|
|
511
|
+
writable: false,
|
|
512
|
+
configurable: true,
|
|
513
|
+
enumerable: false,
|
|
514
|
+
});
|
|
515
|
+
return stmt;
|
|
516
|
+
},
|
|
517
|
+
writable: true,
|
|
518
|
+
configurable: true,
|
|
519
|
+
enumerable: false,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
Object.defineProperty(db, ENHANCED_PREPARE, {
|
|
523
|
+
value: true,
|
|
524
|
+
writable: false,
|
|
525
|
+
configurable: false,
|
|
526
|
+
enumerable: false,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return db as unknown as EnhancedDatabaseSync<T>;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Type guard to check if a database has enhanced methods.
|
|
535
|
+
*
|
|
536
|
+
* @param db The database to check
|
|
537
|
+
* @returns True if the database has `.pragma()` and `.transaction()` methods
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* import { isEnhanced } from '@photostructure/sqlite';
|
|
542
|
+
*
|
|
543
|
+
* if (isEnhanced(db)) {
|
|
544
|
+
* db.pragma('cache_size', { simple: true });
|
|
545
|
+
* }
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
export function isEnhanced(
|
|
549
|
+
db: EnhanceableDatabaseSync,
|
|
550
|
+
): db is EnhanceableDatabaseSync & EnhancedMethods {
|
|
551
|
+
return hasEnhancedMethods(db);
|
|
552
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type { AggregateOptions } from "./types/aggregate-options";
|
|
|
17
17
|
export type { ChangesetApplyOptions } from "./types/changeset-apply-options";
|
|
18
18
|
export type { DatabaseSyncInstance } from "./types/database-sync-instance";
|
|
19
19
|
export type { DatabaseSyncOptions } from "./types/database-sync-options";
|
|
20
|
+
export type { PragmaOptions } from "./types/pragma-options";
|
|
20
21
|
export type { SessionOptions } from "./types/session-options";
|
|
21
22
|
export type { SQLTagStoreInstance } from "./types/sql-tag-store-instance";
|
|
22
23
|
export type { SqliteAuthorizationActions } from "./types/sqlite-authorization-actions";
|
|
@@ -24,9 +25,23 @@ export type { SqliteAuthorizationResults } from "./types/sqlite-authorization-re
|
|
|
24
25
|
export type { SqliteChangesetConflictTypes } from "./types/sqlite-changeset-conflict-types";
|
|
25
26
|
export type { SqliteChangesetResolution } from "./types/sqlite-changeset-resolution";
|
|
26
27
|
export type { SqliteOpenFlags } from "./types/sqlite-open-flags";
|
|
27
|
-
export type {
|
|
28
|
+
export type {
|
|
29
|
+
StatementColumnMetadata,
|
|
30
|
+
StatementSyncInstance,
|
|
31
|
+
} from "./types/statement-sync-instance";
|
|
32
|
+
export type { TransactionFunction, TransactionMode } from "./types/transaction";
|
|
28
33
|
export type { UserFunctionOptions } from "./types/user-functions-options";
|
|
29
34
|
|
|
35
|
+
// Enhancement utilities for adding better-sqlite3-style methods to any compatible database
|
|
36
|
+
export {
|
|
37
|
+
enhance,
|
|
38
|
+
isEnhanced,
|
|
39
|
+
type EnhanceableDatabaseSync,
|
|
40
|
+
type EnhancedDatabaseSync,
|
|
41
|
+
type EnhancedMethods,
|
|
42
|
+
type EnhancedStatementMethods,
|
|
43
|
+
} from "./enhance";
|
|
44
|
+
|
|
30
45
|
// Use _dirname() helper that works in both CJS/ESM and Jest
|
|
31
46
|
const binding = nodeGypBuild(join(_dirname(), ".."));
|
|
32
47
|
|
|
@@ -52,25 +67,54 @@ export type SqliteConstants = SqliteOpenFlags &
|
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
69
|
* Options for creating a prepared statement.
|
|
70
|
+
*
|
|
71
|
+
* **Note:** The per-statement override options (`readBigInts`, `returnArrays`,
|
|
72
|
+
* `allowBareNamedParameters`, `allowUnknownNamedParameters`) are a **Node.js v25+**
|
|
73
|
+
* feature. On Node.js v24 and earlier, `node:sqlite` silently ignores these options.
|
|
74
|
+
* This library implements them for forward compatibility with Node.js v25+.
|
|
55
75
|
*/
|
|
56
76
|
export interface StatementOptions {
|
|
57
77
|
/** If true, the prepared statement's expandedSQL property will contain the expanded SQL. @default false */
|
|
58
78
|
readonly expandedSQL?: boolean;
|
|
59
79
|
/** If true, anonymous parameters are enabled for the statement. @default false */
|
|
60
80
|
readonly anonymousParameters?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* If true, read integer values as JavaScript BigInt. Overrides database-level setting.
|
|
83
|
+
* **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
|
|
84
|
+
* @default database default
|
|
85
|
+
*/
|
|
86
|
+
readonly readBigInts?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* If true, return results as arrays rather than objects. Overrides database-level setting.
|
|
89
|
+
* **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
|
|
90
|
+
* @default database default
|
|
91
|
+
*/
|
|
92
|
+
readonly returnArrays?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* If true, allows bare named parameters (without prefix). Overrides database-level setting.
|
|
95
|
+
* **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
|
|
96
|
+
* @default database default
|
|
97
|
+
*/
|
|
98
|
+
readonly allowBareNamedParameters?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* If true, unknown named parameters are ignored. Overrides database-level setting.
|
|
101
|
+
* **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
|
|
102
|
+
* @default database default
|
|
103
|
+
*/
|
|
104
|
+
readonly allowUnknownNamedParameters?: boolean;
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
export interface Session {
|
|
64
108
|
/**
|
|
65
109
|
* Generate a changeset containing all changes recorded by the session.
|
|
66
|
-
* @returns A
|
|
110
|
+
* @returns A Uint8Array containing the changeset data.
|
|
67
111
|
*/
|
|
68
|
-
changeset():
|
|
112
|
+
changeset(): Uint8Array;
|
|
69
113
|
/**
|
|
70
114
|
* Generate a patchset containing all changes recorded by the session.
|
|
71
|
-
* @returns A
|
|
115
|
+
* @returns A Uint8Array containing the patchset data.
|
|
72
116
|
*/
|
|
73
|
-
patchset():
|
|
117
|
+
patchset(): Uint8Array;
|
|
74
118
|
/**
|
|
75
119
|
* Close the session and release its resources.
|
|
76
120
|
*/
|
|
@@ -133,8 +177,20 @@ export interface SqliteModule {
|
|
|
133
177
|
* const readOnlyDb = new DatabaseSync('./data.db', { readOnly: true });
|
|
134
178
|
* ```
|
|
135
179
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
// Store the native binding's DatabaseSync
|
|
181
|
+
const _DatabaseSync = binding.DatabaseSync;
|
|
182
|
+
// Wrapper around the native constructor to enforce usage of `new` with the correct error code.
|
|
183
|
+
// We use a function wrapper instead of a Proxy for better performance and explicit prototype handling.
|
|
184
|
+
export const DatabaseSync = function DatabaseSync(this: any, ...args: any[]) {
|
|
185
|
+
if (!new.target) {
|
|
186
|
+
const err = new TypeError("Cannot call constructor without `new`");
|
|
187
|
+
(err as NodeJS.ErrnoException).code = "ERR_CONSTRUCT_CALL_REQUIRED";
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
return Reflect.construct(_DatabaseSync, args, new.target);
|
|
191
|
+
} as unknown as SqliteModule["DatabaseSync"];
|
|
192
|
+
Object.setPrototypeOf(DatabaseSync, _DatabaseSync);
|
|
193
|
+
DatabaseSync.prototype = _DatabaseSync.prototype;
|
|
138
194
|
|
|
139
195
|
// node:sqlite implements createTagStore and SQLTagStore entirely in native C++.
|
|
140
196
|
// We use a TypeScript implementation instead, attached via prototype extension.
|
|
@@ -148,6 +204,15 @@ export const DatabaseSync =
|
|
|
148
204
|
return new SQLTagStore(this, capacity);
|
|
149
205
|
};
|
|
150
206
|
|
|
207
|
+
// NOTE: .pragma() and .transaction() are NOT added to the prototype by default.
|
|
208
|
+
// This keeps DatabaseSync 100% API-compatible with node:sqlite.
|
|
209
|
+
// Users who want better-sqlite3-style methods should use enhance():
|
|
210
|
+
//
|
|
211
|
+
// import { DatabaseSync, enhance } from '@photostructure/sqlite';
|
|
212
|
+
// const db = enhance(new DatabaseSync(':memory:'));
|
|
213
|
+
// db.pragma('journal_mode', { simple: true });
|
|
214
|
+
// db.transaction(() => { ... });
|
|
215
|
+
|
|
151
216
|
/**
|
|
152
217
|
* The StatementSync class represents a prepared SQL statement.
|
|
153
218
|
* This class should not be instantiated directly; use DatabaseSync.prepare() instead.
|
|
@@ -159,8 +224,18 @@ export const DatabaseSync =
|
|
|
159
224
|
* stmt.finalize();
|
|
160
225
|
* ```
|
|
161
226
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
227
|
+
// Store the native binding's StatementSync for internal use
|
|
228
|
+
const _StatementSync = binding.StatementSync;
|
|
229
|
+
// Export a wrapper that throws ERR_ILLEGAL_CONSTRUCTOR when called directly
|
|
230
|
+
// but preserves instanceof checks and prototype chain
|
|
231
|
+
export const StatementSync = function StatementSync() {
|
|
232
|
+
const err = new TypeError("Illegal constructor");
|
|
233
|
+
(err as NodeJS.ErrnoException).code = "ERR_ILLEGAL_CONSTRUCTOR";
|
|
234
|
+
throw err;
|
|
235
|
+
} as unknown as SqliteModule["StatementSync"];
|
|
236
|
+
// Use the native prototype directly so instanceof checks work correctly
|
|
237
|
+
// (stmt instanceof StatementSync will check if StatementSync.prototype is in stmt's chain)
|
|
238
|
+
StatementSync.prototype = _StatementSync.prototype;
|
|
164
239
|
|
|
165
240
|
/**
|
|
166
241
|
* The Session class for recording database changes.
|