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