@homeofthings/sqlite3 6.2.0 → 6.3.1

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/README.md CHANGED
@@ -36,9 +36,9 @@ yarn add @homeofthings/sqlite3
36
36
 
37
37
  ### Prebuilt binaries
38
38
 
39
- `sqlite3` v5+ was rewritten to use [Node-API](https://nodejs.org/api/n-api.html) so prebuilt binaries do not need to be built for specific Node versions. `sqlite3` currently builds for both Node-API v3 and v6. Check the [Node-API version matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix) to ensure your Node version supports one of these. The prebuilt binaries should be supported on Node v20.17.0+.
39
+ `@homeofthings/sqlite3` uses [Node-API](https://nodejs.org/api/n-api.html) so prebuilt binaries do not need to be built for specific Node versions. Prebuilt binaries are available for Node-API v3 and v6. Check the [Node-API version matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix) to ensure your Node version supports one of these. Requires Node.js v20.17.0 or later.
40
40
 
41
- The module uses [`prebuild-install`](https://github.com/prebuild/prebuild-install) to download the prebuilt binary for your platform, if it exists. These binaries are hosted on GitHub Releases for `sqlite3` versions above 5.0.2, and they are hosted on S3 otherwise. The following targets are currently provided:
41
+ The module uses [`prebuild-install`](https://github.com/prebuild/prebuild-install) to download the prebuilt binary for your platform, if it exists. These binaries are hosted on GitHub Releases. The following targets are currently provided:
42
42
 
43
43
  * `darwin-arm64`
44
44
  * `darwin-x64`
@@ -64,7 +64,46 @@ SQLite's [SQLCipher extension](https://github.com/sqlcipher/sqlcipher) is also
64
64
 
65
65
  # API
66
66
 
67
- See the [API documentation](https://github.com/gms1/node-sqlite3/wiki/API) in the wiki.
67
+ See the [API documentation](docs/API.md) for detailed documentation of both the callback-based and Promise-based APIs.
68
+
69
+ ## Quick Example
70
+
71
+ ### Callback-based API (Traditional)
72
+
73
+ ```js
74
+ const sqlite3 = require('@homeofthings/sqlite3').verbose();
75
+ const db = new sqlite3.Database(':memory:');
76
+
77
+ db.serialize(() => {
78
+ db.run("CREATE TABLE lorem (info TEXT)");
79
+ db.run("INSERT INTO lorem VALUES (?)", ['test']);
80
+ db.each("SELECT * FROM lorem", (err, row) => {
81
+ console.log(row);
82
+ });
83
+ });
84
+
85
+ db.close();
86
+ ```
87
+
88
+ ### Promise-based API (Modern)
89
+
90
+ ```js
91
+ const { SqliteDatabase } = require('@homeofthings/sqlite3');
92
+
93
+ async function main() {
94
+ const db = await SqliteDatabase.open(':memory:');
95
+
96
+ await db.run("CREATE TABLE lorem (info TEXT)");
97
+ await db.run("INSERT INTO lorem VALUES (?)", ['test']);
98
+
99
+ const rows = await db.all("SELECT * FROM lorem");
100
+ console.log(rows);
101
+
102
+ await db.close();
103
+ }
104
+
105
+ main().catch(console.error);
106
+ ```
68
107
 
69
108
  # Usage
70
109
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Promise-based wrapper for the Backup class from node-sqlite3
3
+ * @module promise/backup
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * A thin wrapper for the 'Backup' class from 'node-sqlite3' using Promises
10
+ * instead of callbacks
11
+ *
12
+ * @see https://github.com/mapbox/node-sqlite3/wiki/API
13
+ */
14
+ class SqliteBackup {
15
+ /**
16
+ * Creates an instance of SqliteBackup.
17
+ *
18
+ * @param {import('../sqlite3.js').Backup} backup - The underlying Backup instance
19
+ */
20
+ constructor(backup) {
21
+ /**
22
+ * @type {import('../sqlite3.js').Backup}
23
+ * @private
24
+ */
25
+ this._backup = backup;
26
+ }
27
+
28
+ /**
29
+ * Returns true if the backup is idle (not actively copying)
30
+ *
31
+ * @returns {boolean}
32
+ */
33
+ get idle() {
34
+ return this._backup.idle;
35
+ }
36
+
37
+ /**
38
+ * Returns true if the backup is completed
39
+ *
40
+ * @returns {boolean}
41
+ */
42
+ get completed() {
43
+ return this._backup.completed;
44
+ }
45
+
46
+ /**
47
+ * Returns true if the backup has failed
48
+ *
49
+ * @returns {boolean}
50
+ */
51
+ get failed() {
52
+ return this._backup.failed;
53
+ }
54
+
55
+ /**
56
+ * Returns the remaining number of pages left to copy
57
+ * Returns -1 if `step` not yet called
58
+ *
59
+ * @returns {number}
60
+ */
61
+ get remaining() {
62
+ return this._backup.remaining;
63
+ }
64
+
65
+ /**
66
+ * Returns the total number of pages
67
+ * Returns -1 if `step` not yet called
68
+ *
69
+ * @returns {number}
70
+ */
71
+ get pageCount() {
72
+ return this._backup.pageCount;
73
+ }
74
+
75
+ /**
76
+ * Returns the progress (percentage completion)
77
+ *
78
+ * @returns {number} Progress as percentage (0-100)
79
+ */
80
+ get progress() {
81
+ const pageCount = this.pageCount;
82
+ const remaining = this.remaining;
83
+ if (pageCount === -1 || remaining === -1) {
84
+ return 0;
85
+ }
86
+ return pageCount === 0 ? 100 : ((pageCount - remaining) / pageCount) * 100;
87
+ }
88
+
89
+ /**
90
+ * Copy the next page or all remaining pages of the backup
91
+ *
92
+ * @param {number} [pages=-1] - Number of pages to copy (-1 for all remaining)
93
+ * @returns {Promise<void>}
94
+ */
95
+ step(pages) {
96
+ return new Promise((resolve, reject) => {
97
+ if (!this._backup) {
98
+ reject(new Error('backup handle not open'));
99
+ return;
100
+ }
101
+ this._backup.step(pages === undefined ? -1 : pages, (err) => {
102
+ if (err) {
103
+ reject(err);
104
+ } else {
105
+ resolve();
106
+ }
107
+ });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Finish the backup (synchronous)
113
+ */
114
+ finish() {
115
+ if (this._backup) {
116
+ this._backup.finish();
117
+ }
118
+ }
119
+ }
120
+
121
+ module.exports = { SqliteBackup };
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Promise-based wrapper for the Database class from node-sqlite3
3
+ * @module promise/database
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Database } = require('../sqlite3.js');
9
+ const { SqliteStatement } = require('./statement');
10
+ const { SqliteBackup } = require('./backup');
11
+
12
+ /**
13
+ * Result object returned by run() operations
14
+ * @typedef {Object} SqlRunResult
15
+ * @property {number} lastID - The ID of the last inserted row
16
+ * @property {number} changes - The number of rows affected
17
+ */
18
+
19
+ /**
20
+ * @typedef {import('../sqlite3-binding.js').Database} Database
21
+ */
22
+
23
+ /**
24
+ * A thin wrapper for the 'Database' class from 'node-sqlite3' using Promises
25
+ * instead of callbacks
26
+ *
27
+ * @see https://github.com/mapbox/node-sqlite3/wiki/API
28
+ */
29
+ class SqliteDatabase {
30
+ /**
31
+ * Creates a new SqliteDatabase instance (without opening a database).
32
+ * Use open() to open a database connection, or use the static factory method open().
33
+ *
34
+ * @example
35
+ * // Using constructor + open
36
+ * const db = new SqliteDatabase();
37
+ * await db.open(':memory:');
38
+ *
39
+ * // Using static factory method
40
+ * const db = await SqliteDatabase.open(':memory:');
41
+ */
42
+ constructor() {
43
+ /**
44
+ * @type {Database | undefined}
45
+ * @protected
46
+ */
47
+ this.db = undefined;
48
+ }
49
+
50
+ /**
51
+ * Static factory method to create and open a database connection.
52
+ *
53
+ * @param {string} filename - The path to the database file or ':memory:' for in-memory
54
+ * @param {number} [mode] - Optional mode flags (OPEN_READONLY, OPEN_READWRITE, OPEN_CREATE)
55
+ * @returns {Promise<SqliteDatabase>} A promise that resolves to the opened database
56
+ * @example
57
+ * const db = await SqliteDatabase.open(':memory:');
58
+ * const db = await SqliteDatabase.open('mydb.sqlite', sqlite3.OPEN_READONLY);
59
+ */
60
+ static async open(filename, mode) {
61
+ const db = new SqliteDatabase();
62
+ await db.open(filename, mode);
63
+ return db;
64
+ }
65
+
66
+ /**
67
+ * Open a database connection
68
+ *
69
+ * @param {string} filename - The path to the database file or ':memory:' for in-memory
70
+ * @param {number} [mode] - Optional mode flags (OPEN_READONLY, OPEN_READWRITE, OPEN_CREATE)
71
+ * @returns {Promise<void>}
72
+ */
73
+ open(filename, mode) {
74
+ return new Promise((resolve, reject) => {
75
+ // Default mode: OPEN_READWRITE | OPEN_CREATE
76
+ const defaultMode = 0x00000002 | 0x00000004; // OPEN_READWRITE | OPEN_CREATE
77
+ const db = new Database(filename, mode === undefined ? defaultMode : mode, (err) => {
78
+ if (err) {
79
+ reject(err);
80
+ } else {
81
+ this.db = db;
82
+ resolve();
83
+ }
84
+ });
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Close the database connection
90
+ *
91
+ * @returns {Promise<void>}
92
+ */
93
+ close() {
94
+ return new Promise((resolve, reject) => {
95
+ if (!this.db) {
96
+ resolve();
97
+ return;
98
+ }
99
+ const db = this.db;
100
+ this.db = undefined;
101
+ db.close((err) => {
102
+ db.removeAllListeners();
103
+ if (err) {
104
+ reject(err);
105
+ } else {
106
+ resolve();
107
+ }
108
+ });
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Test if a connection is open
114
+ *
115
+ * @returns {boolean}
116
+ */
117
+ isOpen() {
118
+ return !!this.db;
119
+ }
120
+
121
+ /**
122
+ * Runs a SQL statement with the specified parameters
123
+ *
124
+ * @param {string} sql - The SQL statement
125
+ * @param {any} [params] - The parameters referenced in the statement
126
+ * @returns {Promise<SqlRunResult>} A promise that resolves to the result object
127
+ */
128
+ run(sql, params) {
129
+ return new Promise((resolve, reject) => {
130
+ if (!this.db) {
131
+ reject(new Error('database connection not open'));
132
+ return;
133
+ }
134
+ // Use function() to get 'this' context with lastID and changes
135
+ this.db.run(sql, params, function (err) {
136
+ if (err) {
137
+ reject(err);
138
+ } else {
139
+ resolve({
140
+ lastID: this.lastID,
141
+ changes: this.changes
142
+ });
143
+ }
144
+ });
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Runs a SQL query with the specified parameters, fetching only the first row
150
+ *
151
+ * @template T
152
+ * @param {string} sql - The DQL statement
153
+ * @param {any} [params] - The parameters referenced in the statement
154
+ * @returns {Promise<T | undefined>} A promise that resolves to the row or undefined
155
+ */
156
+ get(sql, params) {
157
+ return new Promise((resolve, reject) => {
158
+ if (!this.db) {
159
+ reject(new Error('database connection not open'));
160
+ return;
161
+ }
162
+ this.db.get(sql, params, (err, row) => {
163
+ if (err) {
164
+ reject(err);
165
+ } else {
166
+ resolve(row);
167
+ }
168
+ });
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Runs a SQL query with the specified parameters, fetching all rows
174
+ *
175
+ * @template T
176
+ * @param {string} sql - The DQL statement
177
+ * @param {any} [params] - The parameters referenced in the statement
178
+ * @returns {Promise<T[]>} A promise that resolves to an array of rows
179
+ */
180
+ all(sql, params) {
181
+ return new Promise((resolve, reject) => {
182
+ if (!this.db) {
183
+ reject(new Error('database connection not open'));
184
+ return;
185
+ }
186
+ this.db.all(sql, params, (err, rows) => {
187
+ if (err) {
188
+ reject(err);
189
+ } else {
190
+ resolve(rows);
191
+ }
192
+ });
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Runs a SQL query with the specified parameters, fetching all rows
198
+ * using a callback for each row
199
+ *
200
+ * @param {string} sql - The DQL statement
201
+ * @param {any} [params] - The parameters referenced in the statement
202
+ * @param {(err: Error | null, row: any) => void} [callback] - The callback function for each row
203
+ * @returns {Promise<number>} A promise that resolves to the number of rows retrieved
204
+ */
205
+ each(sql, params, callback) {
206
+ // Handle case where params is actually the callback (no params provided)
207
+ if (typeof params === 'function') {
208
+ callback = params;
209
+ params = undefined;
210
+ }
211
+ return new Promise((resolve, reject) => {
212
+ if (!this.db) {
213
+ reject(new Error('database connection not open'));
214
+ return;
215
+ }
216
+ this.db.each(sql, params, callback, (err, count) => {
217
+ if (err) {
218
+ reject(err);
219
+ } else {
220
+ resolve(count);
221
+ }
222
+ });
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Execute a SQL statement
228
+ *
229
+ * @param {string} sql - The SQL statement
230
+ * @returns {Promise<void>}
231
+ */
232
+ exec(sql) {
233
+ return new Promise((resolve, reject) => {
234
+ if (!this.db) {
235
+ reject(new Error('database connection not open'));
236
+ return;
237
+ }
238
+ this.db.exec(sql, (err) => {
239
+ if (err) {
240
+ reject(err);
241
+ } else {
242
+ resolve();
243
+ }
244
+ });
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Prepare a SQL statement
250
+ *
251
+ * @param {string} sql - The SQL statement
252
+ * @param {any} [params] - The parameters referenced in the statement
253
+ * @returns {Promise<SqliteStatement>} A promise that resolves to the prepared statement
254
+ */
255
+ prepare(sql, params) {
256
+ return new Promise((resolve, reject) => {
257
+ if (!this.db) {
258
+ reject(new Error('database connection not open'));
259
+ return;
260
+ }
261
+ const stmt = this.db.prepare(sql, params, (err) => {
262
+ if (err) {
263
+ reject(err);
264
+ } else {
265
+ resolve(new SqliteStatement(stmt));
266
+ }
267
+ });
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Initiate online backup
273
+ *
274
+ * @param {string} filename - The database file to backup from or to
275
+ * @param {boolean} [filenameIsDest=true] - Whether filename is destination
276
+ * @param {string} [destName='main'] - The destination database name
277
+ * @param {string} [sourceName='main'] - The source database name
278
+ * @returns {Promise<SqliteBackup>} A promise that resolves to the backup object
279
+ */
280
+ backup(filename, filenameIsDest = true, destName = 'main', sourceName = 'main') {
281
+ return new Promise((resolve, reject) => {
282
+ if (!this.db) {
283
+ reject(new Error('database connection not open'));
284
+ return;
285
+ }
286
+ const backup = this.db.backup(filename, destName, sourceName, filenameIsDest, (err) => {
287
+ if (err) {
288
+ reject(err);
289
+ } else {
290
+ resolve(new SqliteBackup(backup));
291
+ }
292
+ });
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Serialized sqlite3 calls
298
+ * If callback is provided, run callback in serialized mode
299
+ * Otherwise, switch connection to serialized mode
300
+ *
301
+ * @param {() => void} [callback] - Optional callback to run in serialized mode
302
+ */
303
+ serialize(callback) {
304
+ if (!this.db) {
305
+ throw new Error('database connection not open');
306
+ }
307
+ this.db.serialize(callback);
308
+ }
309
+
310
+ /**
311
+ * Parallelized sqlite3 calls
312
+ * If callback is provided, run callback in parallel mode
313
+ * Otherwise, switch connection to parallel mode
314
+ *
315
+ * @param {() => void} [callback] - Optional callback to run in parallel mode
316
+ */
317
+ parallelize(callback) {
318
+ if (!this.db) {
319
+ throw new Error('database connection not open');
320
+ }
321
+ this.db.parallelize(callback);
322
+ }
323
+
324
+ /**
325
+ * Run callback inside a database transaction
326
+ *
327
+ * @template T
328
+ * @param {() => Promise<T>} callback - The callback to run in transaction
329
+ * @returns {Promise<T>} A promise that resolves to the callback result
330
+ */
331
+ async transactionalize(callback) {
332
+ await this.beginTransaction();
333
+ try {
334
+ const result = await callback();
335
+ await this.commitTransaction();
336
+ return result;
337
+ } catch (err) {
338
+ await this.rollbackTransaction();
339
+ throw err;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Begin a transaction
345
+ *
346
+ * @returns {Promise<SqlRunResult>}
347
+ */
348
+ beginTransaction() {
349
+ return this.run('BEGIN IMMEDIATE TRANSACTION');
350
+ }
351
+
352
+ /**
353
+ * Commit a transaction
354
+ *
355
+ * @returns {Promise<SqlRunResult>}
356
+ */
357
+ commitTransaction() {
358
+ return this.run('COMMIT TRANSACTION');
359
+ }
360
+
361
+ /**
362
+ * Rollback a transaction
363
+ *
364
+ * @returns {Promise<SqlRunResult>}
365
+ */
366
+ rollbackTransaction() {
367
+ return this.run('ROLLBACK TRANSACTION');
368
+ }
369
+
370
+ /**
371
+ * End a transaction (commit or rollback)
372
+ *
373
+ * @param {boolean} commit - Whether to commit (true) or rollback (false)
374
+ * @returns {Promise<void>}
375
+ */
376
+ endTransaction(commit) {
377
+ const sql = commit ? 'COMMIT TRANSACTION' : 'ROLLBACK TRANSACTION';
378
+ return new Promise((resolve, reject) => {
379
+ if (!this.db) {
380
+ reject(new Error('database connection not open'));
381
+ return;
382
+ }
383
+ this.db.run(sql, (err) => {
384
+ // Ignore "no transaction" errors
385
+ if (err && !err.message.includes('no transaction')) {
386
+ reject(err);
387
+ } else {
388
+ resolve();
389
+ }
390
+ });
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Load an extension
396
+ *
397
+ * @param {string} filename - The path to the extension
398
+ * @returns {Promise<void>}
399
+ */
400
+ loadExtension(filename) {
401
+ return new Promise((resolve, reject) => {
402
+ if (!this.db) {
403
+ reject(new Error('database connection not open'));
404
+ return;
405
+ }
406
+ this.db.loadExtension(filename, (err) => {
407
+ if (err) {
408
+ reject(err);
409
+ } else {
410
+ resolve();
411
+ }
412
+ });
413
+ });
414
+ }
415
+
416
+ /**
417
+ * Wait for the database to be ready
418
+ *
419
+ * @returns {Promise<void>}
420
+ */
421
+ wait() {
422
+ return new Promise((resolve, reject) => {
423
+ if (!this.db) {
424
+ reject(new Error('database connection not open'));
425
+ return;
426
+ }
427
+ this.db.wait((err) => {
428
+ if (err) {
429
+ reject(err);
430
+ } else {
431
+ resolve();
432
+ }
433
+ });
434
+ });
435
+ }
436
+
437
+ /**
438
+ * Interrupt a running query
439
+ */
440
+ interrupt() {
441
+ if (!this.db) {
442
+ throw new Error('database connection not open');
443
+ }
444
+ this.db.interrupt();
445
+ }
446
+
447
+ /**
448
+ * Configure database options
449
+ *
450
+ * @param {string} option - The option name
451
+ * @param {number} [value] - The option value
452
+ */
453
+ configure(option, value) {
454
+ if (!this.db) {
455
+ throw new Error('database connection not open');
456
+ }
457
+ this.db.configure(option, value);
458
+ }
459
+
460
+ /**
461
+ * Register an event listener
462
+ *
463
+ * @param {'trace' | 'profile' | 'error' | 'close' | 'open' | 'change'} event - The event name
464
+ * @param {Function} listener - The listener function
465
+ * @returns {this}
466
+ */
467
+ on(event, listener) {
468
+ if (!this.db) {
469
+ throw new Error('database connection not open');
470
+ }
471
+ this.db.on(event, listener);
472
+ return this;
473
+ }
474
+
475
+ /**
476
+ * Remove an event listener
477
+ *
478
+ * @param {string} event - The event name
479
+ * @param {Function} listener - The listener function
480
+ * @returns {this}
481
+ */
482
+ off(event, listener) {
483
+ if (!this.db) {
484
+ throw new Error('database connection not open');
485
+ }
486
+ this.db.off(event, listener);
487
+ return this;
488
+ }
489
+
490
+ /**
491
+ * Remove all event listeners for an event
492
+ *
493
+ * @param {string} [event] - The event name
494
+ * @returns {this}
495
+ */
496
+ removeAllListeners(event) {
497
+ if (!this.db) {
498
+ throw new Error('database connection not open');
499
+ }
500
+ this.db.removeAllListeners(event);
501
+ return this;
502
+ }
503
+ }
504
+
505
+ module.exports = { SqliteDatabase };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Promise-based wrappers for node-sqlite3
3
+ * @module promise
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { SqliteDatabase } = require('./database');
9
+ const { SqliteStatement } = require('./statement');
10
+ const { SqliteBackup } = require('./backup');
11
+
12
+ module.exports = {
13
+ SqliteDatabase,
14
+ SqliteStatement,
15
+ SqliteBackup
16
+ };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Promise-based wrapper for the Statement class from node-sqlite3
3
+ * @module promise/statement
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Result object returned by run() operations
10
+ * @typedef {Object} SqlRunResult
11
+ * @property {number} lastID - The ID of the last inserted row
12
+ * @property {number} changes - The number of rows affected
13
+ */
14
+
15
+ /**
16
+ * A thin wrapper for the 'Statement' class from 'node-sqlite3' using Promises
17
+ * instead of callbacks
18
+ *
19
+ * @see https://github.com/mapbox/node-sqlite3/wiki/API
20
+ */
21
+ class SqliteStatement {
22
+ /**
23
+ * Creates an instance of SqliteStatement.
24
+ *
25
+ * @param {import('../sqlite3.js').Statement} stmt - The underlying Statement instance
26
+ */
27
+ constructor(stmt) {
28
+ /**
29
+ * @type {import('../sqlite3.js').Statement}
30
+ * @private
31
+ */
32
+ this._stmt = stmt;
33
+ }
34
+
35
+ /**
36
+ * Bind the given parameters to the prepared statement
37
+ *
38
+ * @param {...any} params - The parameters to bind
39
+ * @returns {this} Returns this for chaining
40
+ */
41
+ bind(...params) {
42
+ this._stmt.bind(params);
43
+ return this;
44
+ }
45
+
46
+ /**
47
+ * Reset a open cursor of the prepared statement preserving the parameter binding
48
+ * Allows re-execute of the same query
49
+ *
50
+ * @returns {Promise<void>}
51
+ */
52
+ reset() {
53
+ return new Promise((resolve) => {
54
+ this._stmt.reset(() => {
55
+ resolve();
56
+ });
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Finalizes a prepared statement (freeing any resource used by this statement)
62
+ *
63
+ * @returns {Promise<void>}
64
+ */
65
+ finalize() {
66
+ return new Promise((resolve, reject) => {
67
+ this._stmt.finalize((err) => {
68
+ if (err) {
69
+ reject(err);
70
+ } else {
71
+ resolve();
72
+ }
73
+ });
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Runs a prepared statement with the specified parameters
79
+ *
80
+ * @param {any} [params] - The parameters referenced in the statement
81
+ * @returns {Promise<SqlRunResult>} A promise that resolves to the result object
82
+ */
83
+ run(params) {
84
+ return new Promise((resolve, reject) => {
85
+ // Use function() to get 'this' context with lastID and changes
86
+ const callback = function(err) {
87
+ if (err) {
88
+ reject(err);
89
+ } else {
90
+ resolve({
91
+ lastID: this.lastID,
92
+ changes: this.changes
93
+ });
94
+ }
95
+ };
96
+ // Only pass params if provided - otherwise use bound parameters
97
+ if (params === undefined) {
98
+ this._stmt.run(callback);
99
+ } else {
100
+ this._stmt.run(params, callback);
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Runs a prepared statement with the specified parameters, fetching only the first row
107
+ *
108
+ * @template T
109
+ * @param {any} [params] - The parameters referenced in the statement
110
+ * @returns {Promise<T | undefined>} A promise that resolves to the row or undefined
111
+ */
112
+ get(params) {
113
+ return new Promise((resolve, reject) => {
114
+ const callback = (err, row) => {
115
+ if (err) {
116
+ reject(err);
117
+ } else {
118
+ resolve(row);
119
+ }
120
+ };
121
+ // Only pass params if provided - otherwise use bound parameters
122
+ if (params === undefined) {
123
+ this._stmt.get(callback);
124
+ } else {
125
+ this._stmt.get(params, callback);
126
+ }
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Runs a prepared statement with the specified parameters, fetching all rows
132
+ *
133
+ * @template T
134
+ * @param {any} [params] - The parameters referenced in the statement
135
+ * @returns {Promise<T[]>} A promise that resolves to an array of rows
136
+ */
137
+ all(params) {
138
+ return new Promise((resolve, reject) => {
139
+ const callback = (err, rows) => {
140
+ if (err) {
141
+ reject(err);
142
+ } else {
143
+ resolve(rows);
144
+ }
145
+ };
146
+ // Only pass params if provided - otherwise use bound parameters
147
+ if (params === undefined) {
148
+ this._stmt.all(callback);
149
+ } else {
150
+ this._stmt.all(params, callback);
151
+ }
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Runs a prepared statement with the specified parameters, fetching all rows
157
+ * using a callback for each row
158
+ *
159
+ * @param {any} [params] - The parameters referenced in the statement
160
+ * @param {(err: Error | null, row: any) => void} [callback] - The callback function for each row
161
+ * @returns {Promise<number>} A promise that resolves to the number of rows retrieved
162
+ */
163
+ each(params, callback) {
164
+ // Handle case where params is actually the callback (no params provided)
165
+ if (typeof params === 'function') {
166
+ callback = params;
167
+ params = undefined;
168
+ }
169
+ return new Promise((resolve, reject) => {
170
+ this._stmt.each(params, callback, (err, count) => {
171
+ if (err) {
172
+ reject(err);
173
+ } else {
174
+ resolve(count);
175
+ }
176
+ });
177
+ });
178
+ }
179
+ }
180
+
181
+ module.exports = { SqliteStatement };
package/lib/sqlite3.d.ts CHANGED
@@ -26,23 +26,23 @@ export const BUSY: number;
26
26
  export const LOCKED: number;
27
27
  export const NOMEM: number;
28
28
  export const READONLY: number;
29
- export const INTERRUPT: number
29
+ export const INTERRUPT: number;
30
30
  export const IOERR: number;
31
- export const CORRUPT: number
31
+ export const CORRUPT: number;
32
32
  export const NOTFOUND: number;
33
33
  export const FULL: number;
34
34
  export const CANTOPEN: number;
35
35
  export const PROTOCOL: number;
36
36
  export const EMPTY: number;
37
37
  export const SCHEMA: number;
38
- export const TOOBIG: number
39
- export const CONSTRAINT: number
38
+ export const TOOBIG: number;
39
+ export const CONSTRAINT: number;
40
40
  export const MISMATCH: number;
41
41
  export const MISUSE: number;
42
42
  export const NOLFS: number;
43
- export const AUTH: number
43
+ export const AUTH: number;
44
44
  export const FORMAT: number;
45
- export const RANGE: number
45
+ export const RANGE: number;
46
46
  export const NOTADB: number;
47
47
 
48
48
  export const LIMIT_LENGTH: number;
@@ -64,8 +64,8 @@ export const cached: {
64
64
  };
65
65
 
66
66
  export interface RunResult extends Statement {
67
- lastID: number;
68
- changes: number;
67
+ lastID: number;
68
+ changes: number;
69
69
  }
70
70
 
71
71
  export class Statement extends events.EventEmitter {
@@ -143,6 +143,17 @@ export class Database extends events.EventEmitter {
143
143
 
144
144
  export function verbose(): sqlite3;
145
145
 
146
+ export interface Backup {
147
+ idle: boolean;
148
+ completed: boolean;
149
+ failed: boolean;
150
+ remaining: number;
151
+ pageCount: number;
152
+ step(pages: number, callback: (err: Error | null) => void): void;
153
+ finish(): void;
154
+ }
155
+
156
+
146
157
  export interface sqlite3 {
147
158
  OPEN_READONLY: number;
148
159
  OPEN_READWRITE: number;
@@ -165,23 +176,23 @@ export interface sqlite3 {
165
176
  LOCKED: number;
166
177
  NOMEM: number;
167
178
  READONLY: number;
168
- INTERRUPT: number
179
+ INTERRUPT: number;
169
180
  IOERR: number;
170
- CORRUPT: number
181
+ CORRUPT: number;
171
182
  NOTFOUND: number;
172
183
  FULL: number;
173
184
  CANTOPEN: number;
174
185
  PROTOCOL: number;
175
186
  EMPTY: number;
176
187
  SCHEMA: number;
177
- TOOBIG: number
178
- CONSTRAINT: number
188
+ TOOBIG: number;
189
+ CONSTRAINT: number;
179
190
  MISMATCH: number;
180
191
  MISUSE: number;
181
192
  NOLFS: number;
182
- AUTH: number
193
+ AUTH: number;
183
194
  FORMAT: number;
184
- RANGE: number
195
+ RANGE: number;
185
196
  NOTADB: number;
186
197
 
187
198
  LIMIT_LENGTH: number;
@@ -201,5 +212,143 @@ export interface sqlite3 {
201
212
  RunResult: RunResult;
202
213
  Statement: typeof Statement;
203
214
  Database: typeof Database;
215
+ Backup: Backup;
216
+ SqlRunResult: SqlRunResult;
217
+ SqliteDatabase: typeof SqliteDatabase;
218
+ SqliteStatement: typeof SqliteStatement;
219
+ SqliteBackup: typeof SqliteBackup;
204
220
  verbose(): this;
205
221
  }
222
+
223
+ // Promise-based wrapper classes
224
+
225
+ /**
226
+ * Result object returned by run() operations
227
+ */
228
+ export interface SqlRunResult {
229
+ lastID: number;
230
+ changes: number;
231
+ }
232
+
233
+ /**
234
+ * Promise-based wrapper for the Database class
235
+ */
236
+ export class SqliteDatabase {
237
+ protected db?: Database;
238
+
239
+ constructor();
240
+
241
+ static open(filename: string, mode?: number): Promise<SqliteDatabase>;
242
+
243
+ open(filename: string, mode?: number): Promise<void>;
244
+ close(): Promise<void>;
245
+
246
+ run(sql: string, params?: any): Promise<SqlRunResult>;
247
+ get<T = any>(sql: string, params?: any): Promise<T | undefined>;
248
+ all<T = any>(sql: string, params?: any): Promise<T[]>;
249
+ each<T = any>(
250
+ sql: string,
251
+ params?: any,
252
+ callback?: (err: Error | null, row: T) => void,
253
+ ): Promise<number>;
254
+
255
+ exec(sql: string): Promise<void>;
256
+ prepare(sql: string, params?: any): Promise<SqliteStatement>;
257
+
258
+ backup(
259
+ filename: string,
260
+ filenameIsDest?: boolean,
261
+ destName?: string,
262
+ sourceName?: string,
263
+ ): Promise<SqliteBackup>;
264
+
265
+ serialize(callback?: () => void): void;
266
+ parallelize(callback?: () => void): void;
267
+
268
+ transactionalize<T>(callback: () => Promise<T>): Promise<T>;
269
+ beginTransaction(): Promise<void>;
270
+ commitTransaction(): Promise<void>;
271
+ rollbackTransaction(): Promise<void>;
272
+ endTransaction(commit: boolean): Promise<void>;
273
+
274
+ loadExtension(filename: string): Promise<void>;
275
+
276
+ wait(): Promise<void>;
277
+ interrupt(): void;
278
+
279
+ configure(option: string, ...args: any[]): void;
280
+
281
+ on(event: string, listener: (...args: any[]) => void): this;
282
+ off(event: string, listener: (...args: any[]) => void): this;
283
+ removeAllListeners(event?: string): this;
284
+ }
285
+
286
+ /**
287
+ * Promise-based wrapper for the Statement class
288
+ */
289
+ export class SqliteStatement {
290
+ constructor(stmt: Statement);
291
+
292
+ bind(...params: any[]): this;
293
+ reset(): Promise<void>;
294
+
295
+ run(params?: any): Promise<SqlRunResult>;
296
+ get<T = any>(params?: any): Promise<T | undefined>;
297
+ all<T = any>(params?: any): Promise<T[]>;
298
+ each<T = any>(
299
+ params?: any,
300
+ callback?: (err: Error | null, row: T) => void,
301
+ ): Promise<number>;
302
+
303
+ finalize(): Promise<void>;
304
+ }
305
+
306
+ /**
307
+ * Promise-based wrapper for the Backup class
308
+ */
309
+ export class SqliteBackup {
310
+ constructor(backup: Backup);
311
+
312
+ /**
313
+ * Returns true if the backup is idle (not actively copying)
314
+ */
315
+ readonly idle: boolean;
316
+
317
+ /**
318
+ * Returns true if the backup is completed
319
+ */
320
+ readonly completed: boolean;
321
+
322
+ /**
323
+ * Returns true if the backup has failed
324
+ */
325
+ readonly failed: boolean;
326
+
327
+ /**
328
+ * Returns the remaining number of pages left to copy
329
+ * Returns -1 if `step` not yet called
330
+ */
331
+ readonly remaining: number;
332
+
333
+ /**
334
+ * Returns the total number of pages
335
+ * Returns -1 if `step` not yet called
336
+ */
337
+ readonly pageCount: number;
338
+
339
+ /**
340
+ * Returns the progress (percentage completion)
341
+ */
342
+ readonly progress: number;
343
+
344
+ /**
345
+ * Copy the next page or all remaining pages of the backup
346
+ * @param pages - Number of pages to copy (-1 for all remaining)
347
+ */
348
+ step(pages?: number): Promise<void>;
349
+
350
+ /**
351
+ * Finish the backup (synchronous)
352
+ */
353
+ finish(): void;
354
+ }
package/lib/sqlite3.js CHANGED
@@ -205,3 +205,9 @@ sqlite3.verbose = function() {
205
205
 
206
206
  return sqlite3;
207
207
  };
208
+
209
+ // Export Promise-based wrapper classes
210
+ const { SqliteDatabase, SqliteStatement, SqliteBackup } = require('./promise');
211
+ sqlite3.SqliteDatabase = SqliteDatabase;
212
+ sqlite3.SqliteStatement = SqliteStatement;
213
+ sqlite3.SqliteBackup = SqliteBackup;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@homeofthings/sqlite3",
3
3
  "description": "Asynchronous, non-blocking SQLite3 bindings",
4
- "version": "6.2.0",
4
+ "version": "6.3.1",
5
5
  "homepage": "https://github.com/gms1/node-sqlite3",
6
6
  "author": {
7
7
  "name": "Mapbox",
@@ -35,8 +35,8 @@
35
35
  "files": [
36
36
  "binding.gyp",
37
37
  "deps/",
38
- "lib/*.js",
39
- "lib/*.d.ts",
38
+ "lib/**/*.js",
39
+ "lib/**/*.d.ts",
40
40
  "src/"
41
41
  ],
42
42
  "repository": {
@@ -45,15 +45,18 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "bindings": "^1.5.0",
48
- "node-addon-api": "^8.0.0",
48
+ "node-addon-api": "^8.7.0",
49
49
  "prebuild-install": "^7.1.3",
50
- "tar": "^7.5.10"
50
+ "tar": "^7.5.13"
51
51
  },
52
52
  "devDependencies": {
53
- "eslint": "8.56.0",
54
- "mocha": "10.2.0",
53
+ "@eslint/js": "^10.0.1",
54
+ "eslint": "^10.2.0",
55
+ "globals": "^17.4.0",
56
+ "mocha": "11.7.5",
57
+ "nyc": "^18.0.0",
55
58
  "prebuild": "13.0.1",
56
- "tinybench": "^2.9.0"
59
+ "tinybench": "^6.0.0"
57
60
  },
58
61
  "peerDependencies": {
59
62
  "node-gyp": "12.x"
@@ -75,8 +78,8 @@
75
78
  "rebuild": "node-gyp rebuild",
76
79
  "upload": "prebuild --verbose --prerelease",
77
80
  "frozen-install": "yarn install --frozen-lockfile",
78
- "lint": "eslint .eslintrc.js lib/ test/ tools/",
79
- "test": "node test/support/createdb.js && mocha -R spec --timeout 480000"
81
+ "lint": "eslint lib/ test/ tools/",
82
+ "test": "node test/support/createdb.js && nyc mocha -R spec --timeout 480000"
80
83
  },
81
84
  "license": "BSD-3-Clause",
82
85
  "publishConfig": {
package/src/database.cc CHANGED
@@ -14,7 +14,7 @@ Napi::FunctionReference Database::constructor;
14
14
  Napi::Object Database::Init(Napi::Env env, Napi::Object exports) {
15
15
  Napi::HandleScope scope(env);
16
16
  // declare napi_default_method here as it is only available in Node v14.12.0+
17
- auto napi_default_method = static_cast<napi_property_attributes>(napi_writable | napi_configurable);
17
+ auto napi_default_method = static_cast<napi_property_attributes>(napi_writable | napi_configurable);
18
18
 
19
19
  auto t = DefineClass(env, "Database", {
20
20
  InstanceMethod("close", &Database::Close, napi_default_method),
@@ -81,8 +81,21 @@ void Database::Process() {
81
81
  queue.pop();
82
82
  std::unique_ptr<Call> call(c);
83
83
  locked = call->exclusive;
84
+
85
+ // Track pending before callback to detect synchronous operations
86
+ unsigned int before_pending = pending;
84
87
  call->callback(call->baton);
85
88
 
89
+ // If operation was synchronous (pending unchanged) and we're in exclusive mode,
90
+ // reset locked and continue processing the queue.
91
+ // Synchronous operations don't increment pending, so if pending is unchanged
92
+ // and locked is true, the operation completed synchronously.
93
+ if (locked && pending == before_pending) {
94
+ locked = false;
95
+ // Continue processing - don't break
96
+ continue;
97
+ }
98
+
86
99
  if (locked) break;
87
100
  }
88
101
  }
@@ -340,7 +353,7 @@ Napi::Value Database::Configure(const Napi::CallbackInfo& info) {
340
353
  REQUIRE_ARGUMENTS(2);
341
354
 
342
355
  Napi::Function handle;
343
- if (info[0].StrictEquals( Napi::String::New(env, "trace"))) {
356
+ if (info[0].StrictEquals( Napi::String::New(env, "trace"))) {
344
357
  auto* baton = new Baton(db, handle);
345
358
  db->Schedule(RegisterTraceCallback, baton);
346
359
  }
package/src/macros.h CHANGED
@@ -4,9 +4,14 @@
4
4
  const char* sqlite_code_string(int code);
5
5
  const char* sqlite_authorizer_string(int type);
6
6
  #include <vector>
7
+ #include <atomic>
7
8
 
8
9
  // TODO: better way to work around StringConcat?
9
10
  #include <napi.h>
11
+
12
+ // Forward declaration of shutdown flag from node_sqlite3.cc
13
+ extern std::atomic<bool> g_env_shutting_down;
14
+
10
15
  inline Napi::String StringConcat(Napi::Value str1, Napi::Value str2) {
11
16
  return Napi::String::New(str1.Env(), str1.As<Napi::String>().Utf8Value() +
12
17
  str2.As<Napi::String>().Utf8Value() );
@@ -128,8 +133,21 @@ inline bool OtherIsInt(Napi::Number source) {
128
133
  if ((argc != 0) && (passed_argv != NULL)) {\
129
134
  args.assign(passed_argv, passed_argv + argc);\
130
135
  }\
131
- Napi::Value res = (callback).Call(Napi::Value(context), args); \
132
- if (res.IsEmpty()) return __VA_ARGS__;
136
+ try {\
137
+ Napi::Value res = (callback).Call(Napi::Value(context), args);\
138
+ if (res.IsEmpty()) return __VA_ARGS__;\
139
+ } catch (const Napi::Error&) {\
140
+ /* During Node.js/Electron shutdown, when using NAPI_CPP_EXCEPTIONS,*/\
141
+ /* we must take care to not throw any exceptions. */\
142
+ /* But when napi_call_function returns napi_cannot_run_js */\
143
+ /* this throws a C++ exception, when NAPI_CPP_EXCEPTIONS is enabled. */\
144
+ if (g_env_shutting_down.load()) {\
145
+ /* We need to silently ignore exceptions during shutdown. */\
146
+ return __VA_ARGS__;\
147
+ }\
148
+ /* Real rror - re-throw to propagate it */\
149
+ throw;\
150
+ }
133
151
 
134
152
  #define WORK_DEFINITION(name) \
135
153
  Napi::Value name(const Napi::CallbackInfo& info); \
@@ -204,4 +222,4 @@ inline bool OtherIsInt(Napi::Number source) {
204
222
  backup->Process(); \
205
223
  backup->db->Process();
206
224
 
207
- #endif
225
+ #endif
@@ -2,6 +2,7 @@
2
2
  #include <sstream>
3
3
  #include <cstring>
4
4
  #include <string>
5
+ #include <atomic>
5
6
  #include <sqlite3.h>
6
7
 
7
8
  #include "macros.h"
@@ -11,11 +12,21 @@
11
12
 
12
13
  using namespace node_sqlite3;
13
14
 
15
+ // Global flag set when the environment is shutting down.
16
+ std::atomic<bool> g_env_shutting_down{false};
17
+
18
+ static void EnvCleanupHook(void* /*data*/) {
19
+ g_env_shutting_down.store(true);
20
+ }
21
+
14
22
  namespace {
15
23
 
16
24
  Napi::Object RegisterModule(Napi::Env env, Napi::Object exports) {
17
25
  Napi::HandleScope scope(env);
18
26
 
27
+ // Register cleanup hook to detect shutdown
28
+ napi_add_env_cleanup_hook(env, EnvCleanupHook, nullptr);
29
+
19
30
  Database::Init(env, exports);
20
31
  Statement::Init(env, exports);
21
32
  Backup::Init(env, exports);