@photostructure/sqlite 0.4.0 → 1.0.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 +30 -1
- package/README.md +4 -8
- package/binding.gyp +2 -0
- package/dist/index.cjs +247 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -24
- package/dist/index.d.mts +145 -24
- package/dist/index.d.ts +145 -24
- package/dist/index.mjs +247 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -11
- 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/enhance.ts +382 -55
- package/src/index.ts +85 -2
- package/src/sqlite_impl.cpp +133 -10
- package/src/sqlite_impl.h +19 -0
- package/src/types/database-sync-instance.ts +43 -0
- package/src/types/database-sync-options.ts +19 -0
- package/src/upstream/node_sqlite.cc +312 -17
- package/src/upstream/node_sqlite.h +80 -0
- package/src/upstream/sqlite.js +0 -3
- package/src/upstream/sqlite3.c +5027 -3518
- package/src/upstream/sqlite3.h +195 -58
- package/src/upstream/sqlite3ext.h +10 -1
package/src/enhance.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* compatible database, including `node:sqlite` DatabaseSync and this package's
|
|
4
4
|
* DatabaseSync.
|
|
5
5
|
*
|
|
6
|
-
* This module provides the `enhance()` function which adds `.pragma()
|
|
7
|
-
* `.transaction()
|
|
8
|
-
* node:sqlite DatabaseSync).
|
|
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
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createTransaction } from "./transaction";
|
|
@@ -21,10 +21,88 @@ export interface EnhanceableDatabaseSync {
|
|
|
21
21
|
exec(sql: string): void;
|
|
22
22
|
/** Prepare a statement that can return results */
|
|
23
23
|
prepare(sql: string): { all(): unknown[] };
|
|
24
|
+
/** Whether the database connection is open */
|
|
25
|
+
readonly isOpen?: boolean;
|
|
24
26
|
/** Whether a transaction is currently active */
|
|
25
27
|
readonly isTransaction: boolean;
|
|
26
28
|
}
|
|
27
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
|
+
|
|
28
106
|
/**
|
|
29
107
|
* Interface for an enhanced database with pragma() and transaction() methods.
|
|
30
108
|
*/
|
|
@@ -63,10 +141,248 @@ export interface EnhancedMethods {
|
|
|
63
141
|
}
|
|
64
142
|
|
|
65
143
|
/**
|
|
66
|
-
* A database instance that has been enhanced with pragma()
|
|
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.
|
|
67
176
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
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]!; // eslint-disable-line security/detect-object-injection
|
|
199
|
+
// eslint-disable-next-line security/detect-object-injection -- table/column from our own columnMap
|
|
200
|
+
result[table] ??= {};
|
|
201
|
+
result[table]![column] = row[i]; // eslint-disable-line security/detect-object-injection
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Transform a flat row object into a table-namespaced expanded object.
|
|
208
|
+
* Fallback for mocks without setReturnArrays — cannot handle duplicate column
|
|
209
|
+
* names correctly, but mocks typically don't produce them.
|
|
210
|
+
*/
|
|
211
|
+
function expandRowFromObject(
|
|
212
|
+
row: Record<string, unknown>,
|
|
213
|
+
columnMap: Array<{ table: string; column: string }>,
|
|
214
|
+
): Record<string, Record<string, unknown>> {
|
|
215
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
216
|
+
const keys = Object.keys(row);
|
|
217
|
+
for (let i = 0; i < keys.length && i < columnMap.length; i++) {
|
|
218
|
+
const { table, column } = columnMap[i]!; // eslint-disable-line security/detect-object-injection
|
|
219
|
+
// eslint-disable-next-line security/detect-object-injection -- table/column from our own columnMap
|
|
220
|
+
result[table] ??= {};
|
|
221
|
+
result[table]![column] = row[keys[i]!]; // eslint-disable-line security/detect-object-injection
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Validate a boolean toggle argument, matching better-sqlite3's pattern.
|
|
228
|
+
*/
|
|
229
|
+
function validateToggle(value: unknown): boolean {
|
|
230
|
+
const use = value === undefined ? true : value;
|
|
231
|
+
if (typeof use !== "boolean") {
|
|
232
|
+
throw new TypeError("Expected first argument to be a boolean");
|
|
233
|
+
}
|
|
234
|
+
return use;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Transform a row based on the current statement mode.
|
|
239
|
+
*/
|
|
240
|
+
function transformRow(
|
|
241
|
+
row: unknown,
|
|
242
|
+
mode: StatementMode,
|
|
243
|
+
columnMap: Array<{ table: string; column: string }> | undefined,
|
|
244
|
+
): unknown {
|
|
245
|
+
if (row == null || mode === "flat") return row;
|
|
246
|
+
if (mode === "pluck") return extractFirstColumn(row);
|
|
247
|
+
if (mode === "expand") {
|
|
248
|
+
// columnMap is guaranteed non-null here — setMode() throws if columns() is
|
|
249
|
+
// unavailable, so we'll never reach this branch with columnMap == null.
|
|
250
|
+
// Prefer array-based expand (used when setReturnArrays is available)
|
|
251
|
+
// to correctly handle duplicate column names across tables.
|
|
252
|
+
// Fall back to object-based expand for mocks without setReturnArrays.
|
|
253
|
+
if (Array.isArray(row)) {
|
|
254
|
+
return expandRowFromArray(row, columnMap!);
|
|
255
|
+
}
|
|
256
|
+
return expandRowFromObject(row as Record<string, unknown>, columnMap!);
|
|
257
|
+
}
|
|
258
|
+
// "raw" mode is handled natively by setReturnArrays(), so no transform needed
|
|
259
|
+
return row;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Add `.pluck()`, `.raw()`, and `.expand()` methods to a statement instance.
|
|
264
|
+
* These modes are mutually exclusive — enabling one disables the others,
|
|
265
|
+
* matching better-sqlite3's behavior.
|
|
266
|
+
*/
|
|
267
|
+
function enhanceStatement<S extends { all(): unknown[] }>(
|
|
268
|
+
stmt: S,
|
|
269
|
+
): S & EnhancedStatementMethods {
|
|
270
|
+
// Idempotency: if already enhanced, return as-is
|
|
271
|
+
if (typeof (stmt as any).pluck === "function") {
|
|
272
|
+
return stmt as S & EnhancedStatementMethods;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let mode: StatementMode = "flat";
|
|
276
|
+
let columnMap: Array<{ table: string; column: string }> | undefined;
|
|
277
|
+
|
|
278
|
+
// Cast to any to avoid TypeScript strictness around bound method signatures.
|
|
279
|
+
// At runtime these are native C++ methods that accept variadic bind parameters.
|
|
280
|
+
const originalGet: any =
|
|
281
|
+
typeof (stmt as any).get === "function"
|
|
282
|
+
? (stmt as any).get.bind(stmt)
|
|
283
|
+
: undefined;
|
|
284
|
+
const originalAll: any = (stmt as any).all.bind(stmt);
|
|
285
|
+
const originalIterate: any =
|
|
286
|
+
typeof (stmt as any).iterate === "function"
|
|
287
|
+
? (stmt as any).iterate.bind(stmt)
|
|
288
|
+
: undefined;
|
|
289
|
+
|
|
290
|
+
// Toggle helper matching better-sqlite3's mode switching:
|
|
291
|
+
// enable(true) sets to target mode; enable(false) resets to flat ONLY if
|
|
292
|
+
// currently in that mode, otherwise leaves mode unchanged.
|
|
293
|
+
function setMode(target: StatementMode, use: boolean): void {
|
|
294
|
+
if (use) {
|
|
295
|
+
mode = target;
|
|
296
|
+
// Cache column map on first expand() call
|
|
297
|
+
if (target === "expand" && columnMap == null) {
|
|
298
|
+
columnMap = buildColumnTableMap(stmt);
|
|
299
|
+
if (columnMap == null) {
|
|
300
|
+
throw new TypeError(
|
|
301
|
+
"expand() requires the statement to have a columns() method",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else if (mode === target) {
|
|
306
|
+
mode = "flat";
|
|
307
|
+
}
|
|
308
|
+
// If use=false and mode !== target, no-op (matches better-sqlite3)
|
|
309
|
+
|
|
310
|
+
// Sync native array mode: both raw and expand need arrays from the native layer.
|
|
311
|
+
// Expand needs arrays to correctly handle duplicate column names across tables.
|
|
312
|
+
if (typeof (stmt as any).setReturnArrays === "function") {
|
|
313
|
+
(stmt as any).setReturnArrays(mode === "raw" || mode === "expand");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
Object.defineProperty(stmt, "pluck", {
|
|
318
|
+
value: function pluck(toggle?: boolean): S {
|
|
319
|
+
setMode("pluck", validateToggle(toggle));
|
|
320
|
+
return stmt;
|
|
321
|
+
},
|
|
322
|
+
writable: true,
|
|
323
|
+
configurable: true,
|
|
324
|
+
enumerable: false,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
Object.defineProperty(stmt, "raw", {
|
|
328
|
+
value: function raw(toggle?: boolean): S {
|
|
329
|
+
setMode("raw", validateToggle(toggle));
|
|
330
|
+
return stmt;
|
|
331
|
+
},
|
|
332
|
+
writable: true,
|
|
333
|
+
configurable: true,
|
|
334
|
+
enumerable: false,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
Object.defineProperty(stmt, "expand", {
|
|
338
|
+
value: function expand(toggle?: boolean): S {
|
|
339
|
+
setMode("expand", validateToggle(toggle));
|
|
340
|
+
return stmt;
|
|
341
|
+
},
|
|
342
|
+
writable: true,
|
|
343
|
+
configurable: true,
|
|
344
|
+
enumerable: false,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (originalGet != null) {
|
|
348
|
+
Object.defineProperty(stmt, "get", {
|
|
349
|
+
value: (...params: any[]) => {
|
|
350
|
+
const row = originalGet(...params);
|
|
351
|
+
return transformRow(row, mode, columnMap);
|
|
352
|
+
},
|
|
353
|
+
writable: true,
|
|
354
|
+
configurable: true,
|
|
355
|
+
enumerable: false,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Object.defineProperty(stmt, "all", {
|
|
360
|
+
value: (...params: any[]) => {
|
|
361
|
+
const rows = originalAll(...params);
|
|
362
|
+
if (mode === "flat" || mode === "raw") return rows;
|
|
363
|
+
return rows.map((row: unknown) => transformRow(row, mode, columnMap));
|
|
364
|
+
},
|
|
365
|
+
writable: true,
|
|
366
|
+
configurable: true,
|
|
367
|
+
enumerable: false,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (originalIterate != null) {
|
|
371
|
+
Object.defineProperty(stmt, "iterate", {
|
|
372
|
+
value: function* (...params: any[]) {
|
|
373
|
+
const iter = originalIterate(...params);
|
|
374
|
+
for (const row of iter) {
|
|
375
|
+
yield transformRow(row, mode, columnMap);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
writable: true,
|
|
379
|
+
configurable: true,
|
|
380
|
+
enumerable: false,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return stmt as S & EnhancedStatementMethods;
|
|
385
|
+
}
|
|
70
386
|
|
|
71
387
|
/**
|
|
72
388
|
* Implementation of pragma() that works on any EnhanceableDatabaseSync.
|
|
@@ -95,23 +411,12 @@ function pragmaImpl(
|
|
|
95
411
|
}
|
|
96
412
|
|
|
97
413
|
const stmt = this.prepare(`PRAGMA ${source}`);
|
|
98
|
-
const rows = stmt.all() as Record<string, unknown>[];
|
|
99
414
|
|
|
100
415
|
if (simple) {
|
|
101
|
-
|
|
102
|
-
const firstRow = rows[0];
|
|
103
|
-
if (firstRow == null) {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
const keys = Object.keys(firstRow);
|
|
107
|
-
const firstKey = keys[0];
|
|
108
|
-
if (firstKey == null) {
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
return firstRow[firstKey];
|
|
416
|
+
return extractFirstColumn(stmt.all()[0]);
|
|
112
417
|
}
|
|
113
418
|
|
|
114
|
-
return
|
|
419
|
+
return stmt.all();
|
|
115
420
|
}
|
|
116
421
|
|
|
117
422
|
/**
|
|
@@ -139,71 +444,93 @@ function hasEnhancedMethods(
|
|
|
139
444
|
}
|
|
140
445
|
|
|
141
446
|
/**
|
|
142
|
-
* Ensures that `.pragma()
|
|
143
|
-
* given database.
|
|
447
|
+
* Ensures that `.pragma()`, `.transaction()`, and statement modes
|
|
448
|
+
* (`.pluck()`, `.raw()`, `.expand()`) are available on the given database.
|
|
144
449
|
*
|
|
145
450
|
* This function can enhance:
|
|
146
451
|
* - `node:sqlite` DatabaseSync instances (adds the methods)
|
|
147
|
-
* - `@photostructure/sqlite` DatabaseSync instances (
|
|
148
|
-
* methods)
|
|
452
|
+
* - `@photostructure/sqlite` DatabaseSync instances (adds the methods)
|
|
149
453
|
* - Any object with compatible `exec()`, `prepare()`, and `isTransaction`
|
|
150
454
|
*
|
|
151
455
|
* The enhancement is done by adding methods directly to the instance, not the
|
|
152
456
|
* prototype, so it won't affect other instances or the original class.
|
|
153
457
|
*
|
|
154
458
|
* @param db The database instance to enhance
|
|
155
|
-
* @returns The same instance with `.pragma()
|
|
156
|
-
* guaranteed
|
|
459
|
+
* @returns The same instance with `.pragma()`, `.transaction()`, and
|
|
460
|
+
* `.pluck()` / `.raw()` / `.expand()` (on prepared statements) guaranteed
|
|
157
461
|
*
|
|
158
462
|
* @example
|
|
159
463
|
* ```typescript
|
|
160
|
-
*
|
|
161
|
-
* import { DatabaseSync } from 'node:sqlite';
|
|
162
|
-
* import { enhance } from '@photostructure/sqlite';
|
|
464
|
+
* import { DatabaseSync, enhance } from '@photostructure/sqlite';
|
|
163
465
|
*
|
|
164
466
|
* const db = enhance(new DatabaseSync(':memory:'));
|
|
165
467
|
*
|
|
166
|
-
* //
|
|
468
|
+
* // better-sqlite3-style pragma
|
|
167
469
|
* db.pragma('journal_mode = wal');
|
|
470
|
+
*
|
|
471
|
+
* // better-sqlite3-style transactions
|
|
168
472
|
* const insertMany = db.transaction((items) => {
|
|
169
473
|
* for (const item of items) insert.run(item);
|
|
170
474
|
* });
|
|
171
|
-
* ```
|
|
172
475
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* import { DatabaseSync, enhance } from '@photostructure/sqlite';
|
|
177
|
-
*
|
|
178
|
-
* const db = enhance(new DatabaseSync(':memory:'));
|
|
179
|
-
* // db already had these methods, enhance() just returns it unchanged
|
|
476
|
+
* // better-sqlite3-style pluck
|
|
477
|
+
* const count = db.prepare("SELECT COUNT(*) FROM users").pluck().get();
|
|
478
|
+
* const names = db.prepare("SELECT name FROM users").pluck().all();
|
|
180
479
|
* ```
|
|
181
480
|
*/
|
|
182
481
|
export function enhance<T extends EnhanceableDatabaseSync>(
|
|
183
482
|
db: T,
|
|
184
483
|
): EnhancedDatabaseSync<T> {
|
|
185
|
-
//
|
|
186
|
-
if (hasEnhancedMethods(db)) {
|
|
187
|
-
|
|
484
|
+
// Add pragma and transaction if not already present
|
|
485
|
+
if (!hasEnhancedMethods(db)) {
|
|
486
|
+
// Using Object.defineProperty to make them non-enumerable like native methods
|
|
487
|
+
Object.defineProperty(db, "pragma", {
|
|
488
|
+
value: pragmaImpl,
|
|
489
|
+
writable: true,
|
|
490
|
+
configurable: true,
|
|
491
|
+
enumerable: false,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
Object.defineProperty(db, "transaction", {
|
|
495
|
+
value: transactionImpl,
|
|
496
|
+
writable: true,
|
|
497
|
+
configurable: true,
|
|
498
|
+
enumerable: false,
|
|
499
|
+
});
|
|
188
500
|
}
|
|
189
501
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
writable: true,
|
|
195
|
-
configurable: true,
|
|
196
|
-
enumerable: false,
|
|
197
|
-
});
|
|
502
|
+
// Wrap prepare() to add pluck() to returned statements
|
|
503
|
+
// eslint-disable-next-line security/detect-object-injection -- ENHANCED_PREPARE is a Symbol
|
|
504
|
+
if (!(db as any)[ENHANCED_PREPARE]) {
|
|
505
|
+
const originalPrepare: any = db.prepare.bind(db);
|
|
198
506
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
507
|
+
Object.defineProperty(db, "prepare", {
|
|
508
|
+
value: (...args: any[]) => {
|
|
509
|
+
const stmt = originalPrepare(...args);
|
|
510
|
+
enhanceStatement(stmt);
|
|
511
|
+
// Add stmt.database back-reference for better-sqlite3 compat
|
|
512
|
+
Object.defineProperty(stmt, "database", {
|
|
513
|
+
value: db,
|
|
514
|
+
writable: false,
|
|
515
|
+
configurable: true,
|
|
516
|
+
enumerable: false,
|
|
517
|
+
});
|
|
518
|
+
return stmt;
|
|
519
|
+
},
|
|
520
|
+
writable: true,
|
|
521
|
+
configurable: true,
|
|
522
|
+
enumerable: false,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
Object.defineProperty(db, ENHANCED_PREPARE, {
|
|
526
|
+
value: true,
|
|
527
|
+
writable: false,
|
|
528
|
+
configurable: false,
|
|
529
|
+
enumerable: false,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
205
532
|
|
|
206
|
-
return db as EnhancedDatabaseSync<T>;
|
|
533
|
+
return db as unknown as EnhancedDatabaseSync<T>;
|
|
207
534
|
}
|
|
208
535
|
|
|
209
536
|
/**
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,10 @@ import nodeGypBuild from "node-gyp-build";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { _dirname } from "./dirname";
|
|
5
5
|
import { SQLTagStore } from "./sql-tag-store";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DatabaseSyncInstance,
|
|
8
|
+
DatabaseSyncLimits,
|
|
9
|
+
} from "./types/database-sync-instance";
|
|
7
10
|
import { DatabaseSyncOptions } from "./types/database-sync-options";
|
|
8
11
|
import { SQLTagStoreInstance } from "./types/sql-tag-store-instance";
|
|
9
12
|
import { SqliteAuthorizationActions } from "./types/sqlite-authorization-actions";
|
|
@@ -15,7 +18,10 @@ import { StatementSyncInstance } from "./types/statement-sync-instance";
|
|
|
15
18
|
|
|
16
19
|
export type { AggregateOptions } from "./types/aggregate-options";
|
|
17
20
|
export type { ChangesetApplyOptions } from "./types/changeset-apply-options";
|
|
18
|
-
export type {
|
|
21
|
+
export type {
|
|
22
|
+
DatabaseSyncInstance,
|
|
23
|
+
DatabaseSyncLimits,
|
|
24
|
+
} from "./types/database-sync-instance";
|
|
19
25
|
export type { DatabaseSyncOptions } from "./types/database-sync-options";
|
|
20
26
|
export type { PragmaOptions } from "./types/pragma-options";
|
|
21
27
|
export type { SessionOptions } from "./types/session-options";
|
|
@@ -39,6 +45,7 @@ export {
|
|
|
39
45
|
type EnhanceableDatabaseSync,
|
|
40
46
|
type EnhancedDatabaseSync,
|
|
41
47
|
type EnhancedMethods,
|
|
48
|
+
type EnhancedStatementMethods,
|
|
42
49
|
} from "./enhance";
|
|
43
50
|
|
|
44
51
|
// Use _dirname() helper that works in both CJS/ESM and Jest
|
|
@@ -203,6 +210,82 @@ DatabaseSync.prototype = _DatabaseSync.prototype;
|
|
|
203
210
|
return new SQLTagStore(this, capacity);
|
|
204
211
|
};
|
|
205
212
|
|
|
213
|
+
// Limit name to SQLite limit ID mapping (matches upstream kLimitMapping order)
|
|
214
|
+
const LIMIT_MAPPING: ReadonlyArray<{
|
|
215
|
+
name: keyof DatabaseSyncLimits;
|
|
216
|
+
id: number;
|
|
217
|
+
}> = [
|
|
218
|
+
{ name: "length", id: 0 },
|
|
219
|
+
{ name: "sqlLength", id: 1 },
|
|
220
|
+
{ name: "column", id: 2 },
|
|
221
|
+
{ name: "exprDepth", id: 3 },
|
|
222
|
+
{ name: "compoundSelect", id: 4 },
|
|
223
|
+
{ name: "vdbeOp", id: 5 },
|
|
224
|
+
{ name: "functionArg", id: 6 },
|
|
225
|
+
{ name: "attach", id: 7 },
|
|
226
|
+
{ name: "likePatternLength", id: 8 },
|
|
227
|
+
{ name: "variableNumber", id: 9 },
|
|
228
|
+
{ name: "triggerDepth", id: 10 },
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const INT_MAX = 2147483647;
|
|
232
|
+
|
|
233
|
+
// WeakMap to cache limits objects per database instance
|
|
234
|
+
const limitsCache = new WeakMap<DatabaseSyncInstance, DatabaseSyncLimits>();
|
|
235
|
+
|
|
236
|
+
function validateLimitValue(value: unknown): number {
|
|
237
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
238
|
+
throw new TypeError(
|
|
239
|
+
"Limit value must be a non-negative integer or Infinity.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (value === Infinity) {
|
|
243
|
+
return INT_MAX;
|
|
244
|
+
}
|
|
245
|
+
if (!Number.isFinite(value) || value !== Math.trunc(value)) {
|
|
246
|
+
throw new TypeError(
|
|
247
|
+
"Limit value must be a non-negative integer or Infinity.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (value < 0) {
|
|
251
|
+
throw new RangeError("Limit value must be non-negative.");
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function createLimitsObject(db: DatabaseSyncInstance): DatabaseSyncLimits {
|
|
257
|
+
const obj = Object.create(null) as DatabaseSyncLimits;
|
|
258
|
+
for (const { name, id } of LIMIT_MAPPING) {
|
|
259
|
+
Object.defineProperty(obj, name, {
|
|
260
|
+
get() {
|
|
261
|
+
return db.getLimit(id);
|
|
262
|
+
},
|
|
263
|
+
set(value: unknown) {
|
|
264
|
+
const validated = validateLimitValue(value);
|
|
265
|
+
db.setLimit(id, validated);
|
|
266
|
+
},
|
|
267
|
+
enumerable: true,
|
|
268
|
+
configurable: false,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return obj;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!Object.getOwnPropertyDescriptor(DatabaseSync.prototype, "limits")) {
|
|
275
|
+
Object.defineProperty(DatabaseSync.prototype, "limits", {
|
|
276
|
+
get(this: DatabaseSyncInstance) {
|
|
277
|
+
let obj = limitsCache.get(this);
|
|
278
|
+
if (obj == null) {
|
|
279
|
+
obj = createLimitsObject(this);
|
|
280
|
+
limitsCache.set(this, obj);
|
|
281
|
+
}
|
|
282
|
+
return obj;
|
|
283
|
+
},
|
|
284
|
+
enumerable: true,
|
|
285
|
+
configurable: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
206
289
|
// NOTE: .pragma() and .transaction() are NOT added to the prototype by default.
|
|
207
290
|
// This keeps DatabaseSync 100% API-compatible with node:sqlite.
|
|
208
291
|
// Users who want better-sqlite3-style methods should use enhance():
|