node-firebird 2.0.2 → 2.3.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.
@@ -1,7 +1,6 @@
1
1
  const Events = require('events');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const BigInt = require('big-integer');
5
4
 
6
5
  const {XdrWriter, BlrWriter, XdrReader, BitSet, BlrReader} = require('./serialize');
7
6
  const {doCallback, doError} = require('../callback');
@@ -140,6 +139,7 @@ class Connection {
140
139
 
141
140
  self._socket.on('data', function (data) {
142
141
  var xdr;
142
+ var hadSavedBuffer = Boolean(self._xdr);
143
143
 
144
144
  if (!self._xdr) {
145
145
  xdr = new XdrReader(data);
@@ -147,6 +147,11 @@ class Connection {
147
147
  xdr = new XdrReader(Buffer.concat([self._xdr.buffer, data], self._xdr.buffer.length + data.length));
148
148
  delete (self._xdr);
149
149
  }
150
+
151
+ if (process.env.FIREBIRD_DEBUG) {
152
+ console.log('[fb-debug] data event: bytes=%d queue=%d pending=%d xdr.pos=%d savedBuf=%s',
153
+ xdr.buffer.length, self._queue.length, self._pending.length, xdr.pos, hadSavedBuffer);
154
+ }
150
155
 
151
156
  while (xdr.pos < xdr.buffer.length) {
152
157
  var cb = self._queue[0], pos = xdr.pos;
@@ -154,12 +159,30 @@ class Connection {
154
159
  decodeResponse(xdr, cb, self, self._lowercase_keys, function (err, obj) {
155
160
 
156
161
  if (err) {
157
- xdr.buffer = xdr.buffer.slice(pos);
158
- xdr.pos = 0;
159
- self._xdr = xdr;
160
-
161
- if (self.accept.protocolMinimumType === Const.ptype_lazy_send && self._queue.length > 0) {
162
- self._queue[0].lazy_count = 2;
162
+ if (err instanceof RangeError) {
163
+ // Genuinely incomplete packet – buffer the remaining bytes
164
+ // and wait for the next 'data' event to reassemble.
165
+ xdr.buffer = xdr.buffer.slice(pos);
166
+ xdr.pos = 0;
167
+ self._xdr = xdr;
168
+
169
+ if (process.env.FIREBIRD_DEBUG) {
170
+ console.log('[fb-debug] incomplete packet: saved %d bytes at pos=%d queue=%d',
171
+ xdr.buffer.length, pos, self._queue.length);
172
+ }
173
+
174
+ if (self.accept && self.accept.protocolMinimumType === Const.ptype_lazy_send && self._queue.length > 0) {
175
+ self._queue[0].lazy_count = 2;
176
+ }
177
+ } else {
178
+ // Any other error (truly unknown opcode not handled above).
179
+ // Save the buffer so it can be retried, but log a warning.
180
+ if (process.env.FIREBIRD_DEBUG) {
181
+ console.warn(`[fb-debug] unhandled protocol error: ${err.message} pos=${pos} bytes=${xdr.buffer.length} queue=${self._queue.length}`);
182
+ }
183
+ xdr.buffer = xdr.buffer.slice(pos);
184
+ xdr.pos = 0;
185
+ self._xdr = xdr;
163
186
  }
164
187
  return;
165
188
  }
@@ -168,10 +191,27 @@ class Connection {
168
191
  if (xdr.r) {
169
192
  delete (xdr.r);
170
193
  }
194
+
195
+ // op_event / op_response_piggyback received on the main connection:
196
+ // data has been consumed by decodeResponse but it does not belong to
197
+ // any queued request – do NOT shift the queue or invoke any pending
198
+ // callback.
199
+ if (obj && obj._isOpEvent) {
200
+ if (process.env.FIREBIRD_DEBUG) {
201
+ console.log('[fb-debug] async opcode consumed (ignored): queue=%d xdr.pos=%d remaining=%d',
202
+ self._queue.length, xdr.pos, xdr.buffer.length - xdr.pos);
203
+ }
204
+ return;
205
+ }
171
206
 
172
207
  self._queue.shift();
173
208
  self._pending.shift();
174
209
 
210
+ if (process.env.FIREBIRD_DEBUG) {
211
+ console.log('[fb-debug] response dispatched: queue remaining=%d pending remaining=%d xdr.pos=%d',
212
+ self._queue.length, self._pending.length, xdr.pos);
213
+ }
214
+
175
215
  if (obj && obj.status) {
176
216
  obj.message = lookupMessages(obj.status);
177
217
  doCallback(obj, cb);
@@ -272,6 +312,7 @@ class Connection {
272
312
  var blr = this._blr;
273
313
 
274
314
  this._pending.push('connect');
315
+ this._authStartTime = Date.now();
275
316
 
276
317
  msg.pos = 0;
277
318
  blr.pos = 0;
@@ -282,7 +323,11 @@ class Connection {
282
323
 
283
324
  var specificData = '';
284
325
  if (Const.AUTH_PLUGIN_SRP_LIST.indexOf(pluginName) > -1) {
326
+ const _t0 = Date.now();
285
327
  this.clientKeys = srp.clientSeed();
328
+ if (process.env.FIREBIRD_DEBUG) {
329
+ console.log('[fb-debug] srp.clientSeed: %dms', Date.now() - _t0);
330
+ }
286
331
  specificData = this.clientKeys.public.toString(16);
287
332
  blr.addMultiblockPart(Const.CNCT_specific_data, specificData, Const.DEFAULT_ENCODING);
288
333
  } else if (pluginName === Const.AUTH_PLUGIN_LEGACY) {
@@ -335,7 +380,7 @@ class Connection {
335
380
  // Wire encryption: send op_crypt if SRP session key is available
336
381
  if (ret.sessionKey && ret.protocolVersion >= Const.PROTOCOL_VERSION13 &&
337
382
  options.wireCrypt !== Const.WIRE_CRYPT_DISABLE) {
338
- var keyBuf = Buffer.from(srp.hexPad(ret.sessionKey.toString(16)), 'hex');
383
+ var keyBuf = Buffer.from(ret.sessionKey.toString(16).padStart(40, '0'), 'hex');
339
384
  self.sendOpCrypt('Arc4');
340
385
  self._socket.enableEncryption(keyBuf);
341
386
  self._pending.push('crypt');
@@ -354,6 +399,9 @@ class Connection {
354
399
  callback(undefined, ret);
355
400
  }
356
401
 
402
+ if (process.env.FIREBIRD_DEBUG) {
403
+ console.log('[fb-debug] auth: op_connect sent plugin=%s t=%dms', pluginName, Date.now() - this._authStartTime);
404
+ }
357
405
  this._queueEvent(cb);
358
406
  }
359
407
 
@@ -408,6 +456,10 @@ class Connection {
408
456
  if (this.accept.authData) {
409
457
  blr.addString(Const.isc_dpb_specific_auth_data, this.accept.authData, Const.DEFAULT_ENCODING);
410
458
  }
459
+
460
+ if (options.sessionTimeZone) {
461
+ blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
462
+ }
411
463
 
412
464
  msg.addInt(Const.op_attach);
413
465
  msg.addInt(0); // Database Object ID
@@ -423,6 +475,8 @@ class Connection {
423
475
  self.dbhandle = ret.handle;
424
476
  if (callback)
425
477
  callback(undefined, ret);
478
+ if (!db)
479
+ ret.emit('attach', ret);
426
480
  }
427
481
 
428
482
  // For reconnect
@@ -508,6 +562,10 @@ class Connection {
508
562
  if (this.accept.authData) {
509
563
  blr.addString(Const.isc_dpb_specific_auth_data, this.accept.authData, Const.DEFAULT_ENCODING);
510
564
  }
565
+
566
+ if (options.sessionTimeZone) {
567
+ blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
568
+ }
511
569
 
512
570
  blr.addNumeric(Const.isc_dpb_sql_dialect, 3);
513
571
  blr.addNumeric(Const.isc_dpb_force_write, 1);
@@ -527,14 +585,12 @@ class Connection {
527
585
 
528
586
  if (ret)
529
587
  self.dbhandle = ret.handle;
530
-
531
- setImmediate(function() {
532
- if (self.db)
533
- self.db.emit('attach', ret);
534
- });
535
-
588
+
536
589
  if (callback)
537
590
  callback(err, ret);
591
+
592
+ if (!err && ret)
593
+ ret.emit('attach', ret);
538
594
  }
539
595
 
540
596
  cb.response = new Database(this);
@@ -910,17 +966,10 @@ class Connection {
910
966
  }
911
967
 
912
968
  function PrepareParams(params, input, callback) {
913
-
969
+
914
970
  var value, meta;
915
971
  var ret = new Array(params.length);
916
- var wait = params.length;
917
-
918
- function done() {
919
- wait--;
920
- if (wait === 0)
921
- callback(ret);
922
- }
923
-
972
+
924
973
  function putBlobData(index, value, callback) {
925
974
 
926
975
  self.createBlob2(transaction, function(err, blob) {
@@ -943,41 +992,41 @@ class Connection {
943
992
  self.batchSegments(blob, b, next);
944
993
  }, function() {
945
994
  ret[index] = new Xsql.SQLParamQuad(blob.oid);
946
- self.closeBlob(blob, callback);
995
+ self.closeBlob(blob, callback, false);
947
996
  });
948
997
  return;
949
998
  }
950
-
999
+
951
1000
  var isReading = false;
952
1001
  var isEnd = false;
953
-
1002
+
954
1003
  value.on('data', function(chunk) {
955
1004
  // Optimization: If chunk is smaller than transfer size, send directly
956
1005
  if (chunk.length <= chunkSize) {
957
1006
  self.batchSegments(blob, chunk, function () {
958
1007
  if (isEnd && !isReading) {
959
1008
  ret[index] = new Xsql.SQLParamQuad(blob.oid);
960
- self.closeBlob(blob, callback);
1009
+ self.closeBlob(blob, callback, false);
961
1010
  }
962
1011
  });
963
1012
  return;
964
1013
  }
965
-
1014
+
966
1015
  value.pause();
967
1016
  isReading = true;
968
1017
  bufferReader(chunk, chunkSize, function (b, next) {
969
1018
  self.batchSegments(blob, b, next);
970
1019
  }, function() {
971
1020
  isReading = false;
972
-
1021
+
973
1022
  if (isEnd) {
974
1023
  ret[index] = new Xsql.SQLParamQuad(blob.oid);
975
- self.closeBlob(blob, callback);
1024
+ self.closeBlob(blob, callback, false);
976
1025
  } else
977
1026
  value.resume();
978
1027
  });
979
1028
  });
980
-
1029
+
981
1030
  value.on('end', function() {
982
1031
  isEnd = true;
983
1032
  if (isReading)
@@ -986,16 +1035,21 @@ class Connection {
986
1035
  // If we are reading, the callback in batchSegments/bufferReader will handle closure
987
1036
  if (!isReading) {
988
1037
  ret[index] = new Xsql.SQLParamQuad(blob.oid);
989
- self.closeBlob(blob, callback);
1038
+ self.closeBlob(blob, callback, false);
990
1039
  }
991
1040
  });
992
1041
  });
993
1042
  }
994
1043
 
995
- for (var i = 0, length = params.length; i < length; i++) {
1044
+ function step(i) {
1045
+ if (i === params.length) {
1046
+ callback(ret);
1047
+ return;
1048
+ }
1049
+
996
1050
  value = params[i];
997
1051
  meta = input[i];
998
-
1052
+
999
1053
  if (value === null || value === undefined) {
1000
1054
  switch (meta.type) {
1001
1055
  case Const.SQL_VARYING:
@@ -1011,6 +1065,10 @@ class Connection {
1011
1065
  case Const.SQL_TYPE_DATE:
1012
1066
  case Const.SQL_TYPE_TIME:
1013
1067
  case Const.SQL_TIMESTAMP:
1068
+ case Const.SQL_TIME_TZ:
1069
+ case Const.SQL_TIME_TZ_EX:
1070
+ case Const.SQL_TIMESTAMP_TZ:
1071
+ case Const.SQL_TIMESTAMP_TZ_EX:
1014
1072
  ret[i] = new Xsql.SQLParamDate(null);
1015
1073
  break;
1016
1074
  case Const.SQL_BLOB:
@@ -1027,27 +1085,30 @@ class Connection {
1027
1085
  default:
1028
1086
  ret[i] = null;
1029
1087
  }
1030
- done();
1088
+ step(i + 1);
1031
1089
  } else {
1032
1090
  switch (meta.type) {
1033
1091
  case Const.SQL_BLOB:
1034
- putBlobData(i, value, done);
1092
+ putBlobData(i, value, function() { step(i + 1); });
1035
1093
  break;
1036
-
1094
+
1037
1095
  case Const.SQL_TIMESTAMP:
1038
1096
  case Const.SQL_TYPE_DATE:
1039
1097
  case Const.SQL_TYPE_TIME:
1040
-
1098
+ case Const.SQL_TIME_TZ:
1099
+ case Const.SQL_TIME_TZ_EX:
1100
+ case Const.SQL_TIMESTAMP_TZ:
1101
+ case Const.SQL_TIMESTAMP_TZ_EX:
1041
1102
  if (value instanceof Date)
1042
1103
  ret[i] = new Xsql.SQLParamDate(value);
1043
1104
  else if (typeof(value) === 'string')
1044
1105
  ret[i] = new Xsql.SQLParamDate(parseDate(value));
1045
1106
  else
1046
1107
  ret[i] = new Xsql.SQLParamDate(new Date(value));
1047
-
1048
- done();
1108
+
1109
+ step(i + 1);
1049
1110
  break;
1050
-
1111
+
1051
1112
  default:
1052
1113
  switch (typeof value) {
1053
1114
  case 'bigint':
@@ -1073,10 +1134,12 @@ class Connection {
1073
1134
  ret[i] = new Xsql.SQLParamString(value.toString());
1074
1135
  break;
1075
1136
  }
1076
- done();
1137
+ step(i + 1);
1077
1138
  }
1078
1139
  }
1079
1140
  }
1141
+
1142
+ step(0);
1080
1143
  }
1081
1144
 
1082
1145
  var input = statement.input;
@@ -1173,6 +1236,11 @@ class Connection {
1173
1236
  }
1174
1237
  msg.addInt(0); // out_message_number = out_message_type
1175
1238
  }
1239
+
1240
+ if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION16) {
1241
+ // TODO impl statement timout
1242
+ msg.addInt(statement.options?.timeout || 0); // p_sqldata_timeout
1243
+ }
1176
1244
 
1177
1245
  callback.statement = statement;
1178
1246
  this._queueEvent(callback);
@@ -1208,7 +1276,7 @@ class Connection {
1208
1276
 
1209
1277
  fetchAll(statement, transaction, callback) {
1210
1278
  const self = this;
1211
- const custom = statement.custom || {};
1279
+ const custom = statement.options || {};
1212
1280
  const asStream = custom.asStream && custom.on;
1213
1281
  const data = asStream ? null : [];
1214
1282
  let streamIndex = 0;
@@ -1272,12 +1340,12 @@ class Connection {
1272
1340
  }
1273
1341
 
1274
1342
 
1275
- closeBlob(blob, callback) {
1343
+ closeBlob(blob, callback, defer = true) {
1276
1344
  var msg = this._msg;
1277
1345
  msg.pos = 0;
1278
1346
  msg.addInt(Const.op_close_blob);
1279
1347
  msg.addInt(blob.handle);
1280
- this._queueEvent(callback, true);
1348
+ this._queueEvent(callback, defer);
1281
1349
  }
1282
1350
 
1283
1351
 
@@ -1449,9 +1517,16 @@ class Connection {
1449
1517
  msg.addInt(1); // async
1450
1518
  msg.addInt(self.dbhandle);
1451
1519
  msg.addInt(0);
1520
+ if (process.env.FIREBIRD_DEBUG) {
1521
+ console.log('[fb-debug] auxConnection: sending op_connect_request(53) dbhandle=%d queue_before=%d xdr_saved=%s',
1522
+ self.dbhandle, self._queue.length, Boolean(self._xdr));
1523
+ }
1452
1524
  function cb(err, ret) {
1453
1525
 
1454
1526
  if (err) {
1527
+ if (process.env.FIREBIRD_DEBUG) {
1528
+ console.log('[fb-debug] auxConnection: op_connect_request error: %s queue=%d', err.message, self._queue.length);
1529
+ }
1455
1530
  doError(err, callback);
1456
1531
  return;
1457
1532
  }
@@ -1461,6 +1536,11 @@ class Connection {
1461
1536
  port: ret.buffer.readUInt16BE(2),
1462
1537
  host: ret.buffer.readUInt8(4) + '.' + ret.buffer.readUInt8(5) + '.' + ret.buffer.readUInt8(6) + '.' + ret.buffer.readUInt8(7)
1463
1538
  }
1539
+
1540
+ if (process.env.FIREBIRD_DEBUG) {
1541
+ console.log('[fb-debug] auxConnection: op_response ok → aux family=%d port=%d host=%s queue=%d',
1542
+ socket_info.family, socket_info.port, socket_info.host, self._queue.length);
1543
+ }
1464
1544
 
1465
1545
  callback(undefined, socket_info);
1466
1546
  }
@@ -1470,7 +1550,6 @@ class Connection {
1470
1550
 
1471
1551
  queEvents(events, eventid, callback) {
1472
1552
  var self = this;
1473
- console.log('[Connection] queEvents called, eventid:', eventid, 'events:', Object.keys(events), 'isClosed:', this._isClosed);
1474
1553
  if (this._isClosed)
1475
1554
  return this.throwClosed(callback);
1476
1555
  var msg = this._msg;
@@ -1492,9 +1571,7 @@ class Connection {
1492
1571
  msg.addInt(0); // args
1493
1572
  msg.addInt(eventid);
1494
1573
 
1495
- console.log('[Connection] queEvents: Message prepared, queuing event');
1496
1574
  function cb(err, ret) {
1497
- console.log('[Connection] queEvents callback invoked, err:', err, 'ret:', ret);
1498
1575
  if (err) {
1499
1576
  doError(err, callback);
1500
1577
  return;
@@ -1504,13 +1581,11 @@ class Connection {
1504
1581
  }
1505
1582
 
1506
1583
  this._queueEvent(cb);
1507
- console.log('[Connection] queEvents: Event queued');
1508
1584
  }
1509
1585
 
1510
1586
 
1511
1587
  closeEvents(eventid, callback) {
1512
1588
  var self = this;
1513
- console.log('[Connection] closeEvents called, eventid:', eventid, 'isClosed:', this._isClosed);
1514
1589
  if (this._isClosed)
1515
1590
  return this.throwClosed(callback);
1516
1591
  var msg = self._msg;
@@ -1520,7 +1595,6 @@ class Connection {
1520
1595
  msg.addInt(eventid);
1521
1596
 
1522
1597
  function cb(err, ret) {
1523
- console.log('[Connection] closeEvents callback invoked, err:', err);
1524
1598
  if (err) {
1525
1599
  doError(err, callback);
1526
1600
  return;
@@ -1530,17 +1604,26 @@ class Connection {
1530
1604
  }
1531
1605
 
1532
1606
  this._queueEvent(cb);
1533
- console.log('[Connection] closeEvents: Event queued');
1534
1607
  }
1535
1608
 
1536
1609
  }
1537
1610
 
1611
+ // Reverse-lookup table: opcode number → name for FIREBIRD_DEBUG trace logging.
1612
+ const opcodeNames = Object.fromEntries(
1613
+ Object.entries(Const).filter(([k]) => k.startsWith('op_')).map(([k, v]) => [v, k])
1614
+ );
1615
+
1538
1616
  function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1539
1617
  try {
1540
1618
  do {
1541
1619
  var r = data.r || data.readInt();
1542
1620
  } while (r === Const.op_dummy);
1543
1621
 
1622
+ if (process.env.FIREBIRD_DEBUG) {
1623
+ console.log('[fb-debug] decodeResponse: opcode=%d(%s) pos=%d buflen=%d',
1624
+ r, opcodeNames[r] || 'unknown', data.pos, data.buffer.length);
1625
+ }
1626
+
1544
1627
  var item, op, response;
1545
1628
 
1546
1629
  switch (r) {
@@ -1575,7 +1658,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1575
1658
  case Const.op_sql_response:
1576
1659
  var statement = callback.statement;
1577
1660
  var output = statement.output;
1578
- var custom = statement.custom || {};
1661
+ var custom = statement.options || {};
1579
1662
  var isOpFetch = r === Const.op_fetch_response;
1580
1663
  var _xdrpos;
1581
1664
  statement.nbrowsfetched = statement.nbrowsfetched || 0;
@@ -1762,9 +1845,10 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1762
1845
  // Server keys
1763
1846
  cnx.serverKeys = {
1764
1847
  salt: d.buffer.slice(2, saltLen + 2).toString('utf8'),
1765
- public: BigInt(d.buffer.slice(keyStart, d.buffer.length).toString('utf8'), 16)
1848
+ public: BigInt('0x' + d.buffer.slice(keyStart, d.buffer.length).toString('utf8'))
1766
1849
  };
1767
1850
 
1851
+ const _t1 = Date.now();
1768
1852
  var proof = srp.clientProof(
1769
1853
  cnx.options.user.toUpperCase(),
1770
1854
  cnx.options.password,
@@ -1774,6 +1858,9 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1774
1858
  cnx.clientKeys.private,
1775
1859
  accept.srpAlgo
1776
1860
  );
1861
+ if (process.env.FIREBIRD_DEBUG) {
1862
+ console.log('[fb-debug] srp.clientProof(%s): %dms', accept.srpAlgo, Date.now() - _t1);
1863
+ }
1777
1864
 
1778
1865
  accept.authData = proof.authData.toString(16);
1779
1866
  accept.sessionKey = proof.clientSessionKey;
@@ -1792,9 +1879,20 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1792
1879
  cnx._socket.enableCompression();
1793
1880
  }
1794
1881
 
1882
+ if (process.env.FIREBIRD_DEBUG) {
1883
+ console.log('[fb-debug] auth: %s received plugin=%s proto=%d t=%dms',
1884
+ r === Const.op_cond_accept ? 'op_cond_accept' : r === Const.op_accept_data ? 'op_accept_data' : 'op_accept',
1885
+ accept.pluginName, accept.protocolVersion,
1886
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1887
+ }
1888
+
1795
1889
  // For op_cond_accept: send op_cont_auth and wait for response
1796
1890
  if (r === Const.op_cond_accept && accept.authData) {
1797
1891
  cnx._pendingAccept = accept;
1892
+ if (process.env.FIREBIRD_DEBUG) {
1893
+ console.log('[fb-debug] auth: sending op_cont_auth plugin=%s t=%dms',
1894
+ accept.pluginName, cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1895
+ }
1798
1896
  cnx.sendOpContAuth(
1799
1897
  accept.authData,
1800
1898
  Const.DEFAULT_ENCODING,
@@ -1810,8 +1908,43 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1810
1908
  data.readString(Const.DEFAULT_ENCODING); // plist
1811
1909
  data.readString(Const.DEFAULT_ENCODING); // pkey
1812
1910
 
1911
+ if (process.env.FIREBIRD_DEBUG) {
1912
+ console.log('[fb-debug] auth: op_cont_auth received plugin=%s pendingAccept=%s t=%dms',
1913
+ pluginName,
1914
+ cnx._pendingAccept ? cnx._pendingAccept.pluginName : 'none',
1915
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1916
+ }
1917
+
1918
+ // During SRP mutual authentication, the server sends op_cont_auth
1919
+ // with its proof (M2) after receiving the client's proof (M1).
1920
+ // When we have an active auth exchange for this plugin, just wait
1921
+ // for the subsequent op_accept instead of treating it as an error.
1922
+ if (cnx._pendingAccept && cnx._pendingAccept.pluginName === pluginName) {
1923
+ if (process.env.FIREBIRD_DEBUG) {
1924
+ console.log('[fb-debug] auth: server SRP proof (M2) received, waiting for op_accept t=%dms',
1925
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1926
+ }
1927
+ return; // Server SRP proof received - wait for op_accept
1928
+ }
1929
+
1930
+ // Firebird 4/5 (protocols 16/17) chained-auth: after the client sends
1931
+ // the SRP M1 proof, the server sends op_cont_auth with Legacy_Auth.
1932
+ // SRP has already established the session key; the server additionally
1933
+ // requires a Legacy_Auth verification step before sending op_accept.
1934
+ // Respond with Legacy_Auth credentials and wait for op_accept.
1935
+ if (cnx._pendingAccept && pluginName === Const.AUTH_PLUGIN_LEGACY) {
1936
+ if (process.env.FIREBIRD_DEBUG) {
1937
+ console.log('[fb-debug] auth: SRP+Legacy_Auth chained-auth (proto %d), sending Legacy_Auth credentials t=%dms',
1938
+ cnx._pendingAccept.protocolVersion,
1939
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1940
+ }
1941
+ var legacyAuthData = crypt.crypt(cnx.options.password, Const.LEGACY_AUTH_SALT).substring(2);
1942
+ cnx.sendOpContAuth(legacyAuthData, Const.DEFAULT_ENCODING, pluginName);
1943
+ return; // wait for op_accept
1944
+ }
1945
+
1813
1946
  if (!cnx.options.pluginName) {
1814
- if (cnx.accept.pluginName === pluginName) {
1947
+ if (cnx.accept && cnx.accept.pluginName === pluginName) {
1815
1948
  // Erreur plugin not able to connect
1816
1949
  return cb(new Error("Unable to connect with plugin " + cnx.accept.pluginName));
1817
1950
  }
@@ -1830,7 +1963,15 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1830
1963
  }
1831
1964
  }
1832
1965
 
1833
- return data.accept;
1966
+ // Server sent op_cont_auth but we don't know how to handle it.
1967
+ if (process.env.FIREBIRD_DEBUG) {
1968
+ console.warn('[fb-debug] auth: op_cont_auth unhandled plugin=%s pendingAccept=%s options.plugin=%s t=%dms',
1969
+ pluginName,
1970
+ cnx._pendingAccept ? cnx._pendingAccept.pluginName : 'none',
1971
+ cnx.options.pluginName || 'none',
1972
+ cnx._authStartTime ? Date.now() - cnx._authStartTime : -1);
1973
+ }
1974
+ return cb(new Error('Unhandled server op_cont_auth for plugin: ' + pluginName));
1834
1975
  case Const.op_crypt_key_callback:
1835
1976
  // Database encryption key callback
1836
1977
  // Read server data (plugin data sent by server)
@@ -1849,10 +1990,64 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1849
1990
 
1850
1991
  // Don't call cb - wait for next operation (likely op_response or another op_crypt_key_callback)
1851
1992
  return;
1993
+ case Const.op_event:
1994
+ // op_event may occasionally arrive on the main connection
1995
+ // (e.g. Firebird routing an async notification here instead of
1996
+ // the dedicated aux socket). Consume all its fields so the
1997
+ // buffer position advances correctly, then signal the data
1998
+ // handler to skip queue manipulation for this frame.
1999
+ //
2000
+ // Firebird wire protocol – op_event payload (remote protocol):
2001
+ // p_event_database : Int32 – database handle
2002
+ // p_event_items : Array – event parameter block (EPB)
2003
+ // p_event_ast : Int64 – AST routine pointer (0 for remote)
2004
+ // p_event_rid : Int32 – remote event ID
2005
+ {
2006
+ const evtDb = data.readInt(); // p_event_database
2007
+ data.readArray(); // p_event_items (EPB buffer)
2008
+ data.readInt64(); // p_event_ast
2009
+ const evtRid = data.readInt(); // p_event_rid
2010
+ if (process.env.FIREBIRD_DEBUG) {
2011
+ console.log('[fb-debug] op_event on main connection: db=%d rid=%d (consumed, not queued)', evtDb, evtRid);
2012
+ }
2013
+ }
2014
+ return cb(null, { _isOpEvent: true });
2015
+ case Const.op_response_piggyback:
2016
+ // Firebird 5 (Protocol 16/17) sends op_response_piggyback (72)
2017
+ // as an unsolicited cleanup notification after certain operations
2018
+ // (e.g. after the EventConnection aux socket is torn down).
2019
+ // It has the same wire layout as op_response but does NOT
2020
+ // correspond to any queued client request. Parse and discard it
2021
+ // so that the xdr buffer position advances correctly, then signal
2022
+ // the data handler to skip queue manipulation.
2023
+ //
2024
+ // Wire layout (identical to op_response):
2025
+ // handle : Int32
2026
+ // object : Quad (2x Int32)
2027
+ // data : Array
2028
+ // status : status-vector ending with isc_arg_end
2029
+ parseOpResponse(data, {}, function(err) {
2030
+ if (process.env.FIREBIRD_DEBUG) {
2031
+ if (err) {
2032
+ console.warn('[fb-debug] op_response_piggyback parse error:', err.message);
2033
+ } else {
2034
+ console.log('[fb-debug] op_response_piggyback consumed (unsolicited Firebird 5 cleanup)');
2035
+ }
2036
+ }
2037
+ });
2038
+ return cb(null, { _isOpEvent: true });
1852
2039
  default:
2040
+ if (process.env.FIREBIRD_DEBUG) {
2041
+ console.warn('[fb-debug] unknown opcode=%d at pos=%d buflen=%d queue=%d',
2042
+ r, data.pos, data.buffer.length, cnx && cnx._queue ? cnx._queue.length : 0);
2043
+ }
1853
2044
  return cb(new Error('Unexpected:' + r));
1854
2045
  }
1855
2046
  } catch (err) {
2047
+ if (process.env.FIREBIRD_DEBUG) {
2048
+ console.warn('[fb-debug] decodeResponse exception: %s (RangeError=%s) pos=%d buflen=%d',
2049
+ err.message, err instanceof RangeError, data.pos, data.buffer.length);
2050
+ }
1856
2051
  if (err instanceof RangeError) {
1857
2052
  return cb(err);
1858
2053
  }
@@ -1980,6 +2175,10 @@ function describe(buff, statement) {
1980
2175
  case Const.SQL_TYPE_DATE: param = new Xsql.SQLVarDate(); break;
1981
2176
  case Const.SQL_TYPE_TIME: param = new Xsql.SQLVarTime(); break;
1982
2177
  case Const.SQL_TIMESTAMP: param = new Xsql.SQLVarTimeStamp(); break;
2178
+ case Const.SQL_TIME_TZ: param = new Xsql.SQLVarTimeTz(); break;
2179
+ case Const.SQL_TIME_TZ_EX: param = new Xsql.SQLVarTimeTzEx(); break;
2180
+ case Const.SQL_TIMESTAMP_TZ: param = new Xsql.SQLVarTimeStampTz(); break;
2181
+ case Const.SQL_TIMESTAMP_TZ_EX: param = new Xsql.SQLVarTimeStampTzEx(); break;
1983
2182
  case Const.SQL_BLOB: param = new Xsql.SQLVarBlob(); break;
1984
2183
  case Const.SQL_ARRAY: param = new Xsql.SQLVarArray(); break;
1985
2184
  case Const.SQL_QUAD: param = new Xsql.SQLVarQuad(); break;
@@ -1987,6 +2186,8 @@ function describe(buff, statement) {
1987
2186
  case Const.SQL_SHORT: param = new Xsql.SQLVarShort(); break;
1988
2187
  case Const.SQL_INT64: param = new Xsql.SQLVarInt64(); break;
1989
2188
  case Const.SQL_INT128: param = new Xsql.SQLVarInt128(); break;
2189
+ case Const.SQL_DEC16: param = new Xsql.SQLVarDecFloat16(); break;
2190
+ case Const.SQL_DEC34: param = new Xsql.SQLVarDecFloat34(); break;
1990
2191
  case Const.SQL_BOOLEAN: param = new Xsql.SQLVarBoolean(); break;
1991
2192
  default:
1992
2193
  throw new Error('Unexpected');