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/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,
@@ -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
  /**
@@ -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
- var first = result.errors.length ? result.errors[0] : null;
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
- const nest = (0, xsqlvar_1.resolveNestTables)(options, self.connection.options);
277
- fetchBlobSyncRow(row, meta, nest, self.connection._lowercase_keys, function (blobErr) {
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. Result metadata is only available through the
472
- * callback API — the promises resolve with the rows alone.
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;
@@ -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;
@@ -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);
@@ -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, Buffer.from(data).toString('hex'), this.encrypt, defer);
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, Buffer.from(data)])
146
- : Buffer.from(data);
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
@@ -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
- statement.release();
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
- statement.release();
134
- if (callback)
135
- callback(undefined, ret.data[0], statement.output, true);
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, ret) {
272
+ statement.fetch(self, 1, function (err, fret) {
140
273
  if (err) {
141
274
  dropError(err);
142
275
  return;
143
276
  }
144
- statement.release();
145
- if (callback)
146
- callback(undefined, ret.data[0], statement.output, false);
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
- statement.release();
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;