@homeofthings/sqlite3 6.3.0 → 6.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/README.md +32 -2
- package/deps/common-sqlite.gypi +1 -1
- package/deps/sqlite-autoconf-3530000.tar.gz +0 -0
- package/lib/promise/backup.js +121 -0
- package/lib/promise/database.js +517 -0
- package/lib/promise/index.js +16 -0
- package/lib/promise/statement.js +185 -0
- package/lib/sqlite3.d.ts +20 -0
- package/package.json +4 -4
- package/deps/sqlite-autoconf-3510300.tar.gz +0 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Asynchronous, non-blocking [SQLite3](https://sqlite.org/) bindings for [Node.js]
|
|
|
20
20
|
- [Extension support](https://github.com/gms1/node-sqlite3/wiki/API#databaseloadextensionpath-callback), including bundled support for the [json1 extension](https://www.sqlite.org/json1.html)
|
|
21
21
|
- Big test suite
|
|
22
22
|
- Written in modern C++ and tested for memory leaks
|
|
23
|
-
- Bundles SQLite v3.
|
|
23
|
+
- Bundles SQLite v3.53.0, or you can build using a local SQLite
|
|
24
24
|
|
|
25
25
|
# Installing
|
|
26
26
|
|
|
@@ -245,6 +245,34 @@ npm install @homeofthings/sqlite3 --build-from-source --sqlite_libname=sqlcipher
|
|
|
245
245
|
npm test
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
+
# Benchmarks
|
|
249
|
+
|
|
250
|
+
## Driver Comparison
|
|
251
|
+
|
|
252
|
+
The `tools/benchmark-drivers` directory contains a comprehensive benchmark suite comparing different SQLite drivers for Node.js:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
cd tools/benchmark-drivers
|
|
256
|
+
npm install
|
|
257
|
+
node index.js
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This compares `@homeofthings/sqlite3` against other popular SQLite drivers:
|
|
261
|
+
- `better-sqlite3` - Synchronous, high-performance
|
|
262
|
+
- `node:sqlite` - Built-in Node.js SQLite (v22.6.0+)
|
|
263
|
+
|
|
264
|
+
See [tools/benchmark-drivers/README.md](tools/benchmark-drivers/README.md) for details.
|
|
265
|
+
|
|
266
|
+
**Key insight**: Async drivers like `@homeofthings/sqlite3` show lower raw throughput but provide better event loop availability, allowing other operations to proceed concurrently. Sync drivers block the event loop completely.
|
|
267
|
+
|
|
268
|
+
## Internal Benchmarks
|
|
269
|
+
|
|
270
|
+
Internal performance benchmarks are available in `tools/benchmark-internal`:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
node tools/benchmark-internal/run.js
|
|
274
|
+
```
|
|
275
|
+
|
|
248
276
|
# Contributors
|
|
249
277
|
|
|
250
278
|
* [Daniel Lockyer](https://github.com/daniellockyer)
|
|
@@ -269,7 +297,9 @@ Thanks to [Orlando Vazquez](https://github.com/orlandov),
|
|
|
269
297
|
[Eric Fredricksen](https://github.com/grumdrig) and
|
|
270
298
|
[Ryan Dahl](https://github.com/ry) for their SQLite bindings for node, and to mraleph on Freenode's #v8 for answering questions.
|
|
271
299
|
|
|
272
|
-
This module was originally created by [Mapbox](https://mapbox.com/)
|
|
300
|
+
This module was originally created by [Mapbox](https://mapbox.com/), then it was taken over by [Ghost](https://ghost.org), but was then deprecated without prior notice, so that the original is no longer maintained. See [TryGhost/node-sqlite3](https://github.com/TryGhost/node-sqlite3)
|
|
301
|
+
|
|
302
|
+
I still hope that it will eventually be taken over by a larger organization, but in the meantime, I'm trying to maintain this fork here.
|
|
273
303
|
|
|
274
304
|
# Changelog
|
|
275
305
|
|
package/deps/common-sqlite.gypi
CHANGED
|
Binary file
|
|
@@ -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,517 @@
|
|
|
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
|
+
* IMPORTANT: SQLite provides isolation between different database connections,
|
|
328
|
+
* but NOT between operations on the same connection. If you need concurrent
|
|
329
|
+
* transactions with proper isolation, use separate SqliteDatabase instances.
|
|
330
|
+
*
|
|
331
|
+
* Note: This uses BEGIN IMMEDIATE TRANSACTION which acquires a write lock
|
|
332
|
+
* immediately. If another connection holds a write lock, this will fail
|
|
333
|
+
* with SQLITE_BUSY error.
|
|
334
|
+
*
|
|
335
|
+
* @template T
|
|
336
|
+
* @param {() => Promise<T>} callback - The callback to run in transaction
|
|
337
|
+
* @returns {Promise<T>} A promise that resolves to the callback result
|
|
338
|
+
* @see https://www.sqlite.org/isolation.html
|
|
339
|
+
*/
|
|
340
|
+
async transactionalize(callback) {
|
|
341
|
+
await this.beginTransaction();
|
|
342
|
+
try {
|
|
343
|
+
const result = await callback();
|
|
344
|
+
await this.commitTransaction();
|
|
345
|
+
return result;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
await this.rollbackTransaction();
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Begin a transaction using BEGIN IMMEDIATE TRANSACTION
|
|
354
|
+
*
|
|
355
|
+
* This acquires a write lock immediately, preventing deadlocks by
|
|
356
|
+
* failing fast if another connection holds the lock (SQLITE_BUSY).
|
|
357
|
+
*
|
|
358
|
+
* @returns {Promise<SqlRunResult>}
|
|
359
|
+
*/
|
|
360
|
+
beginTransaction() {
|
|
361
|
+
return this.run('BEGIN IMMEDIATE TRANSACTION');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Commit a transaction
|
|
366
|
+
*
|
|
367
|
+
* @returns {Promise<SqlRunResult>}
|
|
368
|
+
*/
|
|
369
|
+
commitTransaction() {
|
|
370
|
+
return this.run('COMMIT TRANSACTION');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Rollback a transaction
|
|
375
|
+
*
|
|
376
|
+
* @returns {Promise<SqlRunResult>}
|
|
377
|
+
*/
|
|
378
|
+
rollbackTransaction() {
|
|
379
|
+
return this.run('ROLLBACK TRANSACTION');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* End a transaction (commit or rollback)
|
|
384
|
+
*
|
|
385
|
+
* @param {boolean} commit - Whether to commit (true) or rollback (false)
|
|
386
|
+
* @returns {Promise<void>}
|
|
387
|
+
*/
|
|
388
|
+
endTransaction(commit) {
|
|
389
|
+
const sql = commit ? 'COMMIT TRANSACTION' : 'ROLLBACK TRANSACTION';
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
if (!this.db) {
|
|
392
|
+
reject(new Error('database connection not open'));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.db.run(sql, (err) => {
|
|
396
|
+
// Ignore "no transaction" errors
|
|
397
|
+
if (err && !err.message.includes('no transaction')) {
|
|
398
|
+
reject(err);
|
|
399
|
+
} else {
|
|
400
|
+
resolve();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Load an extension
|
|
408
|
+
*
|
|
409
|
+
* @param {string} filename - The path to the extension
|
|
410
|
+
* @returns {Promise<void>}
|
|
411
|
+
*/
|
|
412
|
+
loadExtension(filename) {
|
|
413
|
+
return new Promise((resolve, reject) => {
|
|
414
|
+
if (!this.db) {
|
|
415
|
+
reject(new Error('database connection not open'));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.db.loadExtension(filename, (err) => {
|
|
419
|
+
if (err) {
|
|
420
|
+
reject(err);
|
|
421
|
+
} else {
|
|
422
|
+
resolve();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Wait for the database to be ready
|
|
430
|
+
*
|
|
431
|
+
* @returns {Promise<void>}
|
|
432
|
+
*/
|
|
433
|
+
wait() {
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
if (!this.db) {
|
|
436
|
+
reject(new Error('database connection not open'));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.db.wait((err) => {
|
|
440
|
+
if (err) {
|
|
441
|
+
reject(err);
|
|
442
|
+
} else {
|
|
443
|
+
resolve();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Interrupt a running query
|
|
451
|
+
*/
|
|
452
|
+
interrupt() {
|
|
453
|
+
if (!this.db) {
|
|
454
|
+
throw new Error('database connection not open');
|
|
455
|
+
}
|
|
456
|
+
this.db.interrupt();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Configure database options
|
|
461
|
+
*
|
|
462
|
+
* @param {string} option - The option name
|
|
463
|
+
* @param {number} [value] - The option value
|
|
464
|
+
*/
|
|
465
|
+
configure(option, value) {
|
|
466
|
+
if (!this.db) {
|
|
467
|
+
throw new Error('database connection not open');
|
|
468
|
+
}
|
|
469
|
+
this.db.configure(option, value);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Register an event listener
|
|
474
|
+
*
|
|
475
|
+
* @param {'trace' | 'profile' | 'error' | 'close' | 'open' | 'change'} event - The event name
|
|
476
|
+
* @param {Function} listener - The listener function
|
|
477
|
+
* @returns {this}
|
|
478
|
+
*/
|
|
479
|
+
on(event, listener) {
|
|
480
|
+
if (!this.db) {
|
|
481
|
+
throw new Error('database connection not open');
|
|
482
|
+
}
|
|
483
|
+
this.db.on(event, listener);
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Remove an event listener
|
|
489
|
+
*
|
|
490
|
+
* @param {string} event - The event name
|
|
491
|
+
* @param {Function} listener - The listener function
|
|
492
|
+
* @returns {this}
|
|
493
|
+
*/
|
|
494
|
+
off(event, listener) {
|
|
495
|
+
if (!this.db) {
|
|
496
|
+
throw new Error('database connection not open');
|
|
497
|
+
}
|
|
498
|
+
this.db.off(event, listener);
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Remove all event listeners for an event
|
|
504
|
+
*
|
|
505
|
+
* @param {string} [event] - The event name
|
|
506
|
+
* @returns {this}
|
|
507
|
+
*/
|
|
508
|
+
removeAllListeners(event) {
|
|
509
|
+
if (!this.db) {
|
|
510
|
+
throw new Error('database connection not open');
|
|
511
|
+
}
|
|
512
|
+
this.db.removeAllListeners(event);
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
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,185 @@
|
|
|
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
|
+
* IMPORTANT: You MUST finalize all prepared statements before closing the database.
|
|
64
|
+
* If you attempt to close a database with unfinalized statements, you will get:
|
|
65
|
+
* SQLITE_BUSY: unable to close due to unfinalised statements
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
finalize() {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
this._stmt.finalize((err) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
reject(err);
|
|
74
|
+
} else {
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Runs a prepared statement with the specified parameters
|
|
83
|
+
*
|
|
84
|
+
* @param {any} [params] - The parameters referenced in the statement
|
|
85
|
+
* @returns {Promise<SqlRunResult>} A promise that resolves to the result object
|
|
86
|
+
*/
|
|
87
|
+
run(params) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
// Use function() to get 'this' context with lastID and changes
|
|
90
|
+
const callback = function(err) {
|
|
91
|
+
if (err) {
|
|
92
|
+
reject(err);
|
|
93
|
+
} else {
|
|
94
|
+
resolve({
|
|
95
|
+
lastID: this.lastID,
|
|
96
|
+
changes: this.changes
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
// Only pass params if provided - otherwise use bound parameters
|
|
101
|
+
if (params === undefined) {
|
|
102
|
+
this._stmt.run(callback);
|
|
103
|
+
} else {
|
|
104
|
+
this._stmt.run(params, callback);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Runs a prepared statement with the specified parameters, fetching only the first row
|
|
111
|
+
*
|
|
112
|
+
* @template T
|
|
113
|
+
* @param {any} [params] - The parameters referenced in the statement
|
|
114
|
+
* @returns {Promise<T | undefined>} A promise that resolves to the row or undefined
|
|
115
|
+
*/
|
|
116
|
+
get(params) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const callback = (err, row) => {
|
|
119
|
+
if (err) {
|
|
120
|
+
reject(err);
|
|
121
|
+
} else {
|
|
122
|
+
resolve(row);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
// Only pass params if provided - otherwise use bound parameters
|
|
126
|
+
if (params === undefined) {
|
|
127
|
+
this._stmt.get(callback);
|
|
128
|
+
} else {
|
|
129
|
+
this._stmt.get(params, callback);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Runs a prepared statement with the specified parameters, fetching all rows
|
|
136
|
+
*
|
|
137
|
+
* @template T
|
|
138
|
+
* @param {any} [params] - The parameters referenced in the statement
|
|
139
|
+
* @returns {Promise<T[]>} A promise that resolves to an array of rows
|
|
140
|
+
*/
|
|
141
|
+
all(params) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const callback = (err, rows) => {
|
|
144
|
+
if (err) {
|
|
145
|
+
reject(err);
|
|
146
|
+
} else {
|
|
147
|
+
resolve(rows);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
// Only pass params if provided - otherwise use bound parameters
|
|
151
|
+
if (params === undefined) {
|
|
152
|
+
this._stmt.all(callback);
|
|
153
|
+
} else {
|
|
154
|
+
this._stmt.all(params, callback);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Runs a prepared statement with the specified parameters, fetching all rows
|
|
161
|
+
* using a callback for each row
|
|
162
|
+
*
|
|
163
|
+
* @param {any} [params] - The parameters referenced in the statement
|
|
164
|
+
* @param {(err: Error | null, row: any) => void} [callback] - The callback function for each row
|
|
165
|
+
* @returns {Promise<number>} A promise that resolves to the number of rows retrieved
|
|
166
|
+
*/
|
|
167
|
+
each(params, callback) {
|
|
168
|
+
// Handle case where params is actually the callback (no params provided)
|
|
169
|
+
if (typeof params === 'function') {
|
|
170
|
+
callback = params;
|
|
171
|
+
params = undefined;
|
|
172
|
+
}
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
this._stmt.each(params, callback, (err, count) => {
|
|
175
|
+
if (err) {
|
|
176
|
+
reject(err);
|
|
177
|
+
} else {
|
|
178
|
+
resolve(count);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { SqliteStatement };
|
package/lib/sqlite3.d.ts
CHANGED
|
@@ -265,7 +265,22 @@ export class SqliteDatabase {
|
|
|
265
265
|
serialize(callback?: () => void): void;
|
|
266
266
|
parallelize(callback?: () => void): void;
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Run callback inside a database transaction.
|
|
270
|
+
*
|
|
271
|
+
* IMPORTANT: SQLite provides isolation between different database connections,
|
|
272
|
+
* but NOT between operations on the same connection. For concurrent transactions
|
|
273
|
+
* with proper isolation, use separate SqliteDatabase instances.
|
|
274
|
+
*
|
|
275
|
+
* Uses BEGIN IMMEDIATE TRANSACTION which acquires a write lock immediately.
|
|
276
|
+
* @see https://www.sqlite.org/isolation.html
|
|
277
|
+
*/
|
|
268
278
|
transactionalize<T>(callback: () => Promise<T>): Promise<T>;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Begin a transaction using BEGIN IMMEDIATE TRANSACTION.
|
|
282
|
+
* Acquires write lock immediately - fails with SQLITE_BUSY if lock unavailable.
|
|
283
|
+
*/
|
|
269
284
|
beginTransaction(): Promise<void>;
|
|
270
285
|
commitTransaction(): Promise<void>;
|
|
271
286
|
rollbackTransaction(): Promise<void>;
|
|
@@ -300,6 +315,11 @@ export class SqliteStatement {
|
|
|
300
315
|
callback?: (err: Error | null, row: T) => void,
|
|
301
316
|
): Promise<number>;
|
|
302
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Finalizes the statement.
|
|
320
|
+
* IMPORTANT: You MUST finalize all prepared statements before closing the database.
|
|
321
|
+
* Otherwise you will get SQLITE_BUSY: unable to close due to unfinalised statements
|
|
322
|
+
*/
|
|
303
323
|
finalize(): Promise<void>;
|
|
304
324
|
}
|
|
305
325
|
|
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.
|
|
4
|
+
"version": "6.4.0",
|
|
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
|
|
39
|
-
"lib
|
|
38
|
+
"lib/**/*.js",
|
|
39
|
+
"lib/**/*.d.ts",
|
|
40
40
|
"src/"
|
|
41
41
|
],
|
|
42
42
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@eslint/js": "^10.0.1",
|
|
54
54
|
"eslint": "^10.2.0",
|
|
55
|
-
"globals": "^17.
|
|
55
|
+
"globals": "^17.5.0",
|
|
56
56
|
"mocha": "11.7.5",
|
|
57
57
|
"nyc": "^18.0.0",
|
|
58
58
|
"prebuild": "13.0.1",
|
|
Binary file
|