node-firebird 2.12.0 → 2.14.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.
@@ -44,6 +44,7 @@ const srp = __importStar(require("../srp"));
44
44
  const crypt = __importStar(require("../unix-crypt"));
45
45
  const const_1 = __importDefault(require("./const"));
46
46
  const Xsql = __importStar(require("./xsqlvar"));
47
+ const codepages_1 = require("./codepages");
47
48
  const service_1 = __importDefault(require("./service"));
48
49
  const database_1 = __importDefault(require("./database"));
49
50
  const statement_1 = __importDefault(require("./statement"));
@@ -109,6 +110,20 @@ function statementCacheLimit(options) {
109
110
  }
110
111
  // SQL type-code names live in xsqlvar.ts alongside the descriptors
111
112
  const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
113
+ /**
114
+ * Build the wire parameter for a text value in the CONNECTION charset:
115
+ * plain SQLParamString for UTF-8 connections, pre-encoded bytes for
116
+ * everything else — SQLParamString hardwires utf8, which corrupted
117
+ * writes on latin1/codepage connections (issues #319/#301).
118
+ */
119
+ function textParam(options, value) {
120
+ const codec = Xsql.resolveTextCodec(options);
121
+ const enc = Xsql.resolveTextEncoding(options);
122
+ if (!codec && enc === 'utf8') {
123
+ return new Xsql.SQLParamString(value);
124
+ }
125
+ return new Xsql.SQLParamBuffer(Xsql.encodeConnectionText(options, value));
126
+ }
112
127
  /**
113
128
  * Run the user's typeCast hook (options.typeCast) for one column value.
114
129
  * The hook receives the column metadata and a next() returning the value
@@ -240,6 +255,24 @@ class Connection {
240
255
  (0, callback_1.doError)(err, queue[i]);
241
256
  }
242
257
  }
258
+ /**
259
+ * Deliver a connection-level error to 'error' listeners — and ONLY to
260
+ * listeners. Emitting an unlistened 'error' makes Node throw the error
261
+ * object as an uncaught exception; for errors that originate in
262
+ * background contexts (the reconnect timer, socket-level failures whose
263
+ * operations are separately rejected via _rejectPending) that crashes
264
+ * the process — or, under a test runner, fails whatever unrelated test
265
+ * happens to be running. The failing operations themselves always
266
+ * still receive their error through their own callbacks.
267
+ */
268
+ _emitError(err) {
269
+ if (this.db && typeof this.db.listenerCount === 'function' && this.db.listenerCount('error') > 0) {
270
+ this.db.emit('error', err);
271
+ }
272
+ else if (process.env.FIREBIRD_DEBUG) {
273
+ console.warn('[fb-debug] connection error (no error listener):', err && err.message);
274
+ }
275
+ }
243
276
  _bind_events(host, port, callback) {
244
277
  var self = this;
245
278
  self._socket.on('close', function () {
@@ -256,6 +289,14 @@ class Connection {
256
289
  return;
257
290
  }
258
291
  self._rejectPending(lostError);
292
+ // Pooled connections do not self-reconnect: the pool is the
293
+ // recovery authority (dead-connection check on checkout + the
294
+ // reaper), and a background reconnect here would produce a
295
+ // zombie attachment the pool no longer tracks — whose own
296
+ // failures then surface as uncatchable async errors.
297
+ if (self.options && self.options.isPool) {
298
+ return;
299
+ }
259
300
  self._retry_connection_id = setTimeout(function () {
260
301
  self._socket.removeAllListeners();
261
302
  // transiently null while the replacement Connection is built
@@ -264,12 +305,12 @@ class Connection {
264
305
  var ctx = new Connection(host, port, function (err) {
265
306
  ctx.connect(self.options, function (err) {
266
307
  if (err) {
267
- self.db.emit('error', err);
308
+ self._emitError(err);
268
309
  return;
269
310
  }
270
311
  ctx.attach(self.options, function (err) {
271
312
  if (err) {
272
- self.db.emit('error', err);
313
+ self._emitError(err);
273
314
  return;
274
315
  }
275
316
  self.db.emit('reconnect');
@@ -281,8 +322,10 @@ class Connection {
281
322
  });
282
323
  self._socket.on('error', function (e) {
283
324
  self.error = e;
284
- if (self.db)
285
- self.db.emit('error', e);
325
+ // listeners only (_emitError): the affected operations get their
326
+ // errors via the close handler's _rejectPending — an unlistened
327
+ // socket error must not become an uncaught exception
328
+ self._emitError(e);
286
329
  if (callback)
287
330
  callback(e);
288
331
  });
@@ -648,7 +691,9 @@ class Connection {
648
691
  msg.pos = 0;
649
692
  blr.pos = 0;
650
693
  blr.addByte(const_1.default.isc_dpb_version1);
651
- blr.addString(const_1.default.isc_dpb_lc_ctype, options.encoding || 'UTF8', const_1.default.DEFAULT_ENCODING);
694
+ // charset names are sent verbatim — normalize case so encoding:
695
+ // 'utf8'/'win1251' (e.g. from URI query params) is accepted
696
+ blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
652
697
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
653
698
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
654
699
  blr.addByte(const_1.default.isc_dpb_utf8_filename);
@@ -750,8 +795,13 @@ class Connection {
750
795
  var blr = this._blr;
751
796
  blr.pos = 0;
752
797
  blr.addByte(const_1.default.isc_dpb_version1);
753
- blr.addString(const_1.default.isc_dpb_set_db_charset, 'UTF8', const_1.default.DEFAULT_ENCODING);
754
- blr.addString(const_1.default.isc_dpb_lc_ctype, 'UTF8', const_1.default.DEFAULT_ENCODING);
798
+ // honour options.encoding on the CREATE path too — hardcoding UTF8
799
+ // for lc_ctype made attachOrCreate silently ignore the requested
800
+ // connection charset (issue #319: 'Malformed string' for codepage
801
+ // text). The new database's DEFAULT charset follows the connection
802
+ // encoding unless options.defaultCharset overrides it.
803
+ blr.addString(const_1.default.isc_dpb_set_db_charset, String(options.defaultCharset || options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
804
+ blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
755
805
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
756
806
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
757
807
  blr.addByte(const_1.default.isc_dpb_utf8_filename);
@@ -835,7 +885,9 @@ class Connection {
835
885
  }
836
886
  throwClosed(callback) {
837
887
  var err = new Error('Connection is closed.');
838
- this.db.emit('error', err);
888
+ // listeners only: the caller receives the error through its own
889
+ // callback below either way
890
+ this._emitError(err);
839
891
  if (callback)
840
892
  callback(err);
841
893
  return this;
@@ -1027,7 +1079,9 @@ class Connection {
1027
1079
  msg.addInt(transaction.handle);
1028
1080
  msg.addInt(0xFFFF);
1029
1081
  msg.addInt(3); // dialect = 3
1030
- msg.addString(query, const_1.default.DEFAULT_ENCODING);
1082
+ // SQL text travels in the CONNECTION charset (codepage
1083
+ // connections encode via codec — greek/cyrillic literals included)
1084
+ msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
1031
1085
  msg.addBlr(blr);
1032
1086
  msg.addInt(65535); // buffer_length
1033
1087
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
@@ -1075,7 +1129,9 @@ class Connection {
1075
1129
  msg.addInt(transaction.handle);
1076
1130
  msg.addInt(statement.handle);
1077
1131
  msg.addInt(3); // dialect = 3
1078
- msg.addString(query, const_1.default.DEFAULT_ENCODING);
1132
+ // SQL text travels in the CONNECTION charset (codepage
1133
+ // connections encode via codec — greek/cyrillic literals included)
1134
+ msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
1079
1135
  msg.addBlr(blr);
1080
1136
  msg.addInt(65535); // buffer_length
1081
1137
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
@@ -1100,10 +1156,13 @@ class Connection {
1100
1156
  * gets an in-order response (op_batch_cs for exec), so the regular
1101
1157
  * response queue keeps everything in sync.
1102
1158
  *
1103
- * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
1104
- * are not supported yet. The callback receives a completion object:
1105
- * { recordCount, updateCounts, errors: [{recordNumber, error}],
1106
- * errorRecordNumbers, success }.
1159
+ * rows: array of parameter arrays, one per record. BLOB columns accept
1160
+ * Buffers, strings, JSON-able objects or pre-created blob quad ids —
1161
+ * values are uploaded as transaction blobs first (all initiated
1162
+ * back-to-back so they pipeline) and the batch messages reference their
1163
+ * ids. ARRAY columns are not supported. The callback receives a
1164
+ * completion object: { recordCount, updateCounts, errors:
1165
+ * [{recordNumber, error}], errorRecordNumbers, success }.
1107
1166
  */
1108
1167
  executeBatch(transaction, statement, rows, callback, options) {
1109
1168
  options = options || {};
@@ -1135,6 +1194,82 @@ class Connection {
1135
1194
  return;
1136
1195
  }
1137
1196
  }
1197
+ var self = this;
1198
+ // BLOB pre-pass: upload every Buffer/string blob value as a
1199
+ // transaction blob and replace it (in a cloned row) with the quad
1200
+ // id the batch message will carry. All uploads are initiated
1201
+ // back-to-back, so the create/segment/close ops pipeline on the
1202
+ // wire instead of paying a round trip per blob.
1203
+ var blobCols = [];
1204
+ for (var bj = 0; bj < input.length; bj++) {
1205
+ var bt = input[bj].type;
1206
+ if (bt === const_1.default.SQL_BLOB || bt === const_1.default.SQL_QUAD) {
1207
+ blobCols.push(bj);
1208
+ }
1209
+ }
1210
+ // all-NULL blob columns are common — a large row set must not pay
1211
+ // for a full clone when there is nothing to upload or unwrap
1212
+ var needsBlobPass = false;
1213
+ if (blobCols.length) {
1214
+ outer: for (var ri = 0; ri < rows.length; ri++) {
1215
+ for (var ci = 0; ci < blobCols.length; ci++) {
1216
+ var bv = rows[ri][blobCols[ci]];
1217
+ if (bv !== null && bv !== undefined) {
1218
+ needsBlobPass = true;
1219
+ break outer;
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ if (needsBlobPass) {
1225
+ var cloned = rows.map(function (r) { return r.slice(); });
1226
+ var pendingBlobs = 1; // sentinel so zero uploads still settle
1227
+ var blobFailure = null;
1228
+ var settleBlobs = function (err) {
1229
+ if (err && !blobFailure) {
1230
+ blobFailure = err;
1231
+ }
1232
+ if (--pendingBlobs) {
1233
+ return;
1234
+ }
1235
+ if (blobFailure) {
1236
+ (0, callback_1.doError)(blobFailure, callback);
1237
+ return;
1238
+ }
1239
+ self._executeBatchEncoded(transaction, statement, cloned, callback, options);
1240
+ };
1241
+ cloned.forEach(function (row) {
1242
+ blobCols.forEach(function (j) {
1243
+ var v = row[j];
1244
+ if (v === null || v === undefined) {
1245
+ return;
1246
+ }
1247
+ // a pre-created blob id (SQLParamQuad wrapper) passes
1248
+ // through; plain {high, low} objects are deliberately NOT
1249
+ // treated as ids — they are legitimate JSON blob content
1250
+ // and would silently misroute to a bogus blob reference
1251
+ if (v instanceof Xsql.SQLParamQuad) {
1252
+ row[j] = v.value;
1253
+ return;
1254
+ }
1255
+ pendingBlobs++;
1256
+ self.uploadBlob(transaction, v, function (err, oid) {
1257
+ if (!err) {
1258
+ row[j] = oid;
1259
+ }
1260
+ settleBlobs(err);
1261
+ });
1262
+ });
1263
+ });
1264
+ settleBlobs();
1265
+ return;
1266
+ }
1267
+ this._executeBatchEncoded(transaction, statement, rows, callback, options);
1268
+ }
1269
+ /** Encode and send the batch packets (rows are fully materialized:
1270
+ * blob values already replaced by quad ids by executeBatch). */
1271
+ _executeBatchEncoded(transaction, statement, rows, callback, options) {
1272
+ var input = statement.input;
1138
1273
  var built;
1139
1274
  try {
1140
1275
  built = buildBatchEncoders(input, Object.assign({}, this.options, options));
@@ -1302,9 +1437,9 @@ class Connection {
1302
1437
  if (Buffer.isBuffer(value))
1303
1438
  b = value;
1304
1439
  else if (typeof (value) === 'string')
1305
- b = Buffer.from(value, const_1.default.DEFAULT_ENCODING);
1440
+ b = Xsql.encodeConnectionText(self.options, value);
1306
1441
  else if (!isStream)
1307
- b = Buffer.from(JSON.stringify(value), const_1.default.DEFAULT_ENCODING);
1442
+ b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
1308
1443
  // Use configured transfer size or default to 1024
1309
1444
  var chunkSize = self.options.blobChunkSize || 1024;
1310
1445
  if (Buffer.isBuffer(b)) {
@@ -1445,14 +1580,18 @@ class Connection {
1445
1580
  ret[i] = new Xsql.SQLParamDouble(value);
1446
1581
  break;
1447
1582
  case 'string':
1448
- ret[i] = new Xsql.SQLParamString(value);
1583
+ ret[i] = textParam(self.options, value);
1449
1584
  break;
1450
1585
  case 'boolean':
1451
- ret[i] = new Xsql.SQLParamBool(value);
1586
+ // metadata-directed: BOOLEAN targets get a
1587
+ // real blr_bool (issue #122); smallint
1588
+ // targets keep the legacy 0/1 (BOOLEAN
1589
+ // does not convert to numbers)
1590
+ ret[i] = new Xsql.SQLParamBool(value, meta.type === const_1.default.SQL_BOOLEAN);
1452
1591
  break;
1453
1592
  default:
1454
1593
  //throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
1455
- ret[i] = new Xsql.SQLParamString(value.toString());
1594
+ ret[i] = textParam(self.options, value.toString());
1456
1595
  break;
1457
1596
  }
1458
1597
  }
@@ -1683,6 +1822,33 @@ class Connection {
1683
1822
  msg.addInt(65535); // buffer_length
1684
1823
  this._queueEvent(callback);
1685
1824
  }
1825
+ /**
1826
+ * Resolve the pending blobAsText fetches of a decoded row batch
1827
+ * (ret.arrBlob) and write the text back into ret.data. Reads run
1828
+ * sequentially to respect Firebird's per-connection open-blob-handle
1829
+ * limit (issue #387). Used by fetchAll for cursors and by
1830
+ * transaction.execute for op_execute2 singletons (EXECUTE PROCEDURE /
1831
+ * RETURNING — issue #305, whose blobs never resolved before).
1832
+ */
1833
+ resolveTextBlobs(transaction, ret, callback) {
1834
+ const self = this;
1835
+ const fns = (ret && ret.arrBlob) || [];
1836
+ if (!fns.length) {
1837
+ return callback();
1838
+ }
1839
+ const read = (index) => {
1840
+ if (index >= fns.length) {
1841
+ return callback();
1842
+ }
1843
+ fns[index](transaction).then((blob) => {
1844
+ // nestTables === true rows: the value lives in the
1845
+ // per-table sub-object, not on the row itself
1846
+ Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(self.options, blob.meta || {}, parseValueIfJson(blob.value, self.options));
1847
+ read(index + 1);
1848
+ }, callback);
1849
+ };
1850
+ read(0);
1851
+ }
1686
1852
  fetchAll(statement, transaction, callback) {
1687
1853
  const self = this;
1688
1854
  const custom = statement.options || {};
@@ -1695,26 +1861,10 @@ class Connection {
1695
1861
  return;
1696
1862
  }
1697
1863
  if (ret && ret.data && ret.data.length) {
1698
- // Read blobs sequentially instead of in parallel to avoid
1699
- // exceeding Firebird's per-connection open-blob-handle limit,
1700
- // which causes a server-side deadlock when many rows contain
1701
- // BLOBs and blobAsText is true. See issue #387.
1702
- const arrBlobFns = ret.arrBlob || [];
1703
- const readBlobsSequentially = (index, results) => {
1704
- if (index >= arrBlobFns.length) {
1705
- return Promise.resolve(results);
1706
- }
1707
- return arrBlobFns[index](transaction).then((v) => {
1708
- results.push(v);
1709
- return readBlobsSequentially(index + 1, results);
1710
- });
1711
- };
1712
- readBlobsSequentially(0, []).then((arrBlob) => {
1713
- for (let i = 0; i < arrBlob.length; i++) {
1714
- const blob = arrBlob[i];
1715
- // nestTables === true rows: the value lives in the
1716
- // per-table sub-object, not on the row itself
1717
- Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
1864
+ self.resolveTextBlobs(transaction, ret, (blobErr) => {
1865
+ if (blobErr) {
1866
+ callback(blobErr);
1867
+ return;
1718
1868
  }
1719
1869
  doSynchronousLoop(ret.data, (row, _i, next) => {
1720
1870
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -1736,7 +1886,7 @@ class Connection {
1736
1886
  self.fetch(statement, transaction, const_1.default.DEFAULT_FETCHSIZE, loop);
1737
1887
  }
1738
1888
  });
1739
- }).catch(callback);
1889
+ });
1740
1890
  return;
1741
1891
  }
1742
1892
  if (ret && ret.fetched) {
@@ -1794,6 +1944,47 @@ class Connection {
1794
1944
  msg.addBlr(blr);
1795
1945
  this._queueEvent(callback);
1796
1946
  }
1947
+ /**
1948
+ * Create a transaction blob, upload `value` (Buffer, string, or a
1949
+ * JSON-able object) and deliver its quad id. executeBatch's blob
1950
+ * pre-pass uses this: batch messages reference pre-created transaction
1951
+ * blobs (the batch parameter buffer's default BLOB_NONE policy), just
1952
+ * like the classic execute path stores blob params.
1953
+ */
1954
+ uploadBlob(transaction, value, callback) {
1955
+ var self = this;
1956
+ var b;
1957
+ if (Buffer.isBuffer(value)) {
1958
+ b = value;
1959
+ }
1960
+ else if (typeof value === 'string') {
1961
+ b = Xsql.encodeConnectionText(this.options, value);
1962
+ }
1963
+ else {
1964
+ b = Xsql.encodeConnectionText(this.options, JSON.stringify(value));
1965
+ }
1966
+ self.createBlob2(transaction, function (err, blob) {
1967
+ if (err) {
1968
+ return callback(err);
1969
+ }
1970
+ var chunkSize = self.options.blobChunkSize || 1024;
1971
+ // bufferReader's next() drops errors — capture the first segment
1972
+ // failure so a truncated upload is never reported as success
1973
+ var segmentError = null;
1974
+ bufferReader(b, chunkSize, function (part, next) {
1975
+ self.batchSegments(blob, part, function (segErr) {
1976
+ if (segErr && !segmentError) {
1977
+ segmentError = segErr;
1978
+ }
1979
+ next();
1980
+ });
1981
+ }, function () {
1982
+ self.closeBlob(blob, function (closeErr) {
1983
+ callback(segmentError || closeErr, blob.oid);
1984
+ }, false);
1985
+ });
1986
+ });
1987
+ }
1797
1988
  svcattach(options, callback, svc) {
1798
1989
  this._lowercase_keys = options.lowercase_keys || const_1.default.DEFAULT_LOWERCASE_KEYS;
1799
1990
  var database = options.database || options.filename;
@@ -2832,6 +3023,43 @@ function describe(buff, statement) {
2832
3023
  }
2833
3024
  unpackCharSetCollation(statement.input);
2834
3025
  unpackCharSetCollation(statement.output);
3026
+ scaleOutputLengths(statement.output, statement.connection && statement.connection.options);
3027
+ }
3028
+ /**
3029
+ * Widen declared OUTPUT byte lengths for text columns whose charset the
3030
+ * server did NOT transliterate in the describe. Under a UTF8 connection
3031
+ * the server rewrites known charsets (a WIN1251 VARCHAR(15) is described
3032
+ * as UTF8 with length 60) — but NONE (and any untransliterated charset)
3033
+ * keeps its native length, and the engine's fetch-time capacity check
3034
+ * then treats those bytes as connection-charset characters: VARCHAR(15)
3035
+ * NONE declared as 15 bytes only fits floor(15/4) = 3 characters and
3036
+ * longer values fail with "string right truncation" (issue #422).
3037
+ * Declaring charCapacity × connectionWidth bytes makes the check pass;
3038
+ * the width-based trim in the text decoders restores the column's true
3039
+ * character capacity. The native length is preserved for metadata.
3040
+ */
3041
+ function scaleOutputLengths(output, options) {
3042
+ if (!output || !output.length)
3043
+ return;
3044
+ const connWidth = Xsql.getFirebirdCharsetWidth(options && options.encoding);
3045
+ if (connWidth <= 1)
3046
+ return;
3047
+ for (let i = 0; i < output.length; i++) {
3048
+ const p = output[i];
3049
+ if (!p || (p.type !== const_1.default.SQL_TEXT && p.type !== const_1.default.SQL_VARYING))
3050
+ continue;
3051
+ // OCTETS (id 1) is binary and never transliterated
3052
+ if (p.charSetId === undefined || p.charSetId === 1)
3053
+ continue;
3054
+ const colWidth = (0, codepages_1.charsetWidthById)(p.charSetId);
3055
+ if (colWidth >= connWidth)
3056
+ continue;
3057
+ p.nativeLength = p.length;
3058
+ // BLR encodes the length as a 16-bit word — cap the widened value
3059
+ // (a >16KB single-byte column keeps its full byte capacity, it just
3060
+ // can't be over-declared four-fold)
3061
+ p.length = Math.min(Math.floor(p.length / colWidth) * connWidth, Math.floor(0xFFFF / connWidth) * connWidth);
3062
+ }
2835
3063
  }
2836
3064
  /**
2837
3065
  * Batch support: the engine requires every batch message to use EXACTLY the
@@ -2858,7 +3086,7 @@ function buildBatchEncoders(input, options) {
2858
3086
  return String(v);
2859
3087
  };
2860
3088
  var toBytes = function (v, meta, column) {
2861
- var b = Buffer.isBuffer(v) ? v : Buffer.from(toText(v), const_1.default.DEFAULT_ENCODING);
3089
+ var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
2862
3090
  if (b.length > meta.length) {
2863
3091
  throw new Error('Batch value for column ' + column + ' is ' + b.length +
2864
3092
  ' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
@@ -2994,6 +3222,24 @@ function buildBatchEncoders(input, options) {
2994
3222
  align(4);
2995
3223
  offset += 4;
2996
3224
  break;
3225
+ case const_1.default.SQL_BLOB:
3226
+ case const_1.default.SQL_QUAD:
3227
+ // the value is a transaction blob quad id — placed by
3228
+ // executeBatch's uploadBlob pre-pass, or passed by the
3229
+ // caller directly. ISC_QUAD: two longs, align 4.
3230
+ encoders.push((function (col) {
3231
+ return function (msg, v) {
3232
+ if (!v || typeof v.high !== 'number' || typeof v.low !== 'number') {
3233
+ throw new Error('Batch value for BLOB column ' + col +
3234
+ ' must be a Buffer, string, object, or blob quad id');
3235
+ }
3236
+ msg.addInt(v.high);
3237
+ msg.addInt(v.low);
3238
+ };
3239
+ })(column));
3240
+ align(4);
3241
+ offset += 8;
3242
+ break;
2997
3243
  default:
2998
3244
  throw new Error('executeBatch does not support the type of parameter ' + column +
2999
3245
  ' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
@@ -3042,11 +3288,15 @@ function CalcBlr(blr, xsqlda) {
3042
3288
  }
3043
3289
  function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3044
3290
  const infoValue = { row, column, value: '', meta, table };
3291
+ // decode ONCE from the concatenated bytes: per-segment toString() both
3292
+ // ignored the connection charset (#301 — cyrillic mojibake) and could
3293
+ // split a multi-byte UTF-8 character across segment boundaries
3294
+ const chunks = [];
3045
3295
  return (transactionArg) => {
3046
3296
  const cacheKey = `${id.high}:${id.low}`;
3047
3297
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
3048
3298
  const data = statement.connection._inlineBlobs.get(cacheKey);
3049
- infoValue.value = data ? data.toString(const_1.default.DEFAULT_ENCODING) : '';
3299
+ infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
3050
3300
  return Promise.resolve(infoValue);
3051
3301
  }
3052
3302
  const singleTransaction = transactionArg === undefined;
@@ -3085,13 +3335,13 @@ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3085
3335
  }
3086
3336
  if (ret.buffer) {
3087
3337
  const blr = new serialize_1.BlrReader(ret.buffer);
3088
- const data = blr.readSegment();
3089
- infoValue.value += data.toString(const_1.default.DEFAULT_ENCODING);
3338
+ chunks.push(blr.readSegment());
3090
3339
  }
3091
3340
  if (ret.handle !== 2) {
3092
3341
  read();
3093
3342
  return;
3094
3343
  }
3344
+ infoValue.value = Xsql.decodeConnectionText(statement.connection.options, Buffer.concat(chunks));
3095
3345
  statement.connection.closeBlob(blob);
3096
3346
  if (singleTransaction) {
3097
3347
  transaction.commit((err) => {
@@ -52,6 +52,15 @@ declare class Database extends Events.EventEmitter {
52
52
  * blobAsText and jsonAsObject all apply.
53
53
  */
54
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;
55
64
  query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): this;
56
65
  drop(callback?: SimpleCallback): void;
57
66
  /**
@@ -11,6 +11,7 @@ const xsqlvar_1 = require("./xsqlvar");
11
11
  const eventConnection_1 = __importDefault(require("./eventConnection"));
12
12
  const fbEventManager_1 = __importDefault(require("./fbEventManager"));
13
13
  const query_stream_1 = __importDefault(require("./query-stream"));
14
+ const batch_stream_1 = __importDefault(require("./batch-stream"));
14
15
  /***************************************
15
16
  *
16
17
  * Database
@@ -241,12 +242,7 @@ class Database extends events_1.default.EventEmitter {
241
242
  }
242
243
  if (!result.success) {
243
244
  transaction.rollback(function () {
244
- var first = result.errors.length ? result.errors[0] : null;
245
- var batchError = first
246
- ? first.error
247
- : new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
248
- batchError.batchCompletion = result;
249
- (0, callback_1.doError)(batchError, callback);
245
+ (0, callback_1.doError)((0, utils_1.batchResultToError)(result), callback);
250
246
  });
251
247
  return;
252
248
  }
@@ -347,6 +343,17 @@ class Database extends events_1.default.EventEmitter {
347
343
  queryStream(query, params, options) {
348
344
  return (0, query_stream_1.default)(this, query, params, options);
349
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
+ }
350
357
  query(query, params, callback, options = {}) {
351
358
  if (params instanceof Function) {
352
359
  options = callback || {};
@@ -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;
@@ -277,6 +277,16 @@ class XdrWriter {
277
277
  this.buffer.fill(0, this.pos + len, this.pos + alen);
278
278
  this.pos += alen;
279
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
+ }
280
290
  addText(s, encoding) {
281
291
  var len = Buffer.byteLength(s, encoding);
282
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) {
@@ -48,6 +48,12 @@ declare class Transaction {
48
48
  * stream ends — commit or roll back yourself.
49
49
  */
50
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;
51
57
  query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): void;
52
58
  /**
53
59
  * Execute `query` once per row in `rows` using the Firebird 4 batch API