node-firebird 2.11.0 → 2.13.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 +287 -9
- package/lib/pool.d.ts +14 -1
- package/lib/pool.js +59 -16
- package/lib/sql-template.d.ts +81 -0
- package/lib/sql-template.js +162 -0
- package/lib/types.d.ts +152 -5
- package/lib/uri.js +53 -2
- package/lib/utils.d.ts +12 -0
- package/lib/utils.js +20 -2
- package/lib/wire/batch-stream.d.ts +26 -0
- package/lib/wire/batch-stream.js +109 -0
- package/lib/wire/codepages.d.ts +23 -0
- package/lib/wire/codepages.js +137 -0
- package/lib/wire/connection.d.ts +44 -4
- package/lib/wire/connection.js +344 -80
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +12 -0
- package/lib/wire/database.d.ts +18 -0
- package/lib/wire/database.js +40 -13
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +14 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +33 -0
- package/lib/wire/transaction.js +156 -13
- package/lib/wire/xsqlvar.d.ts +118 -2
- package/lib/wire/xsqlvar.js +271 -34
- package/package.json +19 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +153 -6
- package/src/uri.ts +54 -2
- package/src/utils.ts +19 -1
- package/src/wire/batch-stream.ts +121 -0
- package/src/wire/codepages.ts +147 -0
- package/src/wire/connection.ts +374 -86
- package/src/wire/const.ts +13 -0
- package/src/wire/database.ts +46 -15
- package/src/wire/serialize.ts +16 -1
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +166 -19
- package/src/wire/xsqlvar.ts +298 -34
package/lib/wire/const.js
CHANGED
|
@@ -540,6 +540,12 @@ const sqlInfo = {
|
|
|
540
540
|
isc_info_sql_stmt_type: 21,
|
|
541
541
|
isc_info_sql_get_plan: 22,
|
|
542
542
|
isc_info_sql_records: 23,
|
|
543
|
+
// per-verb row counts nested inside an isc_info_sql_records cluster
|
|
544
|
+
// (inf_pub.h isc_info_req_*)
|
|
545
|
+
isc_info_req_select_count: 13,
|
|
546
|
+
isc_info_req_insert_count: 14,
|
|
547
|
+
isc_info_req_update_count: 15,
|
|
548
|
+
isc_info_req_delete_count: 16,
|
|
543
549
|
isc_info_sql_batch_fetch: 24,
|
|
544
550
|
isc_info_sql_relation_alias: 25, // >: 2.0
|
|
545
551
|
isc_info_sql_explain_plan: 26, // >= 3.0
|
|
@@ -618,6 +624,11 @@ const DESCRIBE_WITH_SCHEMA = [
|
|
|
618
624
|
sqlInfo.isc_info_sql_length,
|
|
619
625
|
sqlInfo.isc_info_sql_describe_end
|
|
620
626
|
];
|
|
627
|
+
// op_info_sql request for the per-verb DML row counts of an executed
|
|
628
|
+
// statement (withMeta / affectedRows).
|
|
629
|
+
const RECORDS_INFO = [
|
|
630
|
+
sqlInfo.isc_info_sql_records,
|
|
631
|
+
];
|
|
621
632
|
/***********************/
|
|
622
633
|
/* ISC Services */
|
|
623
634
|
/***********************/
|
|
@@ -843,6 +854,7 @@ const Const = Object.freeze({
|
|
|
843
854
|
...defaultOptions,
|
|
844
855
|
DESCRIBE,
|
|
845
856
|
DESCRIBE_WITH_SCHEMA,
|
|
857
|
+
RECORDS_INFO,
|
|
846
858
|
...dpb,
|
|
847
859
|
...dsql,
|
|
848
860
|
...fetchOp,
|
package/lib/wire/database.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Events from 'events';
|
|
2
2
|
import { type Callback, type SimpleCallback } from '../callback';
|
|
3
|
+
import { type SqlTag } from '../sql-template';
|
|
3
4
|
import FbEventManager from './fbEventManager';
|
|
4
5
|
import type Connection from './connection';
|
|
5
6
|
import type Transaction from './transaction';
|
|
@@ -15,7 +16,15 @@ type TransactionArg = TransactionOptions | Isolation | TransactionCb | undefined
|
|
|
15
16
|
declare class Database extends Events.EventEmitter {
|
|
16
17
|
connection: Connection;
|
|
17
18
|
eventid: number;
|
|
19
|
+
private _sql?;
|
|
18
20
|
constructor(connection: Connection);
|
|
21
|
+
/**
|
|
22
|
+
* Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
|
|
23
|
+
* Built lazily on first access; the compiled text is positional-only,
|
|
24
|
+
* so the namedPlaceholders rewriter is disabled — any `:token` in the
|
|
25
|
+
* template is PSQL (EXECUTE BLOCK), not a placeholder.
|
|
26
|
+
*/
|
|
27
|
+
get sql(): SqlTag;
|
|
19
28
|
escape(value: any): string;
|
|
20
29
|
detach(callback?: Callback, force?: boolean): this;
|
|
21
30
|
transaction(options: TransactionArg, callback?: TransactionCb): this;
|
|
@@ -43,6 +52,15 @@ declare class Database extends Events.EventEmitter {
|
|
|
43
52
|
* blobAsText and jsonAsObject all apply.
|
|
44
53
|
*/
|
|
45
54
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): import("node:stream").Readable;
|
|
55
|
+
/**
|
|
56
|
+
* Bulk-insert Writable (the COPY FROM analogue, Firebird 4.0+): write
|
|
57
|
+
* parameter-array rows, they are flushed in chunks through the batch
|
|
58
|
+
* API on one prepared statement. Runs its own transaction — committed
|
|
59
|
+
* on finish, rolled back on error/destroy (all-or-nothing for the
|
|
60
|
+
* whole stream). BLOB columns accept Buffers/strings. After 'finish',
|
|
61
|
+
* stream.recordCount / stream.affectedRows carry the totals.
|
|
62
|
+
*/
|
|
63
|
+
batchStream(query: string, options?: any): import("node:stream").Writable;
|
|
46
64
|
query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): this;
|
|
47
65
|
drop(callback?: SimpleCallback): void;
|
|
48
66
|
/**
|
package/lib/wire/database.js
CHANGED
|
@@ -6,10 +6,12 @@ const events_1 = __importDefault(require("events"));
|
|
|
6
6
|
const callback_1 = require("../callback");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
const const_1 = __importDefault(require("./const"));
|
|
9
|
+
const sql_template_1 = require("../sql-template");
|
|
9
10
|
const xsqlvar_1 = require("./xsqlvar");
|
|
10
11
|
const eventConnection_1 = __importDefault(require("./eventConnection"));
|
|
11
12
|
const fbEventManager_1 = __importDefault(require("./fbEventManager"));
|
|
12
13
|
const query_stream_1 = __importDefault(require("./query-stream"));
|
|
14
|
+
const batch_stream_1 = __importDefault(require("./batch-stream"));
|
|
13
15
|
/***************************************
|
|
14
16
|
*
|
|
15
17
|
* Database
|
|
@@ -76,7 +78,7 @@ function readblob(blob, callback) {
|
|
|
76
78
|
});
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
|
-
function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, callback) {
|
|
81
|
+
function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, transform, callback) {
|
|
80
82
|
if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === const_1.default.SQL_BLOB)) {
|
|
81
83
|
callback(null, row);
|
|
82
84
|
return;
|
|
@@ -86,7 +88,7 @@ function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, callback) {
|
|
|
86
88
|
// duplicate JOIN column names (and nested rows) break that alignment.
|
|
87
89
|
// Array rows (sequentially's legacy boolean form) are keyed by index.
|
|
88
90
|
const isArrayRow = Array.isArray(row);
|
|
89
|
-
const keys = isArrayRow ? null : (0, xsqlvar_1.computeColumnKeys)(meta, nestTables, lowercaseKeys);
|
|
91
|
+
const keys = isArrayRow ? null : (0, xsqlvar_1.computeColumnKeys)(meta, nestTables, lowercaseKeys, transform);
|
|
90
92
|
const blobCells = [];
|
|
91
93
|
for (let i = 0; i < meta.length; i++) {
|
|
92
94
|
if (!meta[i] || meta[i].type !== const_1.default.SQL_BLOB) {
|
|
@@ -126,6 +128,15 @@ class Database extends events_1.default.EventEmitter {
|
|
|
126
128
|
connection.db = this;
|
|
127
129
|
this.eventid = 1;
|
|
128
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
|
|
133
|
+
* Built lazily on first access; the compiled text is positional-only,
|
|
134
|
+
* so the namedPlaceholders rewriter is disabled — any `:token` in the
|
|
135
|
+
* template is PSQL (EXECUTE BLOCK), not a placeholder.
|
|
136
|
+
*/
|
|
137
|
+
get sql() {
|
|
138
|
+
return this._sql || (this._sql = (0, sql_template_1.makeSqlTag)((text, params, options) => this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
139
|
+
}
|
|
129
140
|
escape(value) {
|
|
130
141
|
return (0, utils_1.escape)(value, this.connection.accept.protocolVersion);
|
|
131
142
|
}
|
|
@@ -231,12 +242,7 @@ class Database extends events_1.default.EventEmitter {
|
|
|
231
242
|
}
|
|
232
243
|
if (!result.success) {
|
|
233
244
|
transaction.rollback(function () {
|
|
234
|
-
|
|
235
|
-
var batchError = first
|
|
236
|
-
? first.error
|
|
237
|
-
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
238
|
-
batchError.batchCompletion = result;
|
|
239
|
-
(0, callback_1.doError)(batchError, callback);
|
|
245
|
+
(0, callback_1.doError)((0, utils_1.batchResultToError)(result), callback);
|
|
240
246
|
});
|
|
241
247
|
return;
|
|
242
248
|
}
|
|
@@ -263,6 +269,9 @@ class Database extends events_1.default.EventEmitter {
|
|
|
263
269
|
callback = undefined;
|
|
264
270
|
}
|
|
265
271
|
var self = this;
|
|
272
|
+
var keyResolutionDone = false;
|
|
273
|
+
var resolvedNest;
|
|
274
|
+
var resolvedTransform;
|
|
266
275
|
var _on = function (row, i, meta, next) {
|
|
267
276
|
var done = false;
|
|
268
277
|
var finish = function (err) {
|
|
@@ -272,9 +281,15 @@ class Database extends events_1.default.EventEmitter {
|
|
|
272
281
|
done = true;
|
|
273
282
|
next(err);
|
|
274
283
|
};
|
|
275
|
-
// options is read at call time, after the normalization below
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
// options is read at call time, after the normalization below;
|
|
285
|
+
// both values are query-invariant, so resolve them once on the
|
|
286
|
+
// first row instead of allocating per row
|
|
287
|
+
if (!keyResolutionDone) {
|
|
288
|
+
resolvedNest = (0, xsqlvar_1.resolveNestTables)(options, self.connection.options);
|
|
289
|
+
resolvedTransform = (0, xsqlvar_1.resolveKeyTransform)(options, self.connection.options);
|
|
290
|
+
keyResolutionDone = true;
|
|
291
|
+
}
|
|
292
|
+
fetchBlobSyncRow(row, meta, resolvedNest, self.connection._lowercase_keys, resolvedTransform, function (blobErr) {
|
|
278
293
|
if (blobErr) {
|
|
279
294
|
finish(blobErr);
|
|
280
295
|
return;
|
|
@@ -328,6 +343,17 @@ class Database extends events_1.default.EventEmitter {
|
|
|
328
343
|
queryStream(query, params, options) {
|
|
329
344
|
return (0, query_stream_1.default)(this, query, params, options);
|
|
330
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Bulk-insert Writable (the COPY FROM analogue, Firebird 4.0+): write
|
|
348
|
+
* parameter-array rows, they are flushed in chunks through the batch
|
|
349
|
+
* API on one prepared statement. Runs its own transaction — committed
|
|
350
|
+
* on finish, rolled back on error/destroy (all-or-nothing for the
|
|
351
|
+
* whole stream). BLOB columns accept Buffers/strings. After 'finish',
|
|
352
|
+
* stream.recordCount / stream.affectedRows carry the totals.
|
|
353
|
+
*/
|
|
354
|
+
batchStream(query, options) {
|
|
355
|
+
return (0, batch_stream_1.default)(this, query, options, true);
|
|
356
|
+
}
|
|
331
357
|
query(query, params, callback, options = {}) {
|
|
332
358
|
if (params instanceof Function) {
|
|
333
359
|
options = callback || {};
|
|
@@ -468,8 +494,9 @@ class Database extends events_1.default.EventEmitter {
|
|
|
468
494
|
/*
|
|
469
495
|
* Promise / async-await API.
|
|
470
496
|
* Each *Async method wraps its callback counterpart; the callback API
|
|
471
|
-
* stays untouched.
|
|
472
|
-
*
|
|
497
|
+
* stays untouched. The promises resolve with the rows alone unless
|
|
498
|
+
* { withMeta: true } is passed, which resolves the full
|
|
499
|
+
* { rows, fields, affectedRows, recordCounts, warnings } result.
|
|
473
500
|
*/
|
|
474
501
|
queryAsync(query, params, options) {
|
|
475
502
|
var self = this;
|
package/lib/wire/serialize.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export declare class XdrWriter {
|
|
|
48
48
|
addDecFloat34(value: number | string | bigint): void;
|
|
49
49
|
addUInt(value: number): void;
|
|
50
50
|
addString(s: string, encoding: BufferEncoding): void;
|
|
51
|
+
/** addString for pre-encoded bytes (codepage connection charsets). */
|
|
52
|
+
addStringBuffer(b: Buffer): void;
|
|
51
53
|
addText(s: string, encoding: BufferEncoding): void;
|
|
52
54
|
addParamBuffer(b: Buffer): void;
|
|
53
55
|
addBlr(blr: BlrWriter): void;
|
package/lib/wire/serialize.js
CHANGED
|
@@ -160,6 +160,10 @@ class BlrReader {
|
|
|
160
160
|
break;
|
|
161
161
|
case 4:
|
|
162
162
|
value = this.buffer.readInt32LE(this.pos);
|
|
163
|
+
break;
|
|
164
|
+
case 8:
|
|
165
|
+
// e.g. record counts above 2^31 (isc_info_sql_records)
|
|
166
|
+
value = Number(this.buffer.readBigInt64LE(this.pos));
|
|
163
167
|
}
|
|
164
168
|
this.pos += len;
|
|
165
169
|
return value;
|
|
@@ -273,6 +277,16 @@ class XdrWriter {
|
|
|
273
277
|
this.buffer.fill(0, this.pos + len, this.pos + alen);
|
|
274
278
|
this.pos += alen;
|
|
275
279
|
}
|
|
280
|
+
/** addString for pre-encoded bytes (codepage connection charsets). */
|
|
281
|
+
addStringBuffer(b) {
|
|
282
|
+
var alen = align(b.length);
|
|
283
|
+
this.ensure(alen + 4);
|
|
284
|
+
this.buffer.writeInt32BE(b.length, this.pos);
|
|
285
|
+
this.pos += 4;
|
|
286
|
+
b.copy(this.buffer, this.pos);
|
|
287
|
+
this.buffer.fill(0, this.pos + b.length, this.pos + alen);
|
|
288
|
+
this.pos += alen;
|
|
289
|
+
}
|
|
276
290
|
addText(s, encoding) {
|
|
277
291
|
var len = Buffer.byteLength(s, encoding);
|
|
278
292
|
var alen = align(len);
|
package/lib/wire/socket.js
CHANGED
|
@@ -132,8 +132,14 @@ class Socket {
|
|
|
132
132
|
* Compress and/or encrypt data before sending to socket.
|
|
133
133
|
*/
|
|
134
134
|
write(data, defer = false) {
|
|
135
|
+
// Callers pass views of the connection's shared _msg buffer, and both
|
|
136
|
+
// net.Socket (when it cannot flush immediately) and zlib keep a
|
|
137
|
+
// REFERENCE to the chunk — a later sender rebuilding _msg would then
|
|
138
|
+
// corrupt this queued packet (intermittent, load-dependent). Own the
|
|
139
|
+
// bytes at the send boundary, once.
|
|
140
|
+
data = Buffer.from(data);
|
|
135
141
|
if (process.env.FIREBIRD_DEBUG) {
|
|
136
|
-
console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length,
|
|
142
|
+
console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length, data.toString('hex'), this.encrypt, defer);
|
|
137
143
|
}
|
|
138
144
|
if (defer) {
|
|
139
145
|
// Accumulate deferred packets instead of overwriting. Multiple
|
|
@@ -142,8 +148,8 @@ class Socket {
|
|
|
142
148
|
// overwriting the buffer silently drops packets and desynchronises
|
|
143
149
|
// the request/response queue, causing the connection to hang.
|
|
144
150
|
this.buffer = this.buffer
|
|
145
|
-
? Buffer.concat([this.buffer,
|
|
146
|
-
:
|
|
151
|
+
? Buffer.concat([this.buffer, data])
|
|
152
|
+
: data;
|
|
147
153
|
return;
|
|
148
154
|
}
|
|
149
155
|
if (!defer && this.buffer) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Callback, type SimpleCallback } from '../callback';
|
|
2
|
+
import { type SqlTag } from '../sql-template';
|
|
2
3
|
import type Connection from './connection';
|
|
3
4
|
import type Database from './database';
|
|
4
5
|
import type Statement from './statement';
|
|
@@ -8,7 +9,33 @@ declare class Transaction {
|
|
|
8
9
|
connection: Connection;
|
|
9
10
|
db: Database;
|
|
10
11
|
handle: number;
|
|
12
|
+
private _sql?;
|
|
11
13
|
constructor(connection: Connection);
|
|
14
|
+
/**
|
|
15
|
+
* Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
|
|
16
|
+
* Built lazily — transactions are created per-query internally, and
|
|
17
|
+
* those throwaway instances must not pay for the tag. The compiled text
|
|
18
|
+
* is positional-only, so the namedPlaceholders rewriter is disabled:
|
|
19
|
+
* any `:token` in the template is PSQL (EXECUTE BLOCK), not a
|
|
20
|
+
* placeholder.
|
|
21
|
+
*/
|
|
22
|
+
get sql(): SqlTag;
|
|
23
|
+
/** Current savepoint nesting depth (names savepoints, see savepoint()). */
|
|
24
|
+
private _savepointDepth;
|
|
25
|
+
/**
|
|
26
|
+
* Run `work` inside a savepoint (Firebird 1.5+): on resolve the
|
|
27
|
+
* savepoint is released, on reject the transaction rolls back TO the
|
|
28
|
+
* savepoint — undoing only work's changes — and the error is rethrown,
|
|
29
|
+
* leaving the transaction itself usable. Nestable (each call generates
|
|
30
|
+
* a fresh NF_SP_n name), mirroring db.withTransaction's style and
|
|
31
|
+
* Postgres.js's sql.savepoint().
|
|
32
|
+
*
|
|
33
|
+
* Do NOT run sibling savepoints concurrently on one transaction
|
|
34
|
+
* (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
|
|
35
|
+
* savepoint created after it, so interleaved siblings release each
|
|
36
|
+
* other. Nested (awaited) savepoints are fine.
|
|
37
|
+
*/
|
|
38
|
+
savepoint<T>(work: (transaction: this) => Promise<T> | T): Promise<T>;
|
|
12
39
|
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
13
40
|
private namedPlaceholdersEnabled;
|
|
14
41
|
newStatement(query: string, callback: StatementCb, options?: InternalQueryOptions): void;
|
|
@@ -21,6 +48,12 @@ declare class Transaction {
|
|
|
21
48
|
* stream ends — commit or roll back yourself.
|
|
22
49
|
*/
|
|
23
50
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): import("node:stream").Readable;
|
|
51
|
+
/**
|
|
52
|
+
* Bulk-insert Writable running inside this transaction (see
|
|
53
|
+
* Database.batchStream). The transaction is NOT committed or rolled
|
|
54
|
+
* back by the stream — settle it yourself after 'finish'/'error'.
|
|
55
|
+
*/
|
|
56
|
+
batchStream(query: string, options?: any): import("node:stream").Writable;
|
|
24
57
|
query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): void;
|
|
25
58
|
/**
|
|
26
59
|
* Execute `query` once per row in `rows` using the Firebird 4 batch API
|
package/lib/wire/transaction.js
CHANGED
|
@@ -6,7 +6,10 @@ const callback_1 = require("../callback");
|
|
|
6
6
|
const named_params_1 = require("../named-params");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
const const_1 = __importDefault(require("./const"));
|
|
9
|
+
const sql_template_1 = require("../sql-template");
|
|
10
|
+
const xsqlvar_1 = require("./xsqlvar");
|
|
9
11
|
const query_stream_1 = __importDefault(require("./query-stream"));
|
|
12
|
+
const batch_stream_1 = __importDefault(require("./batch-stream"));
|
|
10
13
|
/***************************************
|
|
11
14
|
*
|
|
12
15
|
* Transaction
|
|
@@ -44,9 +47,73 @@ function hookAbortSignal(connection, signal, callback) {
|
|
|
44
47
|
}
|
|
45
48
|
class Transaction {
|
|
46
49
|
constructor(connection) {
|
|
50
|
+
/** Current savepoint nesting depth (names savepoints, see savepoint()). */
|
|
51
|
+
this._savepointDepth = 0;
|
|
47
52
|
this.connection = connection;
|
|
48
53
|
this.db = connection.db;
|
|
49
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
|
|
57
|
+
* Built lazily — transactions are created per-query internally, and
|
|
58
|
+
* those throwaway instances must not pay for the tag. The compiled text
|
|
59
|
+
* is positional-only, so the namedPlaceholders rewriter is disabled:
|
|
60
|
+
* any `:token` in the template is PSQL (EXECUTE BLOCK), not a
|
|
61
|
+
* placeholder.
|
|
62
|
+
*/
|
|
63
|
+
get sql() {
|
|
64
|
+
return this._sql || (this._sql = (0, sql_template_1.makeSqlTag)((text, params, options) => this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run `work` inside a savepoint (Firebird 1.5+): on resolve the
|
|
68
|
+
* savepoint is released, on reject the transaction rolls back TO the
|
|
69
|
+
* savepoint — undoing only work's changes — and the error is rethrown,
|
|
70
|
+
* leaving the transaction itself usable. Nestable (each call generates
|
|
71
|
+
* a fresh NF_SP_n name), mirroring db.withTransaction's style and
|
|
72
|
+
* Postgres.js's sql.savepoint().
|
|
73
|
+
*
|
|
74
|
+
* Do NOT run sibling savepoints concurrently on one transaction
|
|
75
|
+
* (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
|
|
76
|
+
* savepoint created after it, so interleaved siblings release each
|
|
77
|
+
* other. Nested (awaited) savepoints are fine.
|
|
78
|
+
*/
|
|
79
|
+
async savepoint(work) {
|
|
80
|
+
if (typeof work !== 'function') {
|
|
81
|
+
throw new Error('savepoint(work) expects a function');
|
|
82
|
+
}
|
|
83
|
+
// named by nesting depth, not a global counter: sequential
|
|
84
|
+
// savepoints at the same depth reuse the same three SQL strings, so
|
|
85
|
+
// the statement cache serves them instead of accumulating
|
|
86
|
+
// single-use entries (redefining a released savepoint name is legal)
|
|
87
|
+
const name = 'NF_SP_' + (++this._savepointDepth);
|
|
88
|
+
try {
|
|
89
|
+
await this.queryAsync('SAVEPOINT ' + name);
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = await work(this);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// only a work() failure rolls back to the savepoint — a
|
|
96
|
+
// RELEASE failure below must NOT undo work's successful
|
|
97
|
+
// changes
|
|
98
|
+
try {
|
|
99
|
+
await this.queryAsync('ROLLBACK TO SAVEPOINT ' + name);
|
|
100
|
+
}
|
|
101
|
+
catch (rollbackErr) {
|
|
102
|
+
// the original failure matters more; keep the rollback
|
|
103
|
+
// failure attached for diagnosis
|
|
104
|
+
if (err && typeof err === 'object') {
|
|
105
|
+
err.savepointRollbackError = rollbackErr;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
await this.queryAsync('RELEASE SAVEPOINT ' + name);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this._savepointDepth--;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
50
117
|
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
51
118
|
namedPlaceholdersEnabled(options) {
|
|
52
119
|
if (options && options.namedPlaceholders !== undefined)
|
|
@@ -116,6 +183,66 @@ class Transaction {
|
|
|
116
183
|
dropError(err);
|
|
117
184
|
return;
|
|
118
185
|
}
|
|
186
|
+
// withMeta applies to query/execute only: in streaming mode
|
|
187
|
+
// (sequentially/queryStream, which spread user options) rows
|
|
188
|
+
// bypass fetchAll's array, so a result object here would
|
|
189
|
+
// carry rows: [] and a meaningless affectedRows
|
|
190
|
+
var withMeta = Boolean(options && typeof options === 'object' &&
|
|
191
|
+
options.withMeta && !options.asStream);
|
|
192
|
+
// Deliver the historic result shape, or — when options.withMeta
|
|
193
|
+
// is set — request the per-verb DML row counts while the
|
|
194
|
+
// statement handle is still open and wrap everything in a
|
|
195
|
+
// { rows, fields, affectedRows, recordCounts, warnings } object.
|
|
196
|
+
function deliver(rows, isSelect, plainDml) {
|
|
197
|
+
if (!withMeta) {
|
|
198
|
+
statement.release();
|
|
199
|
+
if (callback) {
|
|
200
|
+
if (plainDml) {
|
|
201
|
+
// plain DML historically calls back with no args
|
|
202
|
+
callback();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
callback(undefined, rows, statement.output, isSelect);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
var execWarnings = (ret && ret.warnings) || [];
|
|
211
|
+
var finalize = function (counts) {
|
|
212
|
+
statement.release();
|
|
213
|
+
if (!callback) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// DML: what the server actually changed; SELECT: rows
|
|
217
|
+
// returned (pg's rowCount convention)
|
|
218
|
+
var affectedRows = counts
|
|
219
|
+
? counts.insertCount + counts.updateCount + counts.deleteCount
|
|
220
|
+
: (Array.isArray(rows) ? rows.length : (rows !== undefined ? 1 : 0));
|
|
221
|
+
callback(undefined, {
|
|
222
|
+
rows: rows,
|
|
223
|
+
fields: (0, xsqlvar_1.describeFields)(statement.output),
|
|
224
|
+
affectedRows: affectedRows,
|
|
225
|
+
recordCounts: counts,
|
|
226
|
+
warnings: execWarnings,
|
|
227
|
+
}, statement.output, isSelect);
|
|
228
|
+
};
|
|
229
|
+
var t = statement.type;
|
|
230
|
+
var isDml = t === const_1.default.isc_info_sql_stmt_insert ||
|
|
231
|
+
t === const_1.default.isc_info_sql_stmt_update ||
|
|
232
|
+
t === const_1.default.isc_info_sql_stmt_delete ||
|
|
233
|
+
t === const_1.default.isc_info_sql_stmt_exec_procedure;
|
|
234
|
+
if (!isDml) {
|
|
235
|
+
finalize();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
self.connection.statementInfo(statement, const_1.default.RECORDS_INFO, function (err, info) {
|
|
239
|
+
if (err) {
|
|
240
|
+
dropError(err);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
finalize((0, xsqlvar_1.parseRecordCounts)(info && info.buffer));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
119
246
|
switch (statement.type) {
|
|
120
247
|
case const_1.default.isc_info_sql_stmt_select:
|
|
121
248
|
statement.fetchAll(self, function (err, r) {
|
|
@@ -123,35 +250,43 @@ class Transaction {
|
|
|
123
250
|
dropError(err);
|
|
124
251
|
return;
|
|
125
252
|
}
|
|
126
|
-
|
|
127
|
-
if (callback)
|
|
128
|
-
callback(undefined, r, statement.output, true);
|
|
253
|
+
deliver(r, true);
|
|
129
254
|
});
|
|
130
255
|
break;
|
|
131
256
|
case const_1.default.isc_info_sql_stmt_exec_procedure:
|
|
132
257
|
if (ret && ret.data && ret.data.length > 0) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
258
|
+
// singleton op_execute2 rows never pass through
|
|
259
|
+
// fetchAll, so their blobAsText fetches must be
|
|
260
|
+
// resolved here (issue #305: EXECUTE PROCEDURE
|
|
261
|
+
// returned text blobs as unresolved functions)
|
|
262
|
+
self.connection.resolveTextBlobs(self, ret, function (blobErr) {
|
|
263
|
+
if (blobErr) {
|
|
264
|
+
dropError(blobErr);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
deliver(ret.data[0], true);
|
|
268
|
+
});
|
|
136
269
|
break;
|
|
137
270
|
}
|
|
138
271
|
else if (statement.output.length) {
|
|
139
|
-
statement.fetch(self, 1, function (err,
|
|
272
|
+
statement.fetch(self, 1, function (err, fret) {
|
|
140
273
|
if (err) {
|
|
141
274
|
dropError(err);
|
|
142
275
|
return;
|
|
143
276
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
277
|
+
self.connection.resolveTextBlobs(self, fret, function (blobErr) {
|
|
278
|
+
if (blobErr) {
|
|
279
|
+
dropError(blobErr);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
deliver(fret.data[0], false);
|
|
283
|
+
});
|
|
147
284
|
});
|
|
148
285
|
break;
|
|
149
286
|
}
|
|
150
287
|
// Fall through is normal
|
|
151
288
|
default:
|
|
152
|
-
|
|
153
|
-
if (callback)
|
|
154
|
-
callback();
|
|
289
|
+
deliver(undefined, false, true);
|
|
155
290
|
break;
|
|
156
291
|
}
|
|
157
292
|
}, options);
|
|
@@ -226,6 +361,14 @@ class Transaction {
|
|
|
226
361
|
queryStream(query, params, options) {
|
|
227
362
|
return (0, query_stream_1.default)(this, query, params, options);
|
|
228
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Bulk-insert Writable running inside this transaction (see
|
|
366
|
+
* Database.batchStream). The transaction is NOT committed or rolled
|
|
367
|
+
* back by the stream — settle it yourself after 'finish'/'error'.
|
|
368
|
+
*/
|
|
369
|
+
batchStream(query, options) {
|
|
370
|
+
return (0, batch_stream_1.default)(this, query, options, false);
|
|
371
|
+
}
|
|
229
372
|
query(query, params, callback, options = {}) {
|
|
230
373
|
if (params instanceof Function) {
|
|
231
374
|
callback = params;
|