@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.
- package/CHANGELOG.md +52 -16
- package/README.md +5 -4
- package/binding.gyp +2 -2
- package/dist/index.cjs +158 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +284 -89
- package/dist/index.d.mts +284 -89
- package/dist/index.d.ts +284 -89
- package/dist/index.mjs +155 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +71 -62
- 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 +228 -0
- package/src/index.ts +83 -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,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 {
|
|
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
|
|
109
|
+
* @returns A Uint8Array containing the changeset data.
|
|
67
110
|
*/
|
|
68
|
-
changeset():
|
|
111
|
+
changeset(): Uint8Array;
|
|
69
112
|
/**
|
|
70
113
|
* Generate a patchset containing all changes recorded by the session.
|
|
71
|
-
* @returns A
|
|
114
|
+
* @returns A Uint8Array containing the patchset data.
|
|
72
115
|
*/
|
|
73
|
-
patchset():
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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.
|
package/src/shims/node_errors.h
CHANGED
|
@@ -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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
124
|
+
// Keep original code name as sqliteCodeName for debugging
|
|
108
125
|
const char *code_name = GetSqliteErrorCodeName(sqlite_code);
|
|
109
|
-
error.Set("
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
178
|
+
// Keep original code name as sqliteCodeName for debugging
|
|
153
179
|
const char *code_name = GetSqliteErrorCodeName(ex.sqlite_code());
|
|
154
|
-
error.Set("
|
|
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
|
package/src/sql-tag-store.ts
CHANGED
|
@@ -25,7 +25,9 @@ export class SQLTagStore {
|
|
|
25
25
|
|
|
26
26
|
constructor(db: DatabaseSyncInstance, capacity: number = DEFAULT_CAPACITY) {
|
|
27
27
|
if (!db.isOpen) {
|
|
28
|
-
|
|
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("
|
|
109
|
+
throw new Error("database is not open");
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
const sql = this.buildSQL(strings);
|
|
112
113
|
|
|
113
|
-
// Check cache
|
|
114
|
+
// Check cache
|
|
114
115
|
const cached = this.cache.get(sql);
|
|
115
116
|
if (cached) {
|
|
116
|
-
|
|
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
|