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.
@@ -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"));
@@ -107,31 +108,22 @@ function statementCacheLimit(options) {
107
108
  }
108
109
  return 0;
109
110
  }
110
- const SQL_TYPE_NAMES = {
111
- [const_1.default.SQL_TEXT]: 'TEXT',
112
- [const_1.default.SQL_VARYING]: 'VARYING',
113
- [const_1.default.SQL_SHORT]: 'SHORT',
114
- [const_1.default.SQL_LONG]: 'LONG',
115
- [const_1.default.SQL_FLOAT]: 'FLOAT',
116
- [const_1.default.SQL_DOUBLE]: 'DOUBLE',
117
- [const_1.default.SQL_D_FLOAT]: 'D_FLOAT',
118
- [const_1.default.SQL_TIMESTAMP]: 'TIMESTAMP',
119
- [const_1.default.SQL_BLOB]: 'BLOB',
120
- [const_1.default.SQL_ARRAY]: 'ARRAY',
121
- [const_1.default.SQL_QUAD]: 'QUAD',
122
- [const_1.default.SQL_TYPE_TIME]: 'TIME',
123
- [const_1.default.SQL_TYPE_DATE]: 'DATE',
124
- [const_1.default.SQL_INT64]: 'INT64',
125
- [const_1.default.SQL_INT128]: 'INT128',
126
- [const_1.default.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
127
- [const_1.default.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
128
- [const_1.default.SQL_TIME_TZ]: 'TIME_TZ',
129
- [const_1.default.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
130
- [const_1.default.SQL_DEC16]: 'DEC16',
131
- [const_1.default.SQL_DEC34]: 'DEC34',
132
- [const_1.default.SQL_BOOLEAN]: 'BOOLEAN',
133
- [const_1.default.SQL_NULL]: 'NULL',
134
- };
111
+ // SQL type-code names live in xsqlvar.ts alongside the descriptors
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
+ }
135
127
  /**
136
128
  * Run the user's typeCast hook (options.typeCast) for one column value.
137
129
  * The hook receives the column metadata and a next() returning the value
@@ -145,16 +137,7 @@ function applyTypeCast(options, meta, defaultValue) {
145
137
  if (typeof typeCast !== 'function') {
146
138
  return defaultValue;
147
139
  }
148
- const column = {
149
- type: meta.type,
150
- typeName: SQL_TYPE_NAMES[meta.type] || 'UNKNOWN',
151
- subType: meta.subType,
152
- scale: meta.scale,
153
- length: meta.length,
154
- field: meta.field,
155
- relation: meta.relation,
156
- alias: meta.alias,
157
- };
140
+ const column = Xsql.describeField(meta);
158
141
  // A hook exception must never escape into the row-decode loop: there it
159
142
  // would be mistaken for an incomplete packet and desync the response
160
143
  // queue (the same failure mode as issue #341). Fall back to the default
@@ -272,6 +255,24 @@ class Connection {
272
255
  (0, callback_1.doError)(err, queue[i]);
273
256
  }
274
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
+ }
275
276
  _bind_events(host, port, callback) {
276
277
  var self = this;
277
278
  self._socket.on('close', function () {
@@ -288,6 +289,14 @@ class Connection {
288
289
  return;
289
290
  }
290
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
+ }
291
300
  self._retry_connection_id = setTimeout(function () {
292
301
  self._socket.removeAllListeners();
293
302
  // transiently null while the replacement Connection is built
@@ -296,12 +305,12 @@ class Connection {
296
305
  var ctx = new Connection(host, port, function (err) {
297
306
  ctx.connect(self.options, function (err) {
298
307
  if (err) {
299
- self.db.emit('error', err);
308
+ self._emitError(err);
300
309
  return;
301
310
  }
302
311
  ctx.attach(self.options, function (err) {
303
312
  if (err) {
304
- self.db.emit('error', err);
313
+ self._emitError(err);
305
314
  return;
306
315
  }
307
316
  self.db.emit('reconnect');
@@ -313,8 +322,10 @@ class Connection {
313
322
  });
314
323
  self._socket.on('error', function (e) {
315
324
  self.error = e;
316
- if (self.db)
317
- 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);
318
329
  if (callback)
319
330
  callback(e);
320
331
  });
@@ -396,6 +407,30 @@ class Connection {
396
407
  if (process.env.FIREBIRD_DEBUG) {
397
408
  console.log('[fb-debug] response dispatched: queue remaining=%d pending remaining=%d xdr.pos=%d', self._queue.length, self._pending.length, xdr.pos);
398
409
  }
410
+ // Surface isc_arg_warning entries (parsed since 2.10.0 but
411
+ // dropped here): resolve their message text and emit them on
412
+ // the Database on the next tick, so a listener registered
413
+ // inside this very response's callback (e.g. right after
414
+ // attach) still receives them.
415
+ if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
416
+ const warnings = obj.warnings;
417
+ for (const w of warnings) {
418
+ if (w.message === undefined) {
419
+ w.message = (0, utils_1.lookupMessages)([w]);
420
+ if (!w.message || w.message === 'Unknow error') {
421
+ // codes newer than the bundled firebird.msg:
422
+ // still say something actionable
423
+ w.message = 'Firebird warning ' + w.gdscode +
424
+ (w.params && w.params.length ? ': ' + w.params.join(', ') : '');
425
+ }
426
+ }
427
+ }
428
+ process.nextTick(function () {
429
+ for (const w of warnings) {
430
+ self.db.emit('warning', w);
431
+ }
432
+ });
433
+ }
399
434
  if (obj && obj.status) {
400
435
  obj.message = (0, utils_1.lookupMessages)(obj.status);
401
436
  (0, callback_1.doCallback)(obj, cb);
@@ -656,7 +691,9 @@ class Connection {
656
691
  msg.pos = 0;
657
692
  blr.pos = 0;
658
693
  blr.addByte(const_1.default.isc_dpb_version1);
659
- 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);
660
697
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
661
698
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
662
699
  blr.addByte(const_1.default.isc_dpb_utf8_filename);
@@ -758,8 +795,13 @@ class Connection {
758
795
  var blr = this._blr;
759
796
  blr.pos = 0;
760
797
  blr.addByte(const_1.default.isc_dpb_version1);
761
- blr.addString(const_1.default.isc_dpb_set_db_charset, 'UTF8', const_1.default.DEFAULT_ENCODING);
762
- 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);
763
805
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
764
806
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
765
807
  blr.addByte(const_1.default.isc_dpb_utf8_filename);
@@ -843,7 +885,9 @@ class Connection {
843
885
  }
844
886
  throwClosed(callback) {
845
887
  var err = new Error('Connection is closed.');
846
- 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);
847
891
  if (callback)
848
892
  callback(err);
849
893
  return this;
@@ -1035,7 +1079,9 @@ class Connection {
1035
1079
  msg.addInt(transaction.handle);
1036
1080
  msg.addInt(0xFFFF);
1037
1081
  msg.addInt(3); // dialect = 3
1038
- 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));
1039
1085
  msg.addBlr(blr);
1040
1086
  msg.addInt(65535); // buffer_length
1041
1087
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
@@ -1083,7 +1129,9 @@ class Connection {
1083
1129
  msg.addInt(transaction.handle);
1084
1130
  msg.addInt(statement.handle);
1085
1131
  msg.addInt(3); // dialect = 3
1086
- 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));
1087
1135
  msg.addBlr(blr);
1088
1136
  msg.addInt(65535); // buffer_length
1089
1137
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
@@ -1108,10 +1156,13 @@ class Connection {
1108
1156
  * gets an in-order response (op_batch_cs for exec), so the regular
1109
1157
  * response queue keeps everything in sync.
1110
1158
  *
1111
- * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
1112
- * are not supported yet. The callback receives a completion object:
1113
- * { recordCount, updateCounts, errors: [{recordNumber, error}],
1114
- * 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 }.
1115
1166
  */
1116
1167
  executeBatch(transaction, statement, rows, callback, options) {
1117
1168
  options = options || {};
@@ -1143,6 +1194,82 @@ class Connection {
1143
1194
  return;
1144
1195
  }
1145
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;
1146
1273
  var built;
1147
1274
  try {
1148
1275
  built = buildBatchEncoders(input, Object.assign({}, this.options, options));
@@ -1310,9 +1437,9 @@ class Connection {
1310
1437
  if (Buffer.isBuffer(value))
1311
1438
  b = value;
1312
1439
  else if (typeof (value) === 'string')
1313
- b = Buffer.from(value, const_1.default.DEFAULT_ENCODING);
1440
+ b = Xsql.encodeConnectionText(self.options, value);
1314
1441
  else if (!isStream)
1315
- b = Buffer.from(JSON.stringify(value), const_1.default.DEFAULT_ENCODING);
1442
+ b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
1316
1443
  // Use configured transfer size or default to 1024
1317
1444
  var chunkSize = self.options.blobChunkSize || 1024;
1318
1445
  if (Buffer.isBuffer(b)) {
@@ -1453,14 +1580,18 @@ class Connection {
1453
1580
  ret[i] = new Xsql.SQLParamDouble(value);
1454
1581
  break;
1455
1582
  case 'string':
1456
- ret[i] = new Xsql.SQLParamString(value);
1583
+ ret[i] = textParam(self.options, value);
1457
1584
  break;
1458
1585
  case 'boolean':
1459
- 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);
1460
1591
  break;
1461
1592
  default:
1462
1593
  //throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
1463
- ret[i] = new Xsql.SQLParamString(value.toString());
1594
+ ret[i] = textParam(self.options, value.toString());
1464
1595
  break;
1465
1596
  }
1466
1597
  }
@@ -1670,6 +1801,54 @@ class Connection {
1670
1801
  callback.statement = statement;
1671
1802
  this._queueEvent(callback);
1672
1803
  }
1804
+ /**
1805
+ * Query runtime information about a prepared statement via op_info_sql
1806
+ * (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
1807
+ * response is a plain op_response whose buffer holds the info clusters.
1808
+ */
1809
+ statementInfo(statement, items, callback) {
1810
+ if (this._isClosed)
1811
+ return this.throwClosed(callback);
1812
+ this._pending.push('statementInfo');
1813
+ var msg = this._msg;
1814
+ var blr = this._blr;
1815
+ msg.pos = 0;
1816
+ blr.pos = 0;
1817
+ blr.addBytes(items);
1818
+ msg.addInt(const_1.default.op_info_sql);
1819
+ msg.addInt(statement.handle);
1820
+ msg.addInt(0); // incarnation
1821
+ msg.addBlr(blr);
1822
+ msg.addInt(65535); // buffer_length
1823
+ this._queueEvent(callback);
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
+ }
1673
1852
  fetchAll(statement, transaction, callback) {
1674
1853
  const self = this;
1675
1854
  const custom = statement.options || {};
@@ -1682,26 +1861,10 @@ class Connection {
1682
1861
  return;
1683
1862
  }
1684
1863
  if (ret && ret.data && ret.data.length) {
1685
- // Read blobs sequentially instead of in parallel to avoid
1686
- // exceeding Firebird's per-connection open-blob-handle limit,
1687
- // which causes a server-side deadlock when many rows contain
1688
- // BLOBs and blobAsText is true. See issue #387.
1689
- const arrBlobFns = ret.arrBlob || [];
1690
- const readBlobsSequentially = (index, results) => {
1691
- if (index >= arrBlobFns.length) {
1692
- return Promise.resolve(results);
1693
- }
1694
- return arrBlobFns[index](transaction).then((v) => {
1695
- results.push(v);
1696
- return readBlobsSequentially(index + 1, results);
1697
- });
1698
- };
1699
- readBlobsSequentially(0, []).then((arrBlob) => {
1700
- for (let i = 0; i < arrBlob.length; i++) {
1701
- const blob = arrBlob[i];
1702
- // nestTables === true rows: the value lives in the
1703
- // per-table sub-object, not on the row itself
1704
- 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;
1705
1868
  }
1706
1869
  doSynchronousLoop(ret.data, (row, _i, next) => {
1707
1870
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -1723,7 +1886,7 @@ class Connection {
1723
1886
  self.fetch(statement, transaction, const_1.default.DEFAULT_FETCHSIZE, loop);
1724
1887
  }
1725
1888
  });
1726
- }).catch(callback);
1889
+ });
1727
1890
  return;
1728
1891
  }
1729
1892
  if (ret && ret.fetched) {
@@ -1781,6 +1944,47 @@ class Connection {
1781
1944
  msg.addBlr(blr);
1782
1945
  this._queueEvent(callback);
1783
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
+ }
1784
1988
  svcattach(options, callback, svc) {
1785
1989
  this._lowercase_keys = options.lowercase_keys || const_1.default.DEFAULT_LOWERCASE_KEYS;
1786
1990
  var database = options.database || options.filename;
@@ -2106,7 +2310,8 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2106
2310
  data.frows = data.frows || [];
2107
2311
  if (custom.asObject && !data.fcols) {
2108
2312
  const nest = Xsql.resolveNestTables(custom, cnx.options);
2109
- const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2313
+ const transform = Xsql.resolveKeyTransform(custom, cnx.options);
2314
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
2110
2315
  data.fcols = columnKeys.map((k) => k.key);
2111
2316
  if (nest === true) {
2112
2317
  // computeColumnKeys always sets table when nesting
@@ -2818,6 +3023,43 @@ function describe(buff, statement) {
2818
3023
  }
2819
3024
  unpackCharSetCollation(statement.input);
2820
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
+ }
2821
3063
  }
2822
3064
  /**
2823
3065
  * Batch support: the engine requires every batch message to use EXACTLY the
@@ -2844,7 +3086,7 @@ function buildBatchEncoders(input, options) {
2844
3086
  return String(v);
2845
3087
  };
2846
3088
  var toBytes = function (v, meta, column) {
2847
- 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));
2848
3090
  if (b.length > meta.length) {
2849
3091
  throw new Error('Batch value for column ' + column + ' is ' + b.length +
2850
3092
  ' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
@@ -2980,6 +3222,24 @@ function buildBatchEncoders(input, options) {
2980
3222
  align(4);
2981
3223
  offset += 4;
2982
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;
2983
3243
  default:
2984
3244
  throw new Error('executeBatch does not support the type of parameter ' + column +
2985
3245
  ' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
@@ -3028,11 +3288,15 @@ function CalcBlr(blr, xsqlda) {
3028
3288
  }
3029
3289
  function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3030
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 = [];
3031
3295
  return (transactionArg) => {
3032
3296
  const cacheKey = `${id.high}:${id.low}`;
3033
3297
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
3034
3298
  const data = statement.connection._inlineBlobs.get(cacheKey);
3035
- infoValue.value = data ? data.toString(const_1.default.DEFAULT_ENCODING) : '';
3299
+ infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
3036
3300
  return Promise.resolve(infoValue);
3037
3301
  }
3038
3302
  const singleTransaction = transactionArg === undefined;
@@ -3071,13 +3335,13 @@ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3071
3335
  }
3072
3336
  if (ret.buffer) {
3073
3337
  const blr = new serialize_1.BlrReader(ret.buffer);
3074
- const data = blr.readSegment();
3075
- infoValue.value += data.toString(const_1.default.DEFAULT_ENCODING);
3338
+ chunks.push(blr.readSegment());
3076
3339
  }
3077
3340
  if (ret.handle !== 2) {
3078
3341
  read();
3079
3342
  return;
3080
3343
  }
3344
+ infoValue.value = Xsql.decodeConnectionText(statement.connection.options, Buffer.concat(chunks));
3081
3345
  statement.connection.closeBlob(blob);
3082
3346
  if (singleTransaction) {
3083
3347
  transaction.commit((err) => {
@@ -404,6 +404,10 @@ declare const Const: Readonly<{
404
404
  isc_info_sql_stmt_type: number;
405
405
  isc_info_sql_get_plan: number;
406
406
  isc_info_sql_records: number;
407
+ isc_info_req_select_count: number;
408
+ isc_info_req_insert_count: number;
409
+ isc_info_req_update_count: number;
410
+ isc_info_req_delete_count: number;
407
411
  isc_info_sql_batch_fetch: number;
408
412
  isc_info_sql_relation_alias: number;
409
413
  isc_info_sql_explain_plan: number;
@@ -589,6 +593,7 @@ declare const Const: Readonly<{
589
593
  isc_spb_trc_cfg: number;
590
594
  DESCRIBE: number[];
591
595
  DESCRIBE_WITH_SCHEMA: number[];
596
+ RECORDS_INFO: number[];
592
597
  SUPPORTED_PROTOCOL: number[][];
593
598
  }>;
594
599
  export = Const;