@stoolap/node 0.4.2 → 0.5.0-beta.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 +30 -5
- package/index.d.ts +38 -3
- package/index.js +3 -1
- package/lib/database.js +88 -63
- package/lib/ffi.js +13 -1
- package/lib/iterator.js +111 -0
- package/lib/statement.js +155 -107
- package/lib/transaction.js +95 -64
- package/package.json +6 -6
- package/src/stoolap.c +993 -1441
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ const db = await Database.open('file:///absolute/path/to/db');
|
|
|
97
97
|
| `exec(sql)` | `Promise<void>` | Execute a DDL statement |
|
|
98
98
|
| `query(sql, params?)` | `Promise<Object[]>` | Query rows as objects |
|
|
99
99
|
| `queryOne(sql, params?)` | `Promise<Object \| null>` | Query single row |
|
|
100
|
-
| `queryRaw(sql, params?)` | `Promise<{columns, rows}>` | Query
|
|
100
|
+
| `queryRaw(sql, params?)` | `Promise<{columns, rows}>` | Query as arrays of values |
|
|
101
|
+
| `iterate(sql, params?)` | `RowCursor` | Cursor over rows, `for await` or `for...of` |
|
|
101
102
|
| `begin()` | `Promise<Transaction>` | Begin a transaction |
|
|
102
103
|
| `close()` | `Promise<void>` | Close the database |
|
|
103
104
|
|
|
@@ -113,7 +114,8 @@ Sync methods run on the main thread. Faster for simple operations but block the
|
|
|
113
114
|
| `execSync(sql)` | `void` | Execute a DDL statement |
|
|
114
115
|
| `querySync(sql, params?)` | `Object[]` | Query rows as objects |
|
|
115
116
|
| `queryOneSync(sql, params?)` | `Object \| null` | Query single row |
|
|
116
|
-
| `queryRawSync(sql, params?)` | `{columns, rows}` | Query
|
|
117
|
+
| `queryRawSync(sql, params?)` | `{columns, rows}` | Query as arrays of values |
|
|
118
|
+
| `iterate(sql, params?)` | `RowCursor` | Cursor over rows, fetched in chunks |
|
|
117
119
|
| `executeBatchSync(sql, paramsArray)` | `RunResult` | Execute with multiple param sets |
|
|
118
120
|
| `beginSync()` | `Transaction` | Begin a transaction |
|
|
119
121
|
| `prepare(sql)` | `PreparedStatement` | Create a prepared statement |
|
|
@@ -209,7 +211,7 @@ await db.close();
|
|
|
209
211
|
|
|
210
212
|
#### Raw Query Format
|
|
211
213
|
|
|
212
|
-
`queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }
|
|
214
|
+
`queryRaw` / `queryRawSync` return `{ columns: string[], rows: any[][] }`, one array per row, instead of an array of objects. Faster when you don't need named keys.
|
|
213
215
|
|
|
214
216
|
```js
|
|
215
217
|
const raw = db.queryRawSync('SELECT id, name, email FROM users ORDER BY id');
|
|
@@ -217,6 +219,25 @@ console.log(raw.columns); // ['id', 'name', 'email']
|
|
|
217
219
|
console.log(raw.rows); // [[1, 'Alice', 'alice@example.com'], [2, 'Bob', 'bob@example.com']]
|
|
218
220
|
```
|
|
219
221
|
|
|
222
|
+
#### Iterating Rows
|
|
223
|
+
|
|
224
|
+
`iterate` opens a cursor and fetches rows in chunks (32 rows, doubling to 256) as the loop advances, so a large result never has to be materialized at once. The same cursor works with `for...of` and `for await...of`, and it closes itself when the loop ends, breaks or throws. `Database`, `Transaction` and `PreparedStatement` all have it.
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
for (const user of db.iterate('SELECT * FROM users WHERE active = $1', [true])) {
|
|
228
|
+
if (user.balance < 0) break; // the cursor is closed on break
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for await (const row of db.iterate('SELECT * FROM events WHERE day = :day', { day: '2026-10-21' })) {
|
|
232
|
+
await ship(row);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const stmt = db.prepare('SELECT * FROM users WHERE age > :min');
|
|
236
|
+
for (const user of stmt.iterate({ min: 30 })) { /* ... */ }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A cursor opened on a transaction is closed by `commit` and `rollback`; one opened on a database is closed with it. Call `cursor.close()` to end an iteration early without breaking out of a loop.
|
|
240
|
+
|
|
220
241
|
#### Batch Execution
|
|
221
242
|
|
|
222
243
|
Execute the same SQL with multiple parameter sets in a single call. Automatically wraps in a transaction.
|
|
@@ -256,7 +277,8 @@ All methods mirror `Database` but without the `sql` parameter (it's bound at pre
|
|
|
256
277
|
| `execute(params?)` | `executeSync(params?)` | Execute DML statement |
|
|
257
278
|
| `query(params?)` | `querySync(params?)` | Query rows as objects |
|
|
258
279
|
| `queryOne(params?)` | `queryOneSync(params?)` | Query single row |
|
|
259
|
-
| `queryRaw(params?)` | `queryRawSync(params?)` | Query
|
|
280
|
+
| `queryRaw(params?)` | `queryRawSync(params?)` | Query as arrays of values |
|
|
281
|
+
| `iterate(params?)` | — | Cursor over rows, `for await` or `for...of` |
|
|
260
282
|
| | `executeBatchSync(paramsArray)` | Execute with multiple param sets |
|
|
261
283
|
| | `finalize()` | Release the prepared statement |
|
|
262
284
|
|
|
@@ -305,7 +327,8 @@ console.log(result.changes); // 3
|
|
|
305
327
|
| `execute(sql, params?)` | `executeSync(sql, params?)` | Execute DML statement |
|
|
306
328
|
| `query(sql, params?)` | `querySync(sql, params?)` | Query rows as objects |
|
|
307
329
|
| `queryOne(sql, params?)` | `queryOneSync(sql, params?)` | Query single row |
|
|
308
|
-
| `queryRaw(sql, params?)` | `queryRawSync(sql, params?)` | Query
|
|
330
|
+
| `queryRaw(sql, params?)` | `queryRawSync(sql, params?)` | Query as arrays of values |
|
|
331
|
+
| `iterate(sql, params?)` | — | Cursor over rows, closed by commit/rollback |
|
|
309
332
|
| `commit()` | `commitSync()` | Commit the transaction |
|
|
310
333
|
| `rollback()` | `rollbackSync()` | Rollback the transaction |
|
|
311
334
|
| | `executeBatchSync(sql, paramsArray)` | Execute with multiple param sets |
|
|
@@ -379,6 +402,8 @@ db.querySync(
|
|
|
379
402
|
);
|
|
380
403
|
```
|
|
381
404
|
|
|
405
|
+
Synchronous ad hoc statements pass named parameters to the engine as they are: the SQL is not rewritten, a `:name` inside a string literal is left alone, a name used twice is bound once, keys the SQL does not mention are ignored and a name the object lacks binds `NULL`. A prepared statement, and the asynchronous ad hoc methods, resolve `:name` placeholders in the driver and bind by position, which is the engine's fastest path.
|
|
406
|
+
|
|
382
407
|
### Error Handling
|
|
383
408
|
|
|
384
409
|
All methods throw on errors (invalid SQL, constraint violations, etc.):
|
package/index.d.ts
CHANGED
|
@@ -88,7 +88,10 @@ export declare class Database {
|
|
|
88
88
|
* Auto-wraps in a transaction: begin, execute all, commit.
|
|
89
89
|
* Returns { changes: total_rows_affected }.
|
|
90
90
|
*/
|
|
91
|
-
executeBatchSync(
|
|
91
|
+
executeBatchSync(
|
|
92
|
+
sql: string,
|
|
93
|
+
paramsArray: any[][] | Record<string, any>[],
|
|
94
|
+
): RunResult;
|
|
92
95
|
/** Create a prepared statement (synchronous). */
|
|
93
96
|
prepare(sql: string): PreparedStatement;
|
|
94
97
|
/** Begin a transaction. Returns Promise<Transaction>. */
|
|
@@ -97,11 +100,33 @@ export declare class Database {
|
|
|
97
100
|
beginSync(): Transaction;
|
|
98
101
|
/** Close the database. Returns Promise<void>. */
|
|
99
102
|
close(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Open a cursor over the query's rows. Iterate it with `for...of` or
|
|
105
|
+
* `for await...of`; rows are fetched in chunks as the loop advances and
|
|
106
|
+
* the cursor closes itself when the loop ends, breaks or throws.
|
|
107
|
+
*/
|
|
108
|
+
iterate(sql: string, params?: any[] | Record<string, any>): RowCursor;
|
|
100
109
|
/** Close the database synchronously. */
|
|
101
110
|
closeSync(): void;
|
|
102
111
|
}
|
|
103
112
|
export type JsDatabase = Database;
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A cursor over an open result set, from `iterate()`. Iterable both
|
|
116
|
+
* synchronously and with `for await`. Rows cross the native boundary in
|
|
117
|
+
* chunks of 32 to 256 rows.
|
|
118
|
+
*/
|
|
119
|
+
export declare class RowCursor
|
|
120
|
+
implements Iterable<Record<string, any>>, AsyncIterable<Record<string, any>>
|
|
121
|
+
{
|
|
122
|
+
[Symbol.iterator](): Iterator<Record<string, any>>;
|
|
123
|
+
[Symbol.asyncIterator](): AsyncIterator<Record<string, any>>;
|
|
124
|
+
/** End the iteration early and release the result set. */
|
|
125
|
+
close(): void;
|
|
126
|
+
/** True once the cursor was closed or its result set was exhausted. */
|
|
127
|
+
get closed(): boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
export declare class PreparedStatement {
|
|
106
131
|
/** Execute the statement (DML). Returns Promise<{ changes: number }>. */
|
|
107
132
|
execute(params?: any[] | Record<string, any>): Promise<RunResult>;
|
|
@@ -132,7 +157,9 @@ export declare class PreparedStatement {
|
|
|
132
157
|
* Execute the prepared SQL with multiple param sets in a single call.
|
|
133
158
|
* Returns { changes: total_rows_affected }.
|
|
134
159
|
*/
|
|
135
|
-
executeBatchSync(paramsArray: any[][]): RunResult;
|
|
160
|
+
executeBatchSync(paramsArray: any[][] | Record<string, any>[]): RunResult;
|
|
161
|
+
/** Open a cursor over the statement's rows; see `Database.iterate`. */
|
|
162
|
+
iterate(params?: any[] | Record<string, any>): RowCursor;
|
|
136
163
|
/** Get the SQL text of this prepared statement. */
|
|
137
164
|
get sql(): string;
|
|
138
165
|
/** Release the prepared statement. */
|
|
@@ -188,7 +215,15 @@ export declare class Transaction {
|
|
|
188
215
|
* Execute the same SQL with multiple param sets in a single call.
|
|
189
216
|
* Returns { changes: total_rows_affected }.
|
|
190
217
|
*/
|
|
191
|
-
executeBatchSync(
|
|
218
|
+
executeBatchSync(
|
|
219
|
+
sql: string,
|
|
220
|
+
paramsArray: any[][] | Record<string, any>[],
|
|
221
|
+
): RunResult;
|
|
222
|
+
/**
|
|
223
|
+
* Open a cursor over the query's rows inside the transaction; see
|
|
224
|
+
* `Database.iterate`. Commit and rollback close open cursors.
|
|
225
|
+
*/
|
|
226
|
+
iterate(sql: string, params?: any[] | Record<string, any>): RowCursor;
|
|
192
227
|
/** Rollback the transaction synchronously. */
|
|
193
228
|
rollbackSync(): void;
|
|
194
229
|
}
|
package/index.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
const { Database } = require("./lib/database");
|
|
4
4
|
const { Transaction } = require("./lib/transaction");
|
|
5
5
|
const { PreparedStatement } = require("./lib/statement");
|
|
6
|
+
const { RowCursor } = require("./lib/iterator");
|
|
6
7
|
|
|
7
|
-
module.exports = { Database, Transaction, PreparedStatement };
|
|
8
|
+
module.exports = { Database, Transaction, PreparedStatement, RowCursor };
|
|
8
9
|
module.exports.Database = Database;
|
|
9
10
|
module.exports.PreparedStatement = PreparedStatement;
|
|
10
11
|
module.exports.Transaction = Transaction;
|
|
12
|
+
module.exports.RowCursor = RowCursor;
|
|
11
13
|
// Legacy aliases
|
|
12
14
|
module.exports.JsDatabase = Database;
|
|
13
15
|
module.exports.JsPreparedStatement = PreparedStatement;
|
package/lib/database.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
loadStoolap,
|
|
5
|
+
processParams,
|
|
6
|
+
rewriteNamedParams,
|
|
7
|
+
splitNamed,
|
|
8
|
+
} = require("./ffi");
|
|
4
9
|
const { parseRows, parseRaw } = require("./parse");
|
|
5
10
|
const { Transaction } = require("./transaction");
|
|
6
11
|
const { PreparedStatement, encodeBatch } = require("./statement");
|
|
12
|
+
const { RowCursor } = require("./iterator");
|
|
7
13
|
|
|
8
14
|
function normalizeDsn(dbPath) {
|
|
9
15
|
let dsn = dbPath || ":memory:";
|
|
@@ -25,6 +31,7 @@ class Database {
|
|
|
25
31
|
#closeWaiter = null;
|
|
26
32
|
#closingPromise = null;
|
|
27
33
|
#children = new Set();
|
|
34
|
+
#cursors = new Set(); /* WeakRef<RowCursor> */
|
|
28
35
|
|
|
29
36
|
constructor(ptr, stoolap) {
|
|
30
37
|
this.#ptr = ptr;
|
|
@@ -39,13 +46,33 @@ class Database {
|
|
|
39
46
|
this.#children.delete(child);
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
_trackCursor(ref) {
|
|
50
|
+
this.#cursors.add(ref);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_untrackCursor(ref) {
|
|
54
|
+
this.#cursors.delete(ref);
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
#closeChildren() {
|
|
58
|
+
for (const ref of this.#cursors) {
|
|
59
|
+
const cursor = ref.deref();
|
|
60
|
+
if (cursor) cursor._invalidate();
|
|
61
|
+
}
|
|
62
|
+
this.#cursors.clear();
|
|
43
63
|
for (const child of this.#children) {
|
|
44
64
|
child._invalidate();
|
|
45
65
|
}
|
|
46
66
|
this.#children.clear();
|
|
47
67
|
}
|
|
48
68
|
|
|
69
|
+
#childrenPending() {
|
|
70
|
+
for (const child of this.#children) {
|
|
71
|
+
if (child._hasPending && child._hasPending()) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
static async open(dbPath) {
|
|
50
77
|
const stoolap = loadStoolap();
|
|
51
78
|
const ptr = await stoolap.dbOpenAsync(normalizeDsn(dbPath));
|
|
@@ -123,6 +150,16 @@ class Database {
|
|
|
123
150
|
);
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
|
|
154
|
+
_rowsChunk(rowsPtr, maxRows, buf) {
|
|
155
|
+
this.#checkOpen();
|
|
156
|
+
return this.#stoolap.rowsChunk(rowsPtr, maxRows, buf);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_rowsChunkAsync(rowsPtr, maxRows) {
|
|
160
|
+
return this.#runAsync(() => this.#stoolap.rowsChunkAsync(rowsPtr, maxRows));
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
_executeBatchBufferSync(sql, batchBuf) {
|
|
127
164
|
this.#checkOpen();
|
|
128
165
|
return this.#stoolap.dbExecBatchBuf(this.#ptr, sql, batchBuf);
|
|
@@ -152,19 +189,13 @@ class Database {
|
|
|
152
189
|
|
|
153
190
|
executeSync(sql, params) {
|
|
154
191
|
this.#checkOpen();
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return this.#stoolap.dbExec(this.#ptr, sql, params);
|
|
192
|
+
if (params && !Array.isArray(params)) {
|
|
193
|
+
const [names, values] = splitNamed(params);
|
|
194
|
+
return this.#stoolap.dbExecNamed(this.#ptr, sql, names, values);
|
|
159
195
|
}
|
|
160
|
-
|
|
161
|
-
return p.values
|
|
162
|
-
? this.#stoolap.dbExec(this.#ptr, p.sql, p.values)
|
|
163
|
-
: this.#stoolap.dbExec(this.#ptr, sql);
|
|
196
|
+
return this.#stoolap.dbExec(this.#ptr, sql, params);
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
// -- Exec (DDL) --
|
|
167
|
-
|
|
168
199
|
async exec(sql) {
|
|
169
200
|
await this._execAsync(sql);
|
|
170
201
|
}
|
|
@@ -194,27 +225,21 @@ class Database {
|
|
|
194
225
|
return parseRows(buf);
|
|
195
226
|
}
|
|
196
227
|
|
|
228
|
+
/* One FFI call. mode: 0 = rows, 1 = first row, 2 = raw buffer */
|
|
229
|
+
#queryAll(sql, params, mode) {
|
|
230
|
+
if (params && !Array.isArray(params)) {
|
|
231
|
+
const [names, values] = splitNamed(params);
|
|
232
|
+
return this.#stoolap.dbQueryNamedAll(this.#ptr, null, sql, names, values, mode);
|
|
233
|
+
}
|
|
234
|
+
return this.#stoolap.dbQueryAll(this.#ptr, null, sql, params, mode);
|
|
235
|
+
}
|
|
236
|
+
|
|
197
237
|
querySync(sql, params) {
|
|
198
238
|
this.#checkOpen();
|
|
199
|
-
|
|
200
|
-
if (!params) {
|
|
201
|
-
result = this.#stoolap.dbQuery(this.#ptr, sql);
|
|
202
|
-
} else if (Array.isArray(params)) {
|
|
203
|
-
result =
|
|
204
|
-
params.length === 0
|
|
205
|
-
? this.#stoolap.dbQuery(this.#ptr, sql)
|
|
206
|
-
: this.#stoolap.dbQuery(this.#ptr, sql, params);
|
|
207
|
-
} else {
|
|
208
|
-
const p = processParams(sql, params);
|
|
209
|
-
result = p.values
|
|
210
|
-
? this.#stoolap.dbQuery(this.#ptr, p.sql, p.values)
|
|
211
|
-
: this.#stoolap.dbQuery(this.#ptr, sql);
|
|
212
|
-
}
|
|
239
|
+
const result = this.#queryAll(sql, params, 0);
|
|
213
240
|
return Array.isArray(result) ? result : parseRows(result);
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
// -- QueryOne --
|
|
217
|
-
|
|
218
243
|
async queryOne(sql, params) {
|
|
219
244
|
const rows = await this.query(sql, params);
|
|
220
245
|
return rows.length === 0 ? null : rows[0];
|
|
@@ -222,19 +247,14 @@ class Database {
|
|
|
222
247
|
|
|
223
248
|
queryOneSync(sql, params) {
|
|
224
249
|
this.#checkOpen();
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
return
|
|
250
|
+
const result = this.#queryAll(sql, params, 1);
|
|
251
|
+
if (result instanceof ArrayBuffer) {
|
|
252
|
+
const rows = parseRows(result);
|
|
253
|
+
return rows.length === 0 ? null : rows[0];
|
|
229
254
|
}
|
|
230
|
-
|
|
231
|
-
return p.values
|
|
232
|
-
? this.#stoolap.dbQueryOne(this.#ptr, p.sql, p.values)
|
|
233
|
-
: this.#stoolap.dbQueryOne(this.#ptr, sql);
|
|
255
|
+
return result;
|
|
234
256
|
}
|
|
235
257
|
|
|
236
|
-
// -- QueryRaw --
|
|
237
|
-
|
|
238
258
|
async queryRaw(sql, params) {
|
|
239
259
|
let buf;
|
|
240
260
|
if (!params) {
|
|
@@ -255,25 +275,25 @@ class Database {
|
|
|
255
275
|
|
|
256
276
|
queryRawSync(sql, params) {
|
|
257
277
|
this.#checkOpen();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
return parseRaw(this.#queryAll(sql, params, 2));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Open a cursor over the query's rows. Iterate it with `for...of` or
|
|
283
|
+
* `for await...of`; rows are fetched in chunks as the loop advances.
|
|
284
|
+
*/
|
|
285
|
+
iterate(sql, params) {
|
|
286
|
+
this.#checkOpen();
|
|
287
|
+
let ptr;
|
|
288
|
+
if (params && !Array.isArray(params)) {
|
|
289
|
+
const [names, values] = splitNamed(params);
|
|
290
|
+
ptr = this.#stoolap.rowsOpenNamed(this.#ptr, null, sql, names, values);
|
|
266
291
|
} else {
|
|
267
|
-
|
|
268
|
-
buf = p.values
|
|
269
|
-
? this.#stoolap.dbQueryBuf(this.#ptr, p.sql, p.values)
|
|
270
|
-
: this.#stoolap.dbQueryBuf(this.#ptr, sql);
|
|
292
|
+
ptr = this.#stoolap.rowsOpen(this.#ptr, null, sql, params);
|
|
271
293
|
}
|
|
272
|
-
return
|
|
294
|
+
return new RowCursor(this.#stoolap, ptr, this);
|
|
273
295
|
}
|
|
274
296
|
|
|
275
|
-
// -- Batch --
|
|
276
|
-
|
|
277
297
|
executeBatchSync(sql, paramsArray) {
|
|
278
298
|
this.#checkOpen();
|
|
279
299
|
if (paramsArray.length > 0 && Array.isArray(paramsArray[0])) {
|
|
@@ -298,6 +318,7 @@ class Database {
|
|
|
298
318
|
|
|
299
319
|
prepare(sql) {
|
|
300
320
|
this.#checkOpen();
|
|
321
|
+
if (typeof sql !== "string") throw new TypeError("sql must be a string");
|
|
301
322
|
const prepared = rewriteNamedParams(sql);
|
|
302
323
|
const [stmtPtr, queryOneIntBound] = this.#stoolap.dbPrepare(
|
|
303
324
|
this.#ptr,
|
|
@@ -309,7 +330,6 @@ class Database {
|
|
|
309
330
|
queryOneIntBound,
|
|
310
331
|
this,
|
|
311
332
|
sql,
|
|
312
|
-
prepared.sql,
|
|
313
333
|
prepared.names,
|
|
314
334
|
);
|
|
315
335
|
this._trackChild(stmt);
|
|
@@ -338,30 +358,35 @@ class Database {
|
|
|
338
358
|
async close() {
|
|
339
359
|
if (this.#closingPromise) return this.#closingPromise;
|
|
340
360
|
if (!this.#ptr) return;
|
|
341
|
-
|
|
342
|
-
this.#closeChildren();
|
|
343
361
|
const ptr = this.#ptr;
|
|
344
362
|
this.#ptr = null;
|
|
345
|
-
|
|
346
|
-
|
|
363
|
+
/* let queued work on the handle, its statements and its transactions
|
|
364
|
+
* land before anything is released */
|
|
365
|
+
const settling = [];
|
|
366
|
+
for (const child of this.#children) {
|
|
367
|
+
if (child._settle) settling.push(child._settle());
|
|
368
|
+
}
|
|
369
|
+
const wait = Promise.all(settling).then(() =>
|
|
347
370
|
this.#pendingAsync === 0
|
|
348
|
-
?
|
|
371
|
+
? undefined
|
|
349
372
|
: new Promise((resolve) => {
|
|
350
373
|
this.#closeWaiter = resolve;
|
|
351
|
-
})
|
|
352
|
-
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
353
376
|
this.#closingPromise = wait
|
|
354
|
-
.then(() =>
|
|
377
|
+
.then(() => {
|
|
378
|
+
this.#closeChildren();
|
|
379
|
+
return this.#stoolap.dbCloseAsync(ptr);
|
|
380
|
+
})
|
|
355
381
|
.finally(() => {
|
|
356
382
|
this.#closingPromise = null;
|
|
357
383
|
this.#closeWaiter = null;
|
|
358
384
|
});
|
|
359
|
-
|
|
360
385
|
return this.#closingPromise;
|
|
361
386
|
}
|
|
362
387
|
|
|
363
388
|
closeSync() {
|
|
364
|
-
if (this.#closingPromise || this.#pendingAsync > 0) {
|
|
389
|
+
if (this.#closingPromise || this.#pendingAsync > 0 || this.#childrenPending()) {
|
|
365
390
|
throw new Error("Database has pending async operations");
|
|
366
391
|
}
|
|
367
392
|
if (this.#ptr) {
|
package/lib/ffi.js
CHANGED
|
@@ -53,6 +53,7 @@ function loadStoolap() {
|
|
|
53
53
|
|
|
54
54
|
_stoolap = require("../build/Release/stoolap.node");
|
|
55
55
|
_stoolap.loadLibrary(findLibraryPath());
|
|
56
|
+
_stoolap.setRowFactoryMaker(require("./parse").makeRowFactory);
|
|
56
57
|
return _stoolap;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -161,6 +162,9 @@ function processParams(sql, params) {
|
|
|
161
162
|
return { sql: rewritten, values };
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/* Rewrite :name placeholders to $n for a prepared statement. The engine binds
|
|
166
|
+
* prepared statements faster by position than by name, so a statement keeps
|
|
167
|
+
* its parameter names and binds an object's values positionally. */
|
|
164
168
|
function rewriteNamedParams(sql) {
|
|
165
169
|
const seen = new Map();
|
|
166
170
|
const names = [];
|
|
@@ -171,13 +175,21 @@ function rewriteNamedParams(sql) {
|
|
|
171
175
|
}
|
|
172
176
|
return `$${seen.get(name)}`;
|
|
173
177
|
});
|
|
174
|
-
|
|
175
178
|
return names.length === 0 ? { sql, names: null } : { sql: rewritten, names };
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
/* Split a named-parameter object into parallel name and value arrays. */
|
|
182
|
+
function splitNamed(params) {
|
|
183
|
+
const names = Object.keys(params);
|
|
184
|
+
const values = new Array(names.length);
|
|
185
|
+
for (let i = 0; i < names.length; i++) values[i] = params[names[i]];
|
|
186
|
+
return [names, values];
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
module.exports = {
|
|
179
190
|
loadStoolap,
|
|
180
191
|
processParams,
|
|
181
192
|
rewriteNamedParams,
|
|
193
|
+
splitNamed,
|
|
182
194
|
findLibraryPath,
|
|
183
195
|
};
|
package/lib/iterator.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { parseRows, parseHeader, makeRowFactory, parseRowsCached } = require("./parse");
|
|
4
|
+
|
|
5
|
+
const FIRST_CHUNK = 32;
|
|
6
|
+
const MAX_CHUNK = 256;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A row cursor over an open result set. Rows cross the FFI boundary in
|
|
10
|
+
* packed chunks (32 rows, doubling to 256), so a scan costs one call per
|
|
11
|
+
* chunk instead of one per row. Iterate it synchronously or with
|
|
12
|
+
* `for await`; the cursor closes itself when iteration ends, breaks or
|
|
13
|
+
* throws, and `close()` ends it early.
|
|
14
|
+
*/
|
|
15
|
+
class RowCursor {
|
|
16
|
+
#stoolap = null;
|
|
17
|
+
#ptr = null;
|
|
18
|
+
#owner = null;
|
|
19
|
+
#chunk = FIRST_CHUNK;
|
|
20
|
+
#buf = null;
|
|
21
|
+
#meta = null;
|
|
22
|
+
#factory = null;
|
|
23
|
+
#pending = false;
|
|
24
|
+
#closeWanted = false;
|
|
25
|
+
#ref = null;
|
|
26
|
+
|
|
27
|
+
constructor(stoolap, ptr, owner) {
|
|
28
|
+
this.#stoolap = stoolap;
|
|
29
|
+
this.#ptr = ptr;
|
|
30
|
+
this.#owner = owner;
|
|
31
|
+
/* the owner holds only a weak reference: an abandoned cursor can be
|
|
32
|
+
* collected, and the native handle's finalizer closes it */
|
|
33
|
+
this.#ref = new WeakRef(this);
|
|
34
|
+
owner._trackCursor(this.#ref);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#release() {
|
|
38
|
+
if (this.#ptr) {
|
|
39
|
+
this.#stoolap.rowsClose(this.#ptr);
|
|
40
|
+
this.#ptr = null;
|
|
41
|
+
}
|
|
42
|
+
this.#closeWanted = false;
|
|
43
|
+
this.#owner._untrackCursor(this.#ref);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Called by the owner when it closes. A chunk in flight closes the cursor when it lands. */
|
|
47
|
+
_invalidate() {
|
|
48
|
+
if (this.#pending) this.#closeWanted = true;
|
|
49
|
+
else this.#release();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
close() {
|
|
53
|
+
if (this.#pending) this.#closeWanted = true;
|
|
54
|
+
else this.#release();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get closed() {
|
|
58
|
+
return this.#ptr === null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#parse(buf) {
|
|
62
|
+
if (!this.#meta) {
|
|
63
|
+
this.#meta = parseHeader(buf);
|
|
64
|
+
this.#factory = makeRowFactory(this.#meta.colNames);
|
|
65
|
+
}
|
|
66
|
+
const rows = this.#factory
|
|
67
|
+
? parseRowsCached(buf, this.#meta.headerOff, this.#factory, this.#meta.colCount)
|
|
68
|
+
: parseRows(buf);
|
|
69
|
+
if (this.#chunk < MAX_CHUNK) this.#chunk *= 2;
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
*[Symbol.iterator]() {
|
|
74
|
+
try {
|
|
75
|
+
while (this.#ptr) {
|
|
76
|
+
const buf = this.#owner._rowsChunk(this.#ptr, this.#chunk, this.#buf);
|
|
77
|
+
if (!buf) break;
|
|
78
|
+
this.#buf = buf;
|
|
79
|
+
const rows = this.#parse(buf);
|
|
80
|
+
for (let i = 0; i < rows.length; i++) yield rows[i];
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
this.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async *[Symbol.asyncIterator]() {
|
|
88
|
+
try {
|
|
89
|
+
while (this.#ptr) {
|
|
90
|
+
this.#pending = true;
|
|
91
|
+
let buf;
|
|
92
|
+
try {
|
|
93
|
+
buf = await this.#owner._rowsChunkAsync(this.#ptr, this.#chunk);
|
|
94
|
+
} finally {
|
|
95
|
+
this.#pending = false;
|
|
96
|
+
}
|
|
97
|
+
if (this.#closeWanted) {
|
|
98
|
+
this.#release();
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (!buf) break;
|
|
102
|
+
const rows = this.#parse(buf);
|
|
103
|
+
for (let i = 0; i < rows.length; i++) yield rows[i];
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
this.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { RowCursor };
|