@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +65 -16
  2. package/README.md +5 -10
  3. package/binding.gyp +2 -2
  4. package/dist/index.cjs +314 -11
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +346 -89
  7. package/dist/index.d.mts +346 -89
  8. package/dist/index.d.ts +346 -89
  9. package/dist/index.mjs +311 -10
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +72 -63
  12. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  13. package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
  14. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  16. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  18. package/prebuilds/test_extension.so +0 -0
  19. package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
  20. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  21. package/src/aggregate_function.cpp +222 -114
  22. package/src/aggregate_function.h +5 -6
  23. package/src/binding.cpp +30 -21
  24. package/src/enhance.ts +552 -0
  25. package/src/index.ts +84 -9
  26. package/src/shims/node_errors.h +34 -15
  27. package/src/shims/sqlite_errors.h +34 -8
  28. package/src/sql-tag-store.ts +6 -9
  29. package/src/sqlite_impl.cpp +1044 -394
  30. package/src/sqlite_impl.h +46 -7
  31. package/src/transaction.ts +178 -0
  32. package/src/types/database-sync-instance.ts +6 -40
  33. package/src/types/pragma-options.ts +23 -0
  34. package/src/types/statement-sync-instance.ts +38 -12
  35. package/src/types/transaction.ts +72 -0
  36. package/src/upstream/node_sqlite.cc +143 -43
  37. package/src/upstream/node_sqlite.h +15 -11
  38. package/src/upstream/sqlite3.c +102 -58
  39. package/src/upstream/sqlite3.h +5 -5
  40. package/src/user_function.cpp +138 -141
  41. 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 { StatementSyncInstance } from "./types/statement-sync-instance";
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 Buffer containing the changeset data.
110
+ * @returns A Uint8Array containing the changeset data.
67
111
  */
68
- changeset(): Buffer;
112
+ changeset(): Uint8Array;
69
113
  /**
70
114
  * Generate a patchset containing all changes recorded by the session.
71
- * @returns A Buffer containing the patchset data.
115
+ * @returns A Uint8Array containing the patchset data.
72
116
  */
73
- patchset(): Buffer;
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
- export const DatabaseSync =
137
- binding.DatabaseSync as SqliteModule["DatabaseSync"];
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
- export const StatementSync =
163
- binding.StatementSync as SqliteModule["StatementSync"];
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.