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.
@@ -8,6 +8,7 @@ import * as srp from '../srp';
8
8
  import * as crypt from '../unix-crypt';
9
9
  import Const from './const';
10
10
  import * as Xsql from './xsqlvar';
11
+ import { charsetWidthById } from './codepages';
11
12
  import ServiceManager from './service';
12
13
  import Database from './database';
13
14
  import Statement from './statement';
@@ -75,31 +76,23 @@ function statementCacheLimit(options: InternalOptions): number {
75
76
  return 0;
76
77
  }
77
78
 
78
- const SQL_TYPE_NAMES: Record<number, string> = {
79
- [Const.SQL_TEXT]: 'TEXT',
80
- [Const.SQL_VARYING]: 'VARYING',
81
- [Const.SQL_SHORT]: 'SHORT',
82
- [Const.SQL_LONG]: 'LONG',
83
- [Const.SQL_FLOAT]: 'FLOAT',
84
- [Const.SQL_DOUBLE]: 'DOUBLE',
85
- [Const.SQL_D_FLOAT]: 'D_FLOAT',
86
- [Const.SQL_TIMESTAMP]: 'TIMESTAMP',
87
- [Const.SQL_BLOB]: 'BLOB',
88
- [Const.SQL_ARRAY]: 'ARRAY',
89
- [Const.SQL_QUAD]: 'QUAD',
90
- [Const.SQL_TYPE_TIME]: 'TIME',
91
- [Const.SQL_TYPE_DATE]: 'DATE',
92
- [Const.SQL_INT64]: 'INT64',
93
- [Const.SQL_INT128]: 'INT128',
94
- [Const.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
95
- [Const.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
96
- [Const.SQL_TIME_TZ]: 'TIME_TZ',
97
- [Const.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
98
- [Const.SQL_DEC16]: 'DEC16',
99
- [Const.SQL_DEC34]: 'DEC34',
100
- [Const.SQL_BOOLEAN]: 'BOOLEAN',
101
- [Const.SQL_NULL]: 'NULL',
102
- };
79
+ // SQL type-code names live in xsqlvar.ts alongside the descriptors
80
+ const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
81
+
82
+ /**
83
+ * Build the wire parameter for a text value in the CONNECTION charset:
84
+ * plain SQLParamString for UTF-8 connections, pre-encoded bytes for
85
+ * everything else — SQLParamString hardwires utf8, which corrupted
86
+ * writes on latin1/codepage connections (issues #319/#301).
87
+ */
88
+ function textParam(options: InternalOptions, value: string) {
89
+ const codec = Xsql.resolveTextCodec(options);
90
+ const enc = Xsql.resolveTextEncoding(options);
91
+ if (!codec && enc === 'utf8') {
92
+ return new Xsql.SQLParamString(value);
93
+ }
94
+ return new Xsql.SQLParamBuffer(Xsql.encodeConnectionText(options, value));
95
+ }
103
96
 
104
97
  /**
105
98
  * Run the user's typeCast hook (options.typeCast) for one column value.
@@ -114,16 +107,7 @@ function applyTypeCast(options: InternalOptions, meta: Partial<Xsql.SQLVarBase>,
114
107
  if (typeof typeCast !== 'function') {
115
108
  return defaultValue;
116
109
  }
117
- const column = {
118
- type: meta.type!,
119
- typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
120
- subType: meta.subType,
121
- scale: meta.scale,
122
- length: meta.length,
123
- field: meta.field,
124
- relation: meta.relation,
125
- alias: meta.alias,
126
- };
110
+ const column = Xsql.describeField(meta);
127
111
  // A hook exception must never escape into the row-decode loop: there it
128
112
  // would be mistaken for an incomplete packet and desync the response
129
113
  // queue (the same failure mode as issue #341). Fall back to the default
@@ -295,6 +279,25 @@ class Connection {
295
279
  }
296
280
 
297
281
 
282
+ /**
283
+ * Deliver a connection-level error to 'error' listeners — and ONLY to
284
+ * listeners. Emitting an unlistened 'error' makes Node throw the error
285
+ * object as an uncaught exception; for errors that originate in
286
+ * background contexts (the reconnect timer, socket-level failures whose
287
+ * operations are separately rejected via _rejectPending) that crashes
288
+ * the process — or, under a test runner, fails whatever unrelated test
289
+ * happens to be running. The failing operations themselves always
290
+ * still receive their error through their own callbacks.
291
+ */
292
+ _emitError(err: any) {
293
+ if (this.db && typeof this.db.listenerCount === 'function' && this.db.listenerCount('error') > 0) {
294
+ this.db.emit('error', err);
295
+ } else if (process.env.FIREBIRD_DEBUG) {
296
+ console.warn('[fb-debug] connection error (no error listener):', err && err.message);
297
+ }
298
+ }
299
+
300
+
298
301
  _bind_events(host: string, port: number, callback: SimpleCallback | undefined) {
299
302
 
300
303
  var self = this;
@@ -319,6 +322,15 @@ class Connection {
319
322
 
320
323
  self._rejectPending(lostError);
321
324
 
325
+ // Pooled connections do not self-reconnect: the pool is the
326
+ // recovery authority (dead-connection check on checkout + the
327
+ // reaper), and a background reconnect here would produce a
328
+ // zombie attachment the pool no longer tracks — whose own
329
+ // failures then surface as uncatchable async errors.
330
+ if (self.options && self.options.isPool) {
331
+ return;
332
+ }
333
+
322
334
  self._retry_connection_id = setTimeout(function() {
323
335
  self._socket.removeAllListeners();
324
336
  // transiently null while the replacement Connection is built
@@ -329,14 +341,14 @@ class Connection {
329
341
  ctx.connect(self.options, function(err: any) {
330
342
 
331
343
  if (err) {
332
- self.db.emit('error', err);
344
+ self._emitError(err);
333
345
  return;
334
346
  }
335
347
 
336
348
  ctx.attach(self.options, function(err: any) {
337
349
 
338
350
  if (err) {
339
- self.db.emit('error', err);
351
+ self._emitError(err);
340
352
  return;
341
353
  }
342
354
 
@@ -353,11 +365,13 @@ class Connection {
353
365
  });
354
366
 
355
367
  self._socket.on('error', function(e: any) {
356
-
368
+
357
369
  self.error = e;
358
-
359
- if (self.db)
360
- self.db.emit('error', e)
370
+
371
+ // listeners only (_emitError): the affected operations get their
372
+ // errors via the close handler's _rejectPending — an unlistened
373
+ // socket error must not become an uncaught exception
374
+ self._emitError(e);
361
375
 
362
376
  if (callback)
363
377
  callback(e);
@@ -456,13 +470,38 @@ class Connection {
456
470
  self._queue.length, self._pending.length, xdr.pos);
457
471
  }
458
472
 
473
+ // Surface isc_arg_warning entries (parsed since 2.10.0 but
474
+ // dropped here): resolve their message text and emit them on
475
+ // the Database on the next tick, so a listener registered
476
+ // inside this very response's callback (e.g. right after
477
+ // attach) still receives them.
478
+ if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
479
+ const warnings = obj.warnings;
480
+ for (const w of warnings) {
481
+ if (w.message === undefined) {
482
+ w.message = lookupMessages([w]);
483
+ if (!w.message || w.message === 'Unknow error') {
484
+ // codes newer than the bundled firebird.msg:
485
+ // still say something actionable
486
+ w.message = 'Firebird warning ' + w.gdscode +
487
+ (w.params && w.params.length ? ': ' + w.params.join(', ') : '');
488
+ }
489
+ }
490
+ }
491
+ process.nextTick(function () {
492
+ for (const w of warnings) {
493
+ self.db.emit('warning', w);
494
+ }
495
+ });
496
+ }
497
+
459
498
  if (obj && obj.status) {
460
499
  obj.message = lookupMessages(obj.status);
461
500
  doCallback(obj, cb);
462
501
  } else {
463
502
  doCallback(obj, cb);
464
503
  }
465
-
504
+
466
505
  });
467
506
 
468
507
  if (xdr.pos === 0) {
@@ -774,7 +813,9 @@ class Connection {
774
813
  blr.pos = 0;
775
814
 
776
815
  blr.addByte(Const.isc_dpb_version1);
777
- blr.addString(Const.isc_dpb_lc_ctype, options.encoding || 'UTF8', Const.DEFAULT_ENCODING);
816
+ // charset names are sent verbatim — normalize case so encoding:
817
+ // 'utf8'/'win1251' (e.g. from URI query params) is accepted
818
+ blr.addString(Const.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
778
819
 
779
820
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
780
821
  if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
@@ -902,8 +943,15 @@ class Connection {
902
943
 
903
944
  blr.pos = 0;
904
945
  blr.addByte(Const.isc_dpb_version1);
905
- blr.addString(Const.isc_dpb_set_db_charset, 'UTF8', Const.DEFAULT_ENCODING);
906
- blr.addString(Const.isc_dpb_lc_ctype, 'UTF8', Const.DEFAULT_ENCODING);
946
+ // honour options.encoding on the CREATE path too — hardcoding UTF8
947
+ // for lc_ctype made attachOrCreate silently ignore the requested
948
+ // connection charset (issue #319: 'Malformed string' for codepage
949
+ // text). The new database's DEFAULT charset follows the connection
950
+ // encoding unless options.defaultCharset overrides it.
951
+ blr.addString(Const.isc_dpb_set_db_charset,
952
+ String(options.defaultCharset || options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
953
+ blr.addString(Const.isc_dpb_lc_ctype,
954
+ String(options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
907
955
 
908
956
  // For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
909
957
  if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
@@ -1011,7 +1059,9 @@ class Connection {
1011
1059
 
1012
1060
  throwClosed(callback: ((err: Error, ...args: any[]) => void) | undefined) {
1013
1061
  var err = new Error('Connection is closed.');
1014
- this.db.emit('error', err);
1062
+ // listeners only: the caller receives the error through its own
1063
+ // callback below either way
1064
+ this._emitError(err);
1015
1065
  if (callback)
1016
1066
  callback(err);
1017
1067
  return this;
@@ -1261,7 +1311,9 @@ class Connection {
1261
1311
  msg.addInt(transaction.handle);
1262
1312
  msg.addInt(0xFFFF);
1263
1313
  msg.addInt(3); // dialect = 3
1264
- msg.addString(query, Const.DEFAULT_ENCODING);
1314
+ // SQL text travels in the CONNECTION charset (codepage
1315
+ // connections encode via codec — greek/cyrillic literals included)
1316
+ msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
1265
1317
  msg.addBlr(blr);
1266
1318
  msg.addInt(65535); // buffer_length
1267
1319
  if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
@@ -1323,7 +1375,9 @@ class Connection {
1323
1375
  msg.addInt(transaction.handle);
1324
1376
  msg.addInt(statement.handle);
1325
1377
  msg.addInt(3); // dialect = 3
1326
- msg.addString(query, Const.DEFAULT_ENCODING);
1378
+ // SQL text travels in the CONNECTION charset (codepage
1379
+ // connections encode via codec — greek/cyrillic literals included)
1380
+ msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
1327
1381
  msg.addBlr(blr);
1328
1382
  msg.addInt(65535); // buffer_length
1329
1383
  if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
@@ -1355,10 +1409,13 @@ class Connection {
1355
1409
  * gets an in-order response (op_batch_cs for exec), so the regular
1356
1410
  * response queue keeps everything in sync.
1357
1411
  *
1358
- * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
1359
- * are not supported yet. The callback receives a completion object:
1360
- * { recordCount, updateCounts, errors: [{recordNumber, error}],
1361
- * errorRecordNumbers, success }.
1412
+ * rows: array of parameter arrays, one per record. BLOB columns accept
1413
+ * Buffers, strings, JSON-able objects or pre-created blob quad ids
1414
+ * values are uploaded as transaction blobs first (all initiated
1415
+ * back-to-back so they pipeline) and the batch messages reference their
1416
+ * ids. ARRAY columns are not supported. The callback receives a
1417
+ * completion object: { recordCount, updateCounts, errors:
1418
+ * [{recordNumber, error}], errorRecordNumbers, success }.
1362
1419
  */
1363
1420
  executeBatch(transaction: Transaction, statement: Statement, rows: QueryParams[], callback: BatchCb | undefined, options?: BatchOptions) {
1364
1421
  options = options || {};
@@ -1396,6 +1453,90 @@ class Connection {
1396
1453
  }
1397
1454
  }
1398
1455
 
1456
+ var self = this;
1457
+
1458
+ // BLOB pre-pass: upload every Buffer/string blob value as a
1459
+ // transaction blob and replace it (in a cloned row) with the quad
1460
+ // id the batch message will carry. All uploads are initiated
1461
+ // back-to-back, so the create/segment/close ops pipeline on the
1462
+ // wire instead of paying a round trip per blob.
1463
+ var blobCols: number[] = [];
1464
+ for (var bj = 0; bj < input.length; bj++) {
1465
+ var bt = input[bj].type;
1466
+ if (bt === Const.SQL_BLOB || bt === Const.SQL_QUAD) {
1467
+ blobCols.push(bj);
1468
+ }
1469
+ }
1470
+
1471
+ // all-NULL blob columns are common — a large row set must not pay
1472
+ // for a full clone when there is nothing to upload or unwrap
1473
+ var needsBlobPass = false;
1474
+ if (blobCols.length) {
1475
+ outer:
1476
+ for (var ri = 0; ri < rows.length; ri++) {
1477
+ for (var ci = 0; ci < blobCols.length; ci++) {
1478
+ var bv = (rows[ri] as any[])[blobCols[ci]];
1479
+ if (bv !== null && bv !== undefined) {
1480
+ needsBlobPass = true;
1481
+ break outer;
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+
1487
+ if (needsBlobPass) {
1488
+ var cloned: any[][] = rows.map(function(r) { return (r as any[]).slice(); });
1489
+ var pendingBlobs = 1; // sentinel so zero uploads still settle
1490
+ var blobFailure: any = null;
1491
+ var settleBlobs = function(err?: any) {
1492
+ if (err && !blobFailure) {
1493
+ blobFailure = err;
1494
+ }
1495
+ if (--pendingBlobs) {
1496
+ return;
1497
+ }
1498
+ if (blobFailure) {
1499
+ doError(blobFailure, callback);
1500
+ return;
1501
+ }
1502
+ self._executeBatchEncoded(transaction, statement, cloned, callback, options);
1503
+ };
1504
+
1505
+ cloned.forEach(function(row) {
1506
+ blobCols.forEach(function(j) {
1507
+ var v = row[j];
1508
+ if (v === null || v === undefined) {
1509
+ return;
1510
+ }
1511
+ // a pre-created blob id (SQLParamQuad wrapper) passes
1512
+ // through; plain {high, low} objects are deliberately NOT
1513
+ // treated as ids — they are legitimate JSON blob content
1514
+ // and would silently misroute to a bogus blob reference
1515
+ if (v instanceof Xsql.SQLParamQuad) {
1516
+ row[j] = v.value;
1517
+ return;
1518
+ }
1519
+ pendingBlobs++;
1520
+ self.uploadBlob(transaction, v, function(err: any, oid: any) {
1521
+ if (!err) {
1522
+ row[j] = oid;
1523
+ }
1524
+ settleBlobs(err);
1525
+ });
1526
+ });
1527
+ });
1528
+ settleBlobs();
1529
+ return;
1530
+ }
1531
+
1532
+ this._executeBatchEncoded(transaction, statement, rows as any[][], callback, options);
1533
+ }
1534
+
1535
+
1536
+ /** Encode and send the batch packets (rows are fully materialized:
1537
+ * blob values already replaced by quad ids by executeBatch). */
1538
+ _executeBatchEncoded(transaction: Transaction, statement: Statement, rows: any[][], callback: BatchCb | undefined, options: BatchOptions) {
1539
+ var input = statement.input;
1399
1540
  var built;
1400
1541
  try {
1401
1542
  built = buildBatchEncoders(input, Object.assign({}, this.options, options));
@@ -1591,9 +1732,9 @@ class Connection {
1591
1732
  if (Buffer.isBuffer(value))
1592
1733
  b = value;
1593
1734
  else if (typeof(value) === 'string')
1594
- b = Buffer.from(value, Const.DEFAULT_ENCODING);
1735
+ b = Xsql.encodeConnectionText(self.options, value);
1595
1736
  else if (!isStream)
1596
- b = Buffer.from(JSON.stringify(value), Const.DEFAULT_ENCODING);
1737
+ b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
1597
1738
 
1598
1739
  // Use configured transfer size or default to 1024
1599
1740
  var chunkSize = self.options.blobChunkSize || 1024;
@@ -1744,14 +1885,18 @@ class Connection {
1744
1885
  ret[i] = new Xsql.SQLParamDouble(value);
1745
1886
  break;
1746
1887
  case 'string':
1747
- ret[i] = new Xsql.SQLParamString(value);
1888
+ ret[i] = textParam(self.options, value);
1748
1889
  break;
1749
1890
  case 'boolean':
1750
- ret[i] = new Xsql.SQLParamBool(value);
1891
+ // metadata-directed: BOOLEAN targets get a
1892
+ // real blr_bool (issue #122); smallint
1893
+ // targets keep the legacy 0/1 (BOOLEAN
1894
+ // does not convert to numbers)
1895
+ ret[i] = new Xsql.SQLParamBool(value, meta.type === Const.SQL_BOOLEAN);
1751
1896
  break;
1752
1897
  default:
1753
1898
  //throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
1754
- ret[i] = new Xsql.SQLParamString(value.toString());
1899
+ ret[i] = textParam(self.options, value.toString());
1755
1900
  break;
1756
1901
  }
1757
1902
  }
@@ -1984,6 +2129,65 @@ class Connection {
1984
2129
  }
1985
2130
 
1986
2131
 
2132
+ /**
2133
+ * Query runtime information about a prepared statement via op_info_sql
2134
+ * (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
2135
+ * response is a plain op_response whose buffer holds the info clusters.
2136
+ */
2137
+ statementInfo(statement: Statement, items: number[], callback?: QueueCallback) {
2138
+ if (this._isClosed)
2139
+ return this.throwClosed(callback);
2140
+
2141
+ this._pending.push('statementInfo');
2142
+
2143
+ var msg = this._msg;
2144
+ var blr = this._blr;
2145
+ msg.pos = 0;
2146
+ blr.pos = 0;
2147
+
2148
+ blr.addBytes(items);
2149
+
2150
+ msg.addInt(Const.op_info_sql);
2151
+ msg.addInt(statement.handle);
2152
+ msg.addInt(0); // incarnation
2153
+ msg.addBlr(blr);
2154
+ msg.addInt(65535); // buffer_length
2155
+
2156
+ this._queueEvent(callback);
2157
+ }
2158
+
2159
+
2160
+ /**
2161
+ * Resolve the pending blobAsText fetches of a decoded row batch
2162
+ * (ret.arrBlob) and write the text back into ret.data. Reads run
2163
+ * sequentially to respect Firebird's per-connection open-blob-handle
2164
+ * limit (issue #387). Used by fetchAll for cursors and by
2165
+ * transaction.execute for op_execute2 singletons (EXECUTE PROCEDURE /
2166
+ * RETURNING — issue #305, whose blobs never resolved before).
2167
+ */
2168
+ resolveTextBlobs(transaction: Transaction, ret: any, callback: (err?: any) => void) {
2169
+ const self = this;
2170
+ const fns = (ret && ret.arrBlob) || [];
2171
+ if (!fns.length) {
2172
+ return callback();
2173
+ }
2174
+ const read = (index: number) => {
2175
+ if (index >= fns.length) {
2176
+ return callback();
2177
+ }
2178
+ fns[index](transaction).then((blob: any) => {
2179
+ // nestTables === true rows: the value lives in the
2180
+ // per-table sub-object, not on the row itself
2181
+ Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
2182
+ self.options, blob.meta || {},
2183
+ parseValueIfJson(blob.value, self.options));
2184
+ read(index + 1);
2185
+ }, callback);
2186
+ };
2187
+ read(0);
2188
+ }
2189
+
2190
+
1987
2191
  fetchAll(statement: Statement, transaction: Transaction, callback: Callback<any[]>) {
1988
2192
  const self = this;
1989
2193
  const custom = statement.options || {};
@@ -1997,29 +2201,10 @@ class Connection {
1997
2201
  }
1998
2202
 
1999
2203
  if (ret && ret.data && ret.data.length) {
2000
- // Read blobs sequentially instead of in parallel to avoid
2001
- // exceeding Firebird's per-connection open-blob-handle limit,
2002
- // which causes a server-side deadlock when many rows contain
2003
- // BLOBs and blobAsText is true. See issue #387.
2004
- const arrBlobFns = ret.arrBlob || [];
2005
- const readBlobsSequentially = (index: any, results: any) => {
2006
- if (index >= arrBlobFns.length) {
2007
- return Promise.resolve(results);
2008
- }
2009
- return arrBlobFns[index](transaction).then((v: any) => {
2010
- results.push(v);
2011
- return readBlobsSequentially(index + 1, results);
2012
- });
2013
- };
2014
-
2015
- readBlobsSequentially(0, []).then((arrBlob: any) => {
2016
- for (let i = 0; i < arrBlob.length; i++) {
2017
- const blob = arrBlob[i];
2018
- // nestTables === true rows: the value lives in the
2019
- // per-table sub-object, not on the row itself
2020
- Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
2021
- statement.connection.options, blob.meta || {},
2022
- parseValueIfJson(blob.value, statement.connection.options));
2204
+ self.resolveTextBlobs(transaction, ret, (blobErr?: any) => {
2205
+ if (blobErr) {
2206
+ callback(blobErr);
2207
+ return;
2023
2208
  }
2024
2209
 
2025
2210
  doSynchronousLoop(ret.data, (row, _i, next) => {
@@ -2041,7 +2226,7 @@ class Connection {
2041
2226
  self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
2042
2227
  }
2043
2228
  });
2044
- }).catch(callback);
2229
+ });
2045
2230
  return;
2046
2231
  }
2047
2232
 
@@ -2113,6 +2298,48 @@ class Connection {
2113
2298
  }
2114
2299
 
2115
2300
 
2301
+ /**
2302
+ * Create a transaction blob, upload `value` (Buffer, string, or a
2303
+ * JSON-able object) and deliver its quad id. executeBatch's blob
2304
+ * pre-pass uses this: batch messages reference pre-created transaction
2305
+ * blobs (the batch parameter buffer's default BLOB_NONE policy), just
2306
+ * like the classic execute path stores blob params.
2307
+ */
2308
+ uploadBlob(transaction: Transaction, value: any, callback: (err: any, oid?: any) => void) {
2309
+ var self = this;
2310
+ var b: Buffer;
2311
+ if (Buffer.isBuffer(value)) {
2312
+ b = value;
2313
+ } else if (typeof value === 'string') {
2314
+ b = Xsql.encodeConnectionText(this.options, value);
2315
+ } else {
2316
+ b = Xsql.encodeConnectionText(this.options, JSON.stringify(value));
2317
+ }
2318
+
2319
+ self.createBlob2(transaction, function(err: any, blob: any) {
2320
+ if (err) {
2321
+ return callback(err);
2322
+ }
2323
+ var chunkSize = self.options.blobChunkSize || 1024;
2324
+ // bufferReader's next() drops errors — capture the first segment
2325
+ // failure so a truncated upload is never reported as success
2326
+ var segmentError: any = null;
2327
+ bufferReader(b, chunkSize, function(part, next) {
2328
+ self.batchSegments(blob, part, function(segErr: any) {
2329
+ if (segErr && !segmentError) {
2330
+ segmentError = segErr;
2331
+ }
2332
+ next();
2333
+ } as any);
2334
+ }, function() {
2335
+ self.closeBlob(blob, function(closeErr: any) {
2336
+ callback(segmentError || closeErr, blob.oid);
2337
+ } as any, false);
2338
+ });
2339
+ });
2340
+ }
2341
+
2342
+
2116
2343
  svcattach(options: InternalOptions, callback?: Callback<ServiceManager>, svc?: ServiceManager) {
2117
2344
  this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
2118
2345
  var database = options.database || options.filename;
@@ -2494,7 +2721,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2494
2721
 
2495
2722
  if (custom.asObject && !data.fcols) {
2496
2723
  const nest = Xsql.resolveNestTables(custom, cnx.options);
2497
- const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2724
+ const transform = Xsql.resolveKeyTransform(custom, cnx.options);
2725
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
2498
2726
  data.fcols = columnKeys.map((k) => k.key);
2499
2727
  if (nest === true) {
2500
2728
  // computeColumnKeys always sets table when nesting
@@ -3266,6 +3494,43 @@ function describe(buff: Buffer, statement: Statement) {
3266
3494
  }
3267
3495
  unpackCharSetCollation(statement.input);
3268
3496
  unpackCharSetCollation(statement.output);
3497
+ scaleOutputLengths(statement.output, statement.connection && statement.connection.options);
3498
+ }
3499
+
3500
+ /**
3501
+ * Widen declared OUTPUT byte lengths for text columns whose charset the
3502
+ * server did NOT transliterate in the describe. Under a UTF8 connection
3503
+ * the server rewrites known charsets (a WIN1251 VARCHAR(15) is described
3504
+ * as UTF8 with length 60) — but NONE (and any untransliterated charset)
3505
+ * keeps its native length, and the engine's fetch-time capacity check
3506
+ * then treats those bytes as connection-charset characters: VARCHAR(15)
3507
+ * NONE declared as 15 bytes only fits floor(15/4) = 3 characters and
3508
+ * longer values fail with "string right truncation" (issue #422).
3509
+ * Declaring charCapacity × connectionWidth bytes makes the check pass;
3510
+ * the width-based trim in the text decoders restores the column's true
3511
+ * character capacity. The native length is preserved for metadata.
3512
+ */
3513
+ function scaleOutputLengths(output: any[], options: any) {
3514
+ if (!output || !output.length) return;
3515
+ const connWidth = Xsql.getFirebirdCharsetWidth(options && options.encoding);
3516
+ if (connWidth <= 1) return;
3517
+
3518
+ for (let i = 0; i < output.length; i++) {
3519
+ const p = output[i];
3520
+ if (!p || (p.type !== Const.SQL_TEXT && p.type !== Const.SQL_VARYING)) continue;
3521
+ // OCTETS (id 1) is binary and never transliterated
3522
+ if (p.charSetId === undefined || p.charSetId === 1) continue;
3523
+ const colWidth = charsetWidthById(p.charSetId);
3524
+ if (colWidth >= connWidth) continue;
3525
+
3526
+ p.nativeLength = p.length;
3527
+ // BLR encodes the length as a 16-bit word — cap the widened value
3528
+ // (a >16KB single-byte column keeps its full byte capacity, it just
3529
+ // can't be over-declared four-fold)
3530
+ p.length = Math.min(
3531
+ Math.floor(p.length / colWidth) * connWidth,
3532
+ Math.floor(0xFFFF / connWidth) * connWidth);
3533
+ }
3269
3534
  }
3270
3535
 
3271
3536
  /**
@@ -3291,7 +3556,7 @@ function buildBatchEncoders(input: any[], options: any) {
3291
3556
  return String(v);
3292
3557
  };
3293
3558
  var toBytes = function(v: any, meta: any, column: number): Buffer {
3294
- var b = Buffer.isBuffer(v) ? v : Buffer.from(toText(v), Const.DEFAULT_ENCODING);
3559
+ var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
3295
3560
  if (b.length > meta.length) {
3296
3561
  throw new Error('Batch value for column ' + column + ' is ' + b.length +
3297
3562
  ' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
@@ -3429,6 +3694,24 @@ function buildBatchEncoders(input: any[], options: any) {
3429
3694
  align(4); offset += 4;
3430
3695
  break;
3431
3696
 
3697
+ case Const.SQL_BLOB:
3698
+ case Const.SQL_QUAD:
3699
+ // the value is a transaction blob quad id — placed by
3700
+ // executeBatch's uploadBlob pre-pass, or passed by the
3701
+ // caller directly. ISC_QUAD: two longs, align 4.
3702
+ encoders.push((function(col) {
3703
+ return function(msg: any, v: any) {
3704
+ if (!v || typeof v.high !== 'number' || typeof v.low !== 'number') {
3705
+ throw new Error('Batch value for BLOB column ' + col +
3706
+ ' must be a Buffer, string, object, or blob quad id');
3707
+ }
3708
+ msg.addInt(v.high);
3709
+ msg.addInt(v.low);
3710
+ };
3711
+ })(column));
3712
+ align(4); offset += 8;
3713
+ break;
3714
+
3432
3715
  default:
3433
3716
  throw new Error('executeBatch does not support the type of parameter ' + column +
3434
3717
  ' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
@@ -3486,12 +3769,16 @@ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
3486
3769
 
3487
3770
  function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string) {
3488
3771
  const infoValue = { row, column, value: '', meta, table };
3772
+ // decode ONCE from the concatenated bytes: per-segment toString() both
3773
+ // ignored the connection charset (#301 — cyrillic mojibake) and could
3774
+ // split a multi-byte UTF-8 character across segment boundaries
3775
+ const chunks: Buffer[] = [];
3489
3776
 
3490
3777
  return (transactionArg: any) => {
3491
3778
  const cacheKey = `${id.high}:${id.low}`;
3492
3779
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
3493
3780
  const data = statement.connection._inlineBlobs.get(cacheKey);
3494
- infoValue.value = data ? data.toString(Const.DEFAULT_ENCODING) : '';
3781
+ infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
3495
3782
  return Promise.resolve(infoValue);
3496
3783
  }
3497
3784
 
@@ -3535,8 +3822,7 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
3535
3822
 
3536
3823
  if (ret.buffer) {
3537
3824
  const blr = new BlrReader(ret.buffer);
3538
- const data = blr.readSegment();
3539
- infoValue.value += data.toString(Const.DEFAULT_ENCODING);
3825
+ chunks.push(blr.readSegment());
3540
3826
  }
3541
3827
 
3542
3828
  if (ret.handle !== 2) {
@@ -3544,6 +3830,8 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
3544
3830
  return;
3545
3831
  }
3546
3832
 
3833
+ infoValue.value = Xsql.decodeConnectionText(
3834
+ statement.connection.options, Buffer.concat(chunks));
3547
3835
  statement.connection.closeBlob(blob);
3548
3836
  if (singleTransaction) {
3549
3837
  transaction.commit((err: any) => {