@photostructure/sqlite 0.3.0 → 0.4.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 +52 -16
  2. package/README.md +5 -4
  3. package/binding.gyp +2 -2
  4. package/dist/index.cjs +158 -11
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +284 -89
  7. package/dist/index.d.mts +284 -89
  8. package/dist/index.d.ts +284 -89
  9. package/dist/index.mjs +155 -10
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +71 -62
  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 +228 -0
  25. package/src/index.ts +83 -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,228 @@
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()` and
7
+ * `.transaction()` methods to database instances that don't have them (e.g.,
8
+ * 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 a transaction is currently active */
25
+ readonly isTransaction: boolean;
26
+ }
27
+
28
+ /**
29
+ * Interface for an enhanced database with pragma() and transaction() methods.
30
+ */
31
+ export interface EnhancedMethods {
32
+ /**
33
+ * Executes a PRAGMA statement and returns its result.
34
+ *
35
+ * @param source The PRAGMA command (without "PRAGMA" prefix)
36
+ * @param options Optional configuration
37
+ * @returns Array of rows, or single value if `simple: true`
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * db.pragma('cache_size', { simple: true }); // -16000
42
+ * db.pragma('journal_mode = wal');
43
+ * ```
44
+ */
45
+ pragma(source: string, options?: PragmaOptions): unknown;
46
+
47
+ /**
48
+ * Creates a function that always runs inside a transaction.
49
+ *
50
+ * @param fn The function to wrap in a transaction
51
+ * @returns A transaction function with `.deferred`, `.immediate`,
52
+ * `.exclusive` variants
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const insertMany = db.transaction((items) => {
57
+ * for (const item of items) insert.run(item);
58
+ * });
59
+ * insertMany(['a', 'b', 'c']); // All in one transaction
60
+ * ```
61
+ */
62
+ transaction<F extends (...args: any[]) => any>(fn: F): TransactionFunction<F>;
63
+ }
64
+
65
+ /**
66
+ * A database instance that has been enhanced with pragma() and transaction() methods.
67
+ */
68
+ export type EnhancedDatabaseSync<T extends EnhanceableDatabaseSync> = T &
69
+ EnhancedMethods;
70
+
71
+ /**
72
+ * Implementation of pragma() that works on any EnhanceableDatabaseSync.
73
+ */
74
+ function pragmaImpl(
75
+ this: EnhanceableDatabaseSync,
76
+ source: string,
77
+ options?: PragmaOptions,
78
+ ): unknown {
79
+ if (typeof source !== "string") {
80
+ throw new TypeError("Expected first argument to be a string");
81
+ }
82
+ if (options != null && typeof options !== "object") {
83
+ throw new TypeError("Expected second argument to be an options object");
84
+ }
85
+
86
+ const simple = options?.simple === true;
87
+
88
+ // Validate that simple is a boolean if provided
89
+ if (
90
+ options != null &&
91
+ "simple" in options &&
92
+ typeof options.simple !== "boolean"
93
+ ) {
94
+ throw new TypeError('Expected the "simple" option to be a boolean');
95
+ }
96
+
97
+ const stmt = this.prepare(`PRAGMA ${source}`);
98
+ const rows = stmt.all() as Record<string, unknown>[];
99
+
100
+ if (simple) {
101
+ // Return the first column of the first row, or undefined if no rows
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];
112
+ }
113
+
114
+ return rows;
115
+ }
116
+
117
+ /**
118
+ * Implementation of transaction() that works on any EnhanceableDatabaseSync.
119
+ */
120
+ function transactionImpl<F extends (...args: any[]) => any>(
121
+ this: EnhanceableDatabaseSync,
122
+ fn: F,
123
+ ): TransactionFunction<F> {
124
+ // createTransaction expects DatabaseSyncInstance but only uses the subset
125
+ // defined in EnhanceableDatabaseSync, so this cast is safe
126
+ return createTransaction(this as any, fn);
127
+ }
128
+
129
+ /**
130
+ * Checks if a database instance already has the enhanced methods.
131
+ */
132
+ function hasEnhancedMethods(
133
+ db: EnhanceableDatabaseSync,
134
+ ): db is EnhanceableDatabaseSync & EnhancedMethods {
135
+ return (
136
+ typeof (db as any).pragma === "function" &&
137
+ typeof (db as any).transaction === "function"
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Ensures that `.pragma()` and `.transaction()` methods are available on the
143
+ * given database.
144
+ *
145
+ * This function can enhance:
146
+ * - `node:sqlite` DatabaseSync instances (adds the methods)
147
+ * - `@photostructure/sqlite` DatabaseSync instances (no-op, already has these
148
+ * methods)
149
+ * - Any object with compatible `exec()`, `prepare()`, and `isTransaction`
150
+ *
151
+ * The enhancement is done by adding methods directly to the instance, not the
152
+ * prototype, so it won't affect other instances or the original class.
153
+ *
154
+ * @param db The database instance to enhance
155
+ * @returns The same instance with `.pragma()` and `.transaction()` methods
156
+ * guaranteed
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * // With node:sqlite
161
+ * import { DatabaseSync } from 'node:sqlite';
162
+ * import { enhance } from '@photostructure/sqlite';
163
+ *
164
+ * const db = enhance(new DatabaseSync(':memory:'));
165
+ *
166
+ * // Now you can use better-sqlite3-style methods
167
+ * db.pragma('journal_mode = wal');
168
+ * const insertMany = db.transaction((items) => {
169
+ * for (const item of items) insert.run(item);
170
+ * });
171
+ * ```
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * // With @photostructure/sqlite (no-op, already enhanced)
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
180
+ * ```
181
+ */
182
+ export function enhance<T extends EnhanceableDatabaseSync>(
183
+ db: T,
184
+ ): EnhancedDatabaseSync<T> {
185
+ // If already enhanced, return as-is
186
+ if (hasEnhancedMethods(db)) {
187
+ return db;
188
+ }
189
+
190
+ // Add methods directly to the instance
191
+ // Using Object.defineProperty to make them non-enumerable like native methods
192
+ Object.defineProperty(db, "pragma", {
193
+ value: pragmaImpl,
194
+ writable: true,
195
+ configurable: true,
196
+ enumerable: false,
197
+ });
198
+
199
+ Object.defineProperty(db, "transaction", {
200
+ value: transactionImpl,
201
+ writable: true,
202
+ configurable: true,
203
+ enumerable: false,
204
+ });
205
+
206
+ return db as EnhancedDatabaseSync<T>;
207
+ }
208
+
209
+ /**
210
+ * Type guard to check if a database has enhanced methods.
211
+ *
212
+ * @param db The database to check
213
+ * @returns True if the database has `.pragma()` and `.transaction()` methods
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * import { isEnhanced } from '@photostructure/sqlite';
218
+ *
219
+ * if (isEnhanced(db)) {
220
+ * db.pragma('cache_size', { simple: true });
221
+ * }
222
+ * ```
223
+ */
224
+ export function isEnhanced(
225
+ db: EnhanceableDatabaseSync,
226
+ ): db is EnhanceableDatabaseSync & EnhancedMethods {
227
+ return hasEnhancedMethods(db);
228
+ }
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,22 @@ 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
+ } from "./enhance";
43
+
30
44
  // Use _dirname() helper that works in both CJS/ESM and Jest
31
45
  const binding = nodeGypBuild(join(_dirname(), ".."));
32
46
 
@@ -52,25 +66,54 @@ export type SqliteConstants = SqliteOpenFlags &
52
66
 
53
67
  /**
54
68
  * Options for creating a prepared statement.
69
+ *
70
+ * **Note:** The per-statement override options (`readBigInts`, `returnArrays`,
71
+ * `allowBareNamedParameters`, `allowUnknownNamedParameters`) are a **Node.js v25+**
72
+ * feature. On Node.js v24 and earlier, `node:sqlite` silently ignores these options.
73
+ * This library implements them for forward compatibility with Node.js v25+.
55
74
  */
56
75
  export interface StatementOptions {
57
76
  /** If true, the prepared statement's expandedSQL property will contain the expanded SQL. @default false */
58
77
  readonly expandedSQL?: boolean;
59
78
  /** If true, anonymous parameters are enabled for the statement. @default false */
60
79
  readonly anonymousParameters?: boolean;
80
+ /**
81
+ * If true, read integer values as JavaScript BigInt. Overrides database-level setting.
82
+ * **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
83
+ * @default database default
84
+ */
85
+ readonly readBigInts?: boolean;
86
+ /**
87
+ * If true, return results as arrays rather than objects. Overrides database-level setting.
88
+ * **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
89
+ * @default database default
90
+ */
91
+ readonly returnArrays?: boolean;
92
+ /**
93
+ * If true, allows bare named parameters (without prefix). Overrides database-level setting.
94
+ * **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
95
+ * @default database default
96
+ */
97
+ readonly allowBareNamedParameters?: boolean;
98
+ /**
99
+ * If true, unknown named parameters are ignored. Overrides database-level setting.
100
+ * **Node.js v25+ feature** - silently ignored by `node:sqlite` on v24 and earlier.
101
+ * @default database default
102
+ */
103
+ readonly allowUnknownNamedParameters?: boolean;
61
104
  }
62
105
 
63
106
  export interface Session {
64
107
  /**
65
108
  * Generate a changeset containing all changes recorded by the session.
66
- * @returns A Buffer containing the changeset data.
109
+ * @returns A Uint8Array containing the changeset data.
67
110
  */
68
- changeset(): Buffer;
111
+ changeset(): Uint8Array;
69
112
  /**
70
113
  * Generate a patchset containing all changes recorded by the session.
71
- * @returns A Buffer containing the patchset data.
114
+ * @returns A Uint8Array containing the patchset data.
72
115
  */
73
- patchset(): Buffer;
116
+ patchset(): Uint8Array;
74
117
  /**
75
118
  * Close the session and release its resources.
76
119
  */
@@ -133,8 +176,20 @@ export interface SqliteModule {
133
176
  * const readOnlyDb = new DatabaseSync('./data.db', { readOnly: true });
134
177
  * ```
135
178
  */
136
- export const DatabaseSync =
137
- binding.DatabaseSync as SqliteModule["DatabaseSync"];
179
+ // Store the native binding's DatabaseSync
180
+ const _DatabaseSync = binding.DatabaseSync;
181
+ // Wrapper around the native constructor to enforce usage of `new` with the correct error code.
182
+ // We use a function wrapper instead of a Proxy for better performance and explicit prototype handling.
183
+ export const DatabaseSync = function DatabaseSync(this: any, ...args: any[]) {
184
+ if (!new.target) {
185
+ const err = new TypeError("Cannot call constructor without `new`");
186
+ (err as NodeJS.ErrnoException).code = "ERR_CONSTRUCT_CALL_REQUIRED";
187
+ throw err;
188
+ }
189
+ return Reflect.construct(_DatabaseSync, args, new.target);
190
+ } as unknown as SqliteModule["DatabaseSync"];
191
+ Object.setPrototypeOf(DatabaseSync, _DatabaseSync);
192
+ DatabaseSync.prototype = _DatabaseSync.prototype;
138
193
 
139
194
  // node:sqlite implements createTagStore and SQLTagStore entirely in native C++.
140
195
  // We use a TypeScript implementation instead, attached via prototype extension.
@@ -148,6 +203,15 @@ export const DatabaseSync =
148
203
  return new SQLTagStore(this, capacity);
149
204
  };
150
205
 
206
+ // NOTE: .pragma() and .transaction() are NOT added to the prototype by default.
207
+ // This keeps DatabaseSync 100% API-compatible with node:sqlite.
208
+ // Users who want better-sqlite3-style methods should use enhance():
209
+ //
210
+ // import { DatabaseSync, enhance } from '@photostructure/sqlite';
211
+ // const db = enhance(new DatabaseSync(':memory:'));
212
+ // db.pragma('journal_mode', { simple: true });
213
+ // db.transaction(() => { ... });
214
+
151
215
  /**
152
216
  * The StatementSync class represents a prepared SQL statement.
153
217
  * This class should not be instantiated directly; use DatabaseSync.prepare() instead.
@@ -159,8 +223,18 @@ export const DatabaseSync =
159
223
  * stmt.finalize();
160
224
  * ```
161
225
  */
162
- export const StatementSync =
163
- binding.StatementSync as SqliteModule["StatementSync"];
226
+ // Store the native binding's StatementSync for internal use
227
+ const _StatementSync = binding.StatementSync;
228
+ // Export a wrapper that throws ERR_ILLEGAL_CONSTRUCTOR when called directly
229
+ // but preserves instanceof checks and prototype chain
230
+ export const StatementSync = function StatementSync() {
231
+ const err = new TypeError("Illegal constructor");
232
+ (err as NodeJS.ErrnoException).code = "ERR_ILLEGAL_CONSTRUCTOR";
233
+ throw err;
234
+ } as unknown as SqliteModule["StatementSync"];
235
+ // Use the native prototype directly so instanceof checks work correctly
236
+ // (stmt instanceof StatementSync will check if StatementSync.prototype is in stmt's chain)
237
+ StatementSync.prototype = _StatementSync.prototype;
164
238
 
165
239
  /**
166
240
  * The Session class for recording database changes.
@@ -11,25 +11,33 @@ namespace node {
11
11
  inline void THROW_ERR_INVALID_STATE(Napi::Env env,
12
12
  const char *message = nullptr) {
13
13
  const char *msg = message ? message : "Invalid state";
14
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
14
+ Napi::Error error = Napi::Error::New(env, msg);
15
+ error.Set("code", Napi::String::New(env, "ERR_INVALID_STATE"));
16
+ error.ThrowAsJavaScriptException();
15
17
  }
16
18
 
17
19
  inline void THROW_ERR_INVALID_ARG_TYPE(Napi::Env env,
18
20
  const char *message = nullptr) {
19
21
  const char *msg = message ? message : "Invalid argument type";
20
- Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
22
+ Napi::TypeError error = Napi::TypeError::New(env, msg);
23
+ error.Set("code", Napi::String::New(env, "ERR_INVALID_ARG_TYPE"));
24
+ error.ThrowAsJavaScriptException();
21
25
  }
22
26
 
23
27
  inline void THROW_ERR_OUT_OF_RANGE(Napi::Env env,
24
28
  const char *message = nullptr) {
25
29
  const char *msg = message ? message : "Value out of range";
26
- Napi::RangeError::New(env, msg).ThrowAsJavaScriptException();
30
+ Napi::RangeError error = Napi::RangeError::New(env, msg);
31
+ error.Set("code", Napi::String::New(env, "ERR_OUT_OF_RANGE"));
32
+ error.ThrowAsJavaScriptException();
27
33
  }
28
34
 
29
35
  inline void THROW_ERR_INVALID_ARG_VALUE(Napi::Env env,
30
36
  const char *message = nullptr) {
31
37
  const char *msg = message ? message : "Invalid argument value";
32
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
38
+ Napi::Error error = Napi::Error::New(env, msg);
39
+ error.Set("code", Napi::String::New(env, "ERR_INVALID_ARG_VALUE"));
40
+ error.ThrowAsJavaScriptException();
33
41
  }
34
42
 
35
43
  inline void THROW_ERR_SQLITE_ERROR(Napi::Env env,
@@ -37,7 +45,9 @@ inline void THROW_ERR_SQLITE_ERROR(Napi::Env env,
37
45
  // Check for both null and empty string - on Windows (MSVC),
38
46
  // std::exception::what() can sometimes return an empty string
39
47
  const char *msg = (message && message[0] != '\0') ? message : "SQLite error";
40
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
48
+ Napi::Error error = Napi::Error::New(env, msg);
49
+ error.Set("code", Napi::String::New(env, "ERR_SQLITE_ERROR"));
50
+ error.ThrowAsJavaScriptException();
41
51
  }
42
52
 
43
53
  // Database-aware version available when sqlite_impl.h is included
@@ -45,24 +55,33 @@ inline void THROW_ERR_SQLITE_ERROR(Napi::Env env,
45
55
  // issues
46
56
 
47
57
  inline void THROW_ERR_CONSTRUCT_CALL_REQUIRED(Napi::Env env) {
48
- Napi::TypeError::New(env, "Class constructor cannot be invoked without 'new'")
49
- .ThrowAsJavaScriptException();
58
+ Napi::TypeError error = Napi::TypeError::New(
59
+ env, "Class constructor cannot be invoked without 'new'");
60
+ error.Set("code", Napi::String::New(env, "ERR_CONSTRUCT_CALL_REQUIRED"));
61
+ error.ThrowAsJavaScriptException();
62
+ }
63
+
64
+ inline void THROW_ERR_ILLEGAL_CONSTRUCTOR(Napi::Env env) {
65
+ Napi::TypeError error = Napi::TypeError::New(env, "Illegal constructor");
66
+ error.Set("code", Napi::String::New(env, "ERR_ILLEGAL_CONSTRUCTOR"));
67
+ error.ThrowAsJavaScriptException();
50
68
  }
51
69
 
52
70
  inline void THROW_ERR_INVALID_URL_SCHEME(Napi::Env env,
53
- const char *scheme = nullptr) {
54
- std::string msg = "Invalid URL scheme";
55
- if (scheme) {
56
- msg += ": ";
57
- msg += scheme;
58
- }
59
- Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
71
+ const char * /*scheme*/ = nullptr) {
72
+ // Message must match Node.js exactly
73
+ Napi::TypeError error =
74
+ Napi::TypeError::New(env, "The URL must be of scheme file:");
75
+ error.Set("code", Napi::String::New(env, "ERR_INVALID_URL_SCHEME"));
76
+ error.ThrowAsJavaScriptException();
60
77
  }
61
78
 
62
79
  inline void THROW_ERR_LOAD_SQLITE_EXTENSION(Napi::Env env,
63
80
  const char *message = nullptr) {
64
81
  const char *msg = message ? message : "Failed to load SQLite extension";
65
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
82
+ Napi::Error error = Napi::Error::New(env, msg);
83
+ error.Set("code", Napi::String::New(env, "ERR_LOAD_SQLITE_EXTENSION"));
84
+ error.ThrowAsJavaScriptException();
66
85
  }
67
86
 
68
87
  // Macro wrappers for compatibility (removed to avoid conflicts)
@@ -81,6 +81,14 @@ inline const char *GetSqliteErrorCodeName(int code) {
81
81
  }
82
82
 
83
83
  // Enhanced SQLite error that includes system errno information
84
+ // Error format matches Node.js node:sqlite for API compatibility:
85
+ // - code: 'ERR_SQLITE_ERROR' (constant, matches Node.js)
86
+ // - errcode: number (SQLite error code, matches Node.js)
87
+ // - errstr: string (SQLite error string, matches Node.js)
88
+ // We also add extra properties for enhanced debugging:
89
+ // - sqliteCode: number (same as errcode, for backward compat)
90
+ // - sqliteExtendedCode: number (extended SQLite error code)
91
+ // - sqliteErrorString: string (same as errstr, for backward compat)
84
92
  inline void ThrowEnhancedSqliteError(Napi::Env env, sqlite3 *db,
85
93
  int sqlite_code,
86
94
  const std::string &message) {
@@ -89,7 +97,16 @@ inline void ThrowEnhancedSqliteError(Napi::Env env, sqlite3 *db,
89
97
  // truncated or corrupted error messages
90
98
  Napi::Error error = Napi::Error::New(env, message.c_str());
91
99
 
92
- // Add SQLite error code information
100
+ // Node.js compatible properties
101
+ error.Set("code", Napi::String::New(env, "ERR_SQLITE_ERROR"));
102
+ error.Set("errcode", Napi::Number::New(env, sqlite_code));
103
+
104
+ const char *err_str = sqlite3_errstr(sqlite_code);
105
+ if (err_str) {
106
+ error.Set("errstr", Napi::String::New(env, err_str));
107
+ }
108
+
109
+ // Our enhanced properties (for backward compatibility and debugging)
93
110
  error.Set("sqliteCode", Napi::Number::New(env, sqlite_code));
94
111
 
95
112
  if (db) {
@@ -104,12 +121,11 @@ inline void ThrowEnhancedSqliteError(Napi::Env env, sqlite3 *db,
104
121
  }
105
122
  }
106
123
 
107
- // Set a standard error code property for compatibility
124
+ // Keep original code name as sqliteCodeName for debugging
108
125
  const char *code_name = GetSqliteErrorCodeName(sqlite_code);
109
- error.Set("code", Napi::String::New(env, code_name));
126
+ error.Set("sqliteCodeName", Napi::String::New(env, code_name));
110
127
 
111
128
  // Also set the human-readable error string
112
- const char *err_str = sqlite3_errstr(sqlite_code);
113
129
  if (err_str) {
114
130
  error.Set("sqliteErrorString", Napi::String::New(env, err_str));
115
131
  }
@@ -126,7 +142,9 @@ inline void ThrowEnhancedSqliteError(Napi::Env env, sqlite3 *db,
126
142
  inline void ThrowSqliteError(Napi::Env env, sqlite3 *db,
127
143
  const std::string &message) {
128
144
  if (db) {
129
- int errcode = sqlite3_errcode(db);
145
+ // Use extended error code (e.g., 1555 for SQLITE_CONSTRAINT_PRIMARYKEY)
146
+ // instead of basic code (e.g., 19 for SQLITE_CONSTRAINT) to match Node.js
147
+ int errcode = sqlite3_extended_errcode(db);
130
148
  ThrowEnhancedSqliteError(env, db, errcode, message);
131
149
  } else {
132
150
  // Fallback to simple error when no db handle available
@@ -136,12 +154,20 @@ inline void ThrowSqliteError(Napi::Env env, sqlite3 *db,
136
154
  }
137
155
 
138
156
  // Helper to throw from a SqliteException with captured error info
157
+ // Uses same format as ThrowEnhancedSqliteError for consistency
139
158
  inline void
140
159
  ThrowFromSqliteException(Napi::Env env,
141
160
  const photostructure::sqlite::SqliteException &ex) {
142
161
  Napi::Error error = Napi::Error::New(env, ex.what());
143
162
 
144
- // Add all captured error information
163
+ // Node.js compatible properties
164
+ error.Set("code", Napi::String::New(env, "ERR_SQLITE_ERROR"));
165
+ error.Set("errcode", Napi::Number::New(env, ex.sqlite_code()));
166
+ if (!ex.error_string().empty()) {
167
+ error.Set("errstr", Napi::String::New(env, ex.error_string().c_str()));
168
+ }
169
+
170
+ // Our enhanced properties (for backward compatibility and debugging)
145
171
  error.Set("sqliteCode", Napi::Number::New(env, ex.sqlite_code()));
146
172
  error.Set("sqliteExtendedCode", Napi::Number::New(env, ex.extended_code()));
147
173
 
@@ -149,9 +175,9 @@ ThrowFromSqliteException(Napi::Env env,
149
175
  error.Set("systemErrno", Napi::Number::New(env, ex.system_errno()));
150
176
  }
151
177
 
152
- // Set the error code name
178
+ // Keep original code name as sqliteCodeName for debugging
153
179
  const char *code_name = GetSqliteErrorCodeName(ex.sqlite_code());
154
- error.Set("code", Napi::String::New(env, code_name));
180
+ error.Set("sqliteCodeName", Napi::String::New(env, code_name));
155
181
 
156
182
  // Also set the human-readable error string
157
183
  // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
@@ -25,7 +25,9 @@ export class SQLTagStore {
25
25
 
26
26
  constructor(db: DatabaseSyncInstance, capacity: number = DEFAULT_CAPACITY) {
27
27
  if (!db.isOpen) {
28
- throw new Error("Database is not open");
28
+ const err = new Error("database is not open");
29
+ (err as NodeJS.ErrnoException).code = "ERR_INVALID_STATE";
30
+ throw err;
29
31
  }
30
32
  this.database = db;
31
33
  this.maxCapacity = capacity;
@@ -101,23 +103,18 @@ export class SQLTagStore {
101
103
 
102
104
  /**
103
105
  * Get a cached statement or prepare a new one.
104
- * If a cached statement has been finalized, it's evicted and a new one is prepared.
105
106
  */
106
107
  private getOrPrepare(strings: TemplateStringsArray): StatementSyncInstance {
107
108
  if (!this.database.isOpen) {
108
- throw new Error("Database is not open");
109
+ throw new Error("database is not open");
109
110
  }
110
111
 
111
112
  const sql = this.buildSQL(strings);
112
113
 
113
- // Check cache - evict if finalized
114
+ // Check cache
114
115
  const cached = this.cache.get(sql);
115
116
  if (cached) {
116
- if (!cached.finalized) {
117
- return cached;
118
- }
119
- // Statement was finalized externally - remove from cache
120
- this.cache.delete(sql);
117
+ return cached;
121
118
  }
122
119
 
123
120
  // Prepare new statement and cache it