node-firebird 2.8.1 → 2.10.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.
Files changed (53) hide show
  1. package/README.md +268 -7
  2. package/lib/index.d.ts +17 -9
  3. package/lib/index.js +42 -3
  4. package/lib/named-params.d.ts +42 -0
  5. package/lib/named-params.js +133 -0
  6. package/lib/pool.js +1 -1
  7. package/lib/srp.d.ts +3 -3
  8. package/lib/types.d.ts +185 -25
  9. package/lib/uri.d.ts +57 -0
  10. package/lib/uri.js +193 -0
  11. package/lib/wire/connection.d.ts +92 -59
  12. package/lib/wire/connection.js +286 -55
  13. package/lib/wire/const.d.ts +9 -1
  14. package/lib/wire/const.js +21 -9
  15. package/lib/wire/database.d.ts +51 -26
  16. package/lib/wire/database.js +26 -8
  17. package/lib/wire/eventConnection.js +5 -3
  18. package/lib/wire/query-stream.d.ts +18 -0
  19. package/lib/wire/query-stream.js +73 -0
  20. package/lib/wire/serialize.d.ts +18 -2
  21. package/lib/wire/serialize.js +7 -0
  22. package/lib/wire/service.d.ts +42 -0
  23. package/lib/wire/service.js +145 -0
  24. package/lib/wire/socket.d.ts +3 -1
  25. package/lib/wire/socket.js +5 -2
  26. package/lib/wire/statement.d.ts +40 -20
  27. package/lib/wire/statement.js +26 -6
  28. package/lib/wire/transaction.d.ts +32 -18
  29. package/lib/wire/transaction.js +50 -8
  30. package/lib/wire/wire-types.d.ts +116 -0
  31. package/lib/wire/wire-types.js +10 -0
  32. package/lib/wire/xsqlvar.d.ts +18 -18
  33. package/package.json +1 -1
  34. package/src/index.ts +54 -15
  35. package/src/messages.ts +1 -1
  36. package/src/named-params.ts +145 -0
  37. package/src/pool.ts +1 -1
  38. package/src/srp.ts +6 -6
  39. package/src/types.ts +183 -25
  40. package/src/unix-crypt.ts +9 -9
  41. package/src/uri.ts +204 -0
  42. package/src/wire/connection.ts +481 -234
  43. package/src/wire/const.ts +21 -9
  44. package/src/wire/database.ts +75 -43
  45. package/src/wire/eventConnection.ts +8 -5
  46. package/src/wire/query-stream.ts +80 -0
  47. package/src/wire/serialize.ts +29 -0
  48. package/src/wire/service.ts +188 -6
  49. package/src/wire/socket.ts +17 -8
  50. package/src/wire/statement.ts +68 -31
  51. package/src/wire/transaction.ts +85 -33
  52. package/src/wire/wire-types.ts +127 -0
  53. package/src/wire/xsqlvar.ts +9 -7
@@ -61,6 +61,112 @@ function parseValueIfJson(value, options) {
61
61
  }
62
62
  return value;
63
63
  }
64
+ /**
65
+ * Resolve the prepared-statement cache limit from the connection options:
66
+ * `statementCacheSize` (new), or the legacy `cacheQuery`/`maxCachedQuery`
67
+ * pair (which had no eviction; it now gets a bounded LRU). 0 = disabled.
68
+ */
69
+ /**
70
+ * Build the isc_dpb_search_path value from the defaultSchema / searchPath
71
+ * options (Firebird 6.0 / protocol 20+). There is no "default schema" DPB
72
+ * tag in Firebird: CURRENT_SCHEMA is simply the first existing schema of
73
+ * the search path, so defaultSchema is implemented by putting it at the
74
+ * front of the list. When only defaultSchema is given, PUBLIC is kept as a
75
+ * fallback so unqualified names outside the new schema still resolve (the
76
+ * server always appends SYSTEM itself). Returns null when neither option
77
+ * is set.
78
+ */
79
+ function buildSchemaSearchPath(options) {
80
+ var list = [];
81
+ if (options.searchPath) {
82
+ list = Array.isArray(options.searchPath)
83
+ ? options.searchPath.slice()
84
+ : String(options.searchPath).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
85
+ }
86
+ var def = options.defaultSchema;
87
+ if (def) {
88
+ if (list.length === 0) {
89
+ // defaultSchema alone: keep PUBLIC as a fallback after it
90
+ list = def === 'PUBLIC' ? ['PUBLIC'] : [def, 'PUBLIC'];
91
+ }
92
+ else {
93
+ // explicit searchPath: respect it, just move defaultSchema first
94
+ list = [def].concat(list.filter(function (s) { return s !== def; }));
95
+ }
96
+ }
97
+ return list.length ? list.join(',') : null;
98
+ }
99
+ function statementCacheLimit(options) {
100
+ const size = options && options.statementCacheSize;
101
+ if (size && size > 0) {
102
+ return Math.floor(size);
103
+ }
104
+ if (options && options.cacheQuery) {
105
+ const legacy = options.maxCachedQuery;
106
+ return legacy && legacy > 0 ? Math.floor(legacy) : 100;
107
+ }
108
+ return 0;
109
+ }
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
+ };
135
+ /**
136
+ * Run the user's typeCast hook (options.typeCast) for one column value.
137
+ * The hook receives the column metadata and a next() returning the value
138
+ * the driver would produce by default (after blobAsText/jsonAsObject);
139
+ * whatever it returns becomes the value in the row. Rows may be decoded
140
+ * more than once when a response spans TCP packets, so the hook must be
141
+ * a pure function of its inputs.
142
+ */
143
+ function applyTypeCast(options, meta, defaultValue) {
144
+ const typeCast = options && options.typeCast;
145
+ if (typeof typeCast !== 'function') {
146
+ return defaultValue;
147
+ }
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
+ };
158
+ // A hook exception must never escape into the row-decode loop: there it
159
+ // would be mistaken for an incomplete packet and desync the response
160
+ // queue (the same failure mode as issue #341). Fall back to the default
161
+ // value instead and tell the user.
162
+ try {
163
+ return typeCast(column, function () { return defaultValue; });
164
+ }
165
+ catch (err) {
166
+ console.warn('[node-firebird] typeCast hook threw for column "%s" (%s): %s — using default value', column.alias || column.field, column.typeName, err && err.message);
167
+ return defaultValue;
168
+ }
169
+ }
64
170
  /***************************************
65
171
  *
66
172
  * Connection
@@ -69,6 +175,8 @@ function parseValueIfJson(value, options) {
69
175
  class Connection {
70
176
  constructor(host, port, callback, options, db, svc) {
71
177
  var self = this;
178
+ // db is absent for service-manager connections; the wire core only
179
+ // touches it on database attachments, where it is always set
72
180
  this.db = db;
73
181
  this.svc = svc;
74
182
  this._msg = new serialize_1.XdrWriter(32);
@@ -77,35 +185,78 @@ class Connection {
77
185
  this._detachTimeout;
78
186
  this._detachCallback;
79
187
  this._detachAuto;
80
- this._socket = new socket_1.default(port, host);
188
+ this._socket = new socket_1.default(port, host, options.enableKeepAlive !== false, options.keepAliveInitialDelay);
81
189
  this._pending = [];
82
190
  this._isOpened = false;
83
191
  this._isClosed = false;
84
192
  this._isDetach = false;
85
193
  this._isUsed = false;
86
194
  this._pooled = options.isPool || false;
87
- if (options && options.blobChunkSize > 65535)
195
+ // Credentials may be absent (e.g. a traditional host:database
196
+ // connection string) — apply the driver defaults once here, so every
197
+ // auth path (op_connect CNCT block, SRP proof, legacy cont_auth) sees
198
+ // the same values.
199
+ if (options && !options.user)
200
+ options.user = const_1.default.DEFAULT_USER;
201
+ if (options && !options.password)
202
+ options.password = const_1.default.DEFAULT_PASSWORD;
203
+ if (options && options.blobChunkSize && options.blobChunkSize > 65535)
88
204
  options.blobChunkSize = 65535;
89
- if (options && options.blobReadChunkSize > 65535)
205
+ if (options && options.blobReadChunkSize && options.blobReadChunkSize > 65535)
90
206
  options.blobReadChunkSize = 65535;
91
207
  this.options = options;
92
208
  this._bind_events(host, port, callback);
93
209
  this.error;
94
210
  this._retry_connection_id;
95
211
  this._retry_connection_interval = options.retryConnectionInterval || 1000;
96
- this._max_cached_query = options.maxCachedQuery || -1;
97
- this._cache_query = options.cacheQuery ? {} : null;
212
+ this._statementCacheSize = statementCacheLimit(options);
213
+ this._statementCache = this._statementCacheSize > 0 ? new Map() : null;
98
214
  this._messageFile = options.messageFile || path_1.default.join(__dirname, 'firebird.msg');
99
215
  }
100
- _setcachedquery(query, statement) {
101
- if (this._cache_query) {
102
- if (this._max_cached_query === -1 || this._max_cached_query > Object.keys(this._cache_query).length) {
103
- this._cache_query[query] = statement;
104
- }
216
+ /**
217
+ * Take an idle prepared statement for `query` out of the cache, or null.
218
+ * The statement leaves the cache while in use, so concurrent callers of
219
+ * the same query never share a server-side cursor — they simply prepare
220
+ * a fresh statement and the spare is dropped when released.
221
+ */
222
+ takeCachedStatement(query) {
223
+ const cache = this._statementCache;
224
+ if (!cache) {
225
+ return null;
226
+ }
227
+ const statement = cache.get(query);
228
+ if (!statement) {
229
+ return null;
105
230
  }
231
+ cache.delete(query);
232
+ return statement;
106
233
  }
107
- getCachedQuery(query) {
108
- return this._cache_query ? this._cache_query[query] : null;
234
+ /**
235
+ * Return a statement after use. With the statement cache enabled the
236
+ * statement goes back into the cache as most-recently-used (closing its
237
+ * cursor but keeping the prepared handle), evicting the least-recently
238
+ * used statement over the limit. Failed statements, DDL and spares for
239
+ * an already-cached query are dropped instead.
240
+ */
241
+ releaseStatement(statement, callback) {
242
+ const cache = this._statementCache;
243
+ const cacheable = cache &&
244
+ statement.query &&
245
+ !statement._failed &&
246
+ statement.type !== const_1.default.isc_info_sql_stmt_ddl &&
247
+ !cache.has(statement.query);
248
+ if (!cacheable) {
249
+ this.dropStatement(statement, callback);
250
+ return;
251
+ }
252
+ cache.set(statement.query, statement);
253
+ while (cache.size > this._statementCacheSize) {
254
+ const oldestKey = cache.keys().next().value;
255
+ const oldest = cache.get(oldestKey);
256
+ cache.delete(oldestKey);
257
+ this.dropStatement(oldest, undefined);
258
+ }
259
+ this.closeStatement(statement, callback);
109
260
  }
110
261
  // Reject every request that is still awaiting a server response (e.g. a
111
262
  // transaction.commit() sent right before the server went away) instead of
@@ -139,6 +290,8 @@ class Connection {
139
290
  self._rejectPending(lostError);
140
291
  self._retry_connection_id = setTimeout(function () {
141
292
  self._socket.removeAllListeners();
293
+ // transiently null while the replacement Connection is built
294
+ // (restored by the Object.assign(self, ctx) below)
142
295
  self._socket = null;
143
296
  var ctx = new Connection(host, port, function (err) {
144
297
  ctx.connect(self.options, function (err) {
@@ -339,8 +492,17 @@ class Connection {
339
492
  }
340
493
  const canDefer = defer && this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION11;
341
494
  self._socket.write(self._msg.getData(), canDefer);
342
- if (canDefer && callback) {
343
- callback();
495
+ if (canDefer) {
496
+ // A deferred packet sits in the socket buffer until the next
497
+ // non-deferred write flushes it, but the server still answers it
498
+ // with its own op_response (delivered along with that next
499
+ // exchange). Queue a placeholder to consume that response —
500
+ // otherwise the queue pairs it with the NEXT request and every
501
+ // later response is off by one. The op itself is fire-and-forget,
502
+ // so complete the caller right away.
503
+ self._queue.push(undefined);
504
+ if (callback)
505
+ callback();
344
506
  }
345
507
  else {
346
508
  self._queue.push(callback);
@@ -384,10 +546,17 @@ class Connection {
384
546
  msg.addInt(const_1.default.CONNECT_VERSION3);
385
547
  msg.addInt(const_1.default.ARCHITECTURE_GENERIC);
386
548
  msg.addString(options.database || options.filename, const_1.default.DEFAULT_ENCODING);
387
- var maxProtocols = options.maxNegotiatedProtocols !== undefined ? options.maxNegotiatedProtocols : 10;
549
+ // Send the full list by default. Servers parse every entry and ignore
550
+ // versions they do not know (verified back to Firebird 2.5), so the
551
+ // list length itself is harmless; the option remains as an escape
552
+ // hatch to cap negotiation at an older protocol.
553
+ var maxProtocols = options.maxNegotiatedProtocols !== undefined ? options.maxNegotiatedProtocols : const_1.default.SUPPORTED_PROTOCOL.length;
388
554
  var protocolsToSend = const_1.default.SUPPORTED_PROTOCOL;
389
555
  if (protocolsToSend.length > maxProtocols) {
390
- protocolsToSend = protocolsToSend.slice(-maxProtocols);
556
+ // keep the FIRST N entries: the list is ordered oldest→newest, so
557
+ // capping the count caps the newest protocol offered (e.g. 10 =
558
+ // stop at protocol 19, the documented pre-Firebird-6 behavior)
559
+ protocolsToSend = protocolsToSend.slice(0, maxProtocols);
391
560
  }
392
561
  msg.addInt(protocolsToSend.length); // Count of Protocol version understood count.
393
562
  msg.addBlr(this._blr);
@@ -523,21 +692,9 @@ class Connection {
523
692
  blr.addNumeric(const_1.default.isc_dpb_max_inline_blob_size, options.maxInlineBlobSize);
524
693
  }
525
694
  // Firebird 6.0 SQL Schema parameters (Protocol 20+).
526
- // These DPB tags configure the session's current schema and the
527
- // schema search path for unqualified object name resolution.
528
695
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
529
- if (options.defaultSchema) {
530
- // Sets CURRENT_SCHEMA for the session. Equivalent to issuing
531
- // SET SCHEMA <name> immediately after connecting.
532
- blr.addString(const_1.default.isc_dpb_default_schema, options.defaultSchema, const_1.default.DEFAULT_ENCODING);
533
- }
534
- if (options.searchPath) {
535
- // Comma-separated ordered schema name list, like PostgreSQL's
536
- // search_path. Unqualified object references are resolved by
537
- // scanning schemas in this order.
538
- const sp = Array.isArray(options.searchPath)
539
- ? options.searchPath.join(',')
540
- : String(options.searchPath);
696
+ const sp = buildSchemaSearchPath(options);
697
+ if (sp) {
541
698
  blr.addString(const_1.default.isc_dpb_search_path, sp, const_1.default.DEFAULT_ENCODING);
542
699
  }
543
700
  }
@@ -637,16 +794,17 @@ class Connection {
637
794
  }
638
795
  // Firebird 6.0 SQL Schema parameters (Protocol 20+).
639
796
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
640
- if (options.defaultSchema) {
641
- blr.addString(const_1.default.isc_dpb_default_schema, options.defaultSchema, const_1.default.DEFAULT_ENCODING);
642
- }
643
- if (options.searchPath) {
644
- const sp = Array.isArray(options.searchPath)
645
- ? options.searchPath.join(',')
646
- : String(options.searchPath);
797
+ const sp = buildSchemaSearchPath(options);
798
+ if (sp) {
647
799
  blr.addString(const_1.default.isc_dpb_search_path, sp, const_1.default.DEFAULT_ENCODING);
648
800
  }
649
801
  }
802
+ if (options.owner) {
803
+ // Firebird 6.0+ (issue #7718): create the database owned by a
804
+ // different user (requires superuser rights). Older servers
805
+ // ignore unknown DPB tags, so this is safe to always send.
806
+ blr.addString(const_1.default.isc_dpb_owner, options.owner, const_1.default.DEFAULT_ENCODING);
807
+ }
650
808
  blr.addNumeric(const_1.default.isc_dpb_sql_dialect, 3);
651
809
  blr.addNumeric(const_1.default.isc_dpb_force_write, 1);
652
810
  blr.addNumeric(const_1.default.isc_dpb_overwrite, 1);
@@ -690,6 +848,8 @@ class Connection {
690
848
  callback(err);
691
849
  return this;
692
850
  }
851
+ /** `options` is a resolved options object, a bare isolation array, or
852
+ * the callback itself when no options are given. */
693
853
  startTransaction(options, callback) {
694
854
  if (typeof (options) === 'function') {
695
855
  var tmp = options;
@@ -851,7 +1011,6 @@ class Connection {
851
1011
  mainCallback.response.query = query;
852
1012
  self.db.emit('query', query);
853
1013
  ret = mainCallback.response;
854
- self._setcachedquery(query, ret);
855
1014
  }
856
1015
  if (callback)
857
1016
  callback(err, ret);
@@ -879,6 +1038,12 @@ class Connection {
879
1038
  msg.addString(query, const_1.default.DEFAULT_ENCODING);
880
1039
  msg.addBlr(blr);
881
1040
  msg.addInt(65535); // buffer_length
1041
+ if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
1042
+ // p_sqlst_flags (IStatement::PREPARE_* bits, none needed) — the
1043
+ // server blocks reading this field if it is missing, which was
1044
+ // the protocol-20 "prepare hang"
1045
+ msg.addInt(0);
1046
+ }
882
1047
  mainCallback.lazy_count += 1;
883
1048
  mainCallback.response = new statement_1.default(this);
884
1049
  this._queueEvent(mainCallback);
@@ -898,6 +1063,7 @@ class Connection {
898
1063
  });
899
1064
  }
900
1065
  }
1066
+ /** `plan` may be the callback itself when no plan flag is given. */
901
1067
  prepareStatement(transaction, statement, query, plan, callback) {
902
1068
  if (this._isClosed)
903
1069
  return this.throwClosed(callback);
@@ -920,6 +1086,9 @@ class Connection {
920
1086
  msg.addString(query, const_1.default.DEFAULT_ENCODING);
921
1087
  msg.addBlr(blr);
922
1088
  msg.addInt(65535); // buffer_length
1089
+ if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
1090
+ msg.addInt(0); // p_sqlst_flags (see allocateAndPrepareStatement)
1091
+ }
923
1092
  var self = this;
924
1093
  this._queueEvent(function (err, ret) {
925
1094
  if (!err) {
@@ -927,7 +1096,6 @@ class Connection {
927
1096
  statement.query = query;
928
1097
  self.db.emit('query', query);
929
1098
  ret = statement;
930
- self._setcachedquery(query, ret);
931
1099
  }
932
1100
  if (callback)
933
1101
  callback(err, ret);
@@ -985,7 +1153,7 @@ class Connection {
985
1153
  }
986
1154
  var self = this;
987
1155
  var encoders = built.encoders;
988
- var chunkSize = options.chunkSize > 0 ? options.chunkSize : 500;
1156
+ var chunkSize = options.chunkSize && options.chunkSize > 0 ? options.chunkSize : 500;
989
1157
  var chunkCount = Math.ceil(rows.length / chunkSize);
990
1158
  var failure = null;
991
1159
  var completion = null;
@@ -1043,6 +1211,7 @@ class Connection {
1043
1211
  msg.addInt(statement.handle);
1044
1212
  msg.addInt(end - start);
1045
1213
  for (var i = start; i < end; i++) {
1214
+ // validated as an array of input.length values above
1046
1215
  var row = rows[i];
1047
1216
  var nullBits = new serialize_1.BitSet();
1048
1217
  for (var j = 0; j < input.length; j++) {
@@ -1114,6 +1283,7 @@ class Connection {
1114
1283
  }
1115
1284
  }
1116
1285
  }
1286
+ /** `params` may be the callback itself when the statement has no parameters. */
1117
1287
  executeStatement(transaction, statement, params, callback, custom) {
1118
1288
  if (this._isClosed)
1119
1289
  return this.throwClosed(callback);
@@ -1425,6 +1595,7 @@ class Connection {
1425
1595
  callback.statement = statement;
1426
1596
  this._queueEvent(callback);
1427
1597
  }
1598
+ /** `count` may be the callback itself when no fetch size is given. */
1428
1599
  fetch(statement, transaction, count, callback) {
1429
1600
  var msg = this._msg;
1430
1601
  var blr = this._blr;
@@ -1528,7 +1699,7 @@ class Connection {
1528
1699
  readBlobsSequentially(0, []).then((arrBlob) => {
1529
1700
  for (let i = 0; i < arrBlob.length; i++) {
1530
1701
  const blob = arrBlob[i];
1531
- ret.data[blob.row][blob.column] = parseValueIfJson(blob.value, statement.connection.options);
1702
+ ret.data[blob.row][blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
1532
1703
  }
1533
1704
  doSynchronousLoop(ret.data, (row, _i, next) => {
1534
1705
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -1895,12 +2066,25 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1895
2066
  }
1896
2067
  case const_1.default.op_fetch_response:
1897
2068
  case const_1.default.op_sql_response:
2069
+ // fetch/sql_response entries always carry their statement
1898
2070
  var statement = callback.statement;
1899
2071
  var output = statement.output;
1900
2072
  var custom = statement.options || {};
1901
2073
  var isOpFetch = r === const_1.default.op_fetch_response;
1902
2074
  var _xdrpos;
1903
2075
  statement.nbrowsfetched = statement.nbrowsfetched || 0;
2076
+ // The f* decode state is only meaningful within a single
2077
+ // decode call: incomplete packets are re-decoded from scratch
2078
+ // on a fresh XdrReader (see the 'data' handler). State left by
2079
+ // an earlier packet in the same data event (e.g. fstatus=100 /
2080
+ // fcount=0 from a completed fetch) would make this decode
2081
+ // consume just the opcode and desync every later response.
2082
+ delete data.fstatus;
2083
+ delete data.fcount;
2084
+ delete data.fcolumn;
2085
+ delete data.frow;
2086
+ delete data.frows;
2087
+ delete data.fcols;
1904
2088
  if (isOpFetch && data.fop) { // could be set when a packet is not complete
1905
2089
  data.readBuffer(68); // ??
1906
2090
  op = data.readInt(); // ??
@@ -1927,6 +2111,13 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1927
2111
  }
1928
2112
  const arrBlob = [];
1929
2113
  const lowerV13 = statement.connection.accept.protocolVersion < const_1.default.PROTOCOL_VERSION13;
2114
+ // op_sql_response (op_execute2) is always followed by an
2115
+ // op_response carrying the execute status vector. The row loop
2116
+ // below consumes it after the last row, but with zero rows
2117
+ // (e.g. INSERT ... RETURNING failing on a constraint) it stays
2118
+ // in the buffer, shifting every later response to the wrong
2119
+ // callback and poisoning the connection (issue #341).
2120
+ var sqlResponseTrailerPending = !isOpFetch && !data.fcount;
1930
2121
  while (data.fcount && (data.fstatus !== 100)) {
1931
2122
  let nullBitSet;
1932
2123
  if (!lowerV13) {
@@ -1937,12 +2128,8 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1937
2128
  for (let length = output.length; data.fcolumn < length; data.fcolumn++) {
1938
2129
  item = output[data.fcolumn];
1939
2130
  if (!lowerV13 && nullBitSet.get(data.fcolumn)) {
1940
- if (custom.asObject) {
1941
- data.frow[data.fcols[data.fcolumn]] = null;
1942
- }
1943
- else {
1944
- data.frow[data.fcolumn] = null;
1945
- }
2131
+ const nullKey = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
2132
+ data.frow[nullKey] = applyTypeCast(cnx.options, item, null);
1946
2133
  continue;
1947
2134
  }
1948
2135
  try {
@@ -1950,16 +2137,23 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1950
2137
  const key = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
1951
2138
  const row = data.frows.length;
1952
2139
  let value = item.decode(data, lowerV13, cnx.options);
2140
+ // text blobs resolved by blobAsText run through the
2141
+ // typeCast hook once the text arrives (see fetchAll),
2142
+ // not here where the value is still a pending fetch
2143
+ let pendingTextBlob = false;
1953
2144
  if (item.type === const_1.default.SQL_BLOB && value !== null) {
1954
2145
  if (item.subType === const_1.default.isc_blob_text && cnx.options.blobAsText) {
1955
- value = fetch_blob_async_transaction(statement, value, key, row);
2146
+ value = fetch_blob_async_transaction(statement, value, key, row, item);
1956
2147
  arrBlob.push(value);
2148
+ pendingTextBlob = true;
1957
2149
  }
1958
2150
  else {
1959
2151
  value = fetch_blob_async(statement, value, key, row);
1960
2152
  }
1961
2153
  }
1962
- data.frow[key] = parseValueIfJson(value, cnx.options);
2154
+ data.frow[key] = pendingTextBlob
2155
+ ? value
2156
+ : applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
1963
2157
  }
1964
2158
  catch (e) {
1965
2159
  // uncomplete packet read
@@ -2006,6 +2200,16 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2006
2200
  }
2007
2201
  statement.nbrowsfetched++;
2008
2202
  }
2203
+ if (sqlResponseTrailerPending) {
2204
+ op = data.readInt();
2205
+ if (op === const_1.default.op_response) {
2206
+ response = {};
2207
+ parseOpResponse(data, response);
2208
+ if (response.status) {
2209
+ return cb(null, response);
2210
+ }
2211
+ }
2212
+ }
2009
2213
  // ToDo: emit "result" with blob subtype string decoded
2010
2214
  statement.connection.db.emit('result', data.frows, arrBlob);
2011
2215
  return cb(null, { data: data.frows, fetched: Boolean(!isOpFetch || data.fstatus === 100), arrBlob });
@@ -2340,6 +2544,17 @@ function readStatusVector(data) {
2340
2544
  result.sqlcode = n;
2341
2545
  }
2342
2546
  break;
2547
+ case const_1.default.isc_arg_warning:
2548
+ // A warning attached to a SUCCESS vector (e.g. "parallel
2549
+ // workers value capped"). Keep it out of `status` so the
2550
+ // operation is not mistaken for a failure; later string/
2551
+ // number items attach to the warning entry.
2552
+ var wnum = data.readInt();
2553
+ item = { gdscode: wnum };
2554
+ if (wnum) {
2555
+ (result.warnings = result.warnings || []).push(item);
2556
+ }
2557
+ break;
2343
2558
  default:
2344
2559
  throw new Error('Unexpected status vector item: ' + op);
2345
2560
  }
@@ -2400,13 +2615,25 @@ function parseOpResponse(data, response, cb) {
2400
2615
  response.sqlcode = num;
2401
2616
  }
2402
2617
  break;
2618
+ case const_1.default.isc_arg_warning:
2619
+ // A warning attached to a SUCCESS response (e.g. Firebird's
2620
+ // "parallel workers value capped" on attach). Keep it out of
2621
+ // `status` so the response is not mistaken for an error;
2622
+ // later string/number items attach to the warning entry.
2623
+ num = data.readInt();
2624
+ item = { gdscode: num };
2625
+ if (num) {
2626
+ (response.warnings = response.warnings || []).push(item);
2627
+ }
2628
+ break;
2403
2629
  default:
2630
+ // Stop parsing: continuing the loop after an unknown item
2631
+ // re-read the same bytes forever (the caller resets the
2632
+ // reader position when the error is delivered).
2404
2633
  if (cb) {
2405
- cb(new Error('Unexpected: ' + op));
2406
- }
2407
- else {
2408
- throw new Error('Unexpected: ' + op);
2634
+ return cb(new Error('Unexpected: ' + op));
2409
2635
  }
2636
+ throw new Error('Unexpected: ' + op);
2410
2637
  }
2411
2638
  }
2412
2639
  }
@@ -2443,6 +2670,8 @@ function describe(buff, statement) {
2443
2670
  case const_1.default.isc_info_sql_describe_end:
2444
2671
  break;
2445
2672
  case const_1.default.isc_info_sql_sqlda_seq:
2673
+ // describe output always encodes the sequence as a
2674
+ // 1/2/4-byte int, so readInt cannot return undefined
2446
2675
  var num = br.readInt();
2447
2676
  break;
2448
2677
  case const_1.default.isc_info_sql_type:
@@ -2518,6 +2747,8 @@ function describe(buff, statement) {
2518
2747
  default:
2519
2748
  throw new Error('Unexpected');
2520
2749
  }
2750
+ // isc_info_sql_sqlda_seq always precedes the type
2751
+ // item in the describe stream, so num is set here
2521
2752
  parameters[num - 1] = param;
2522
2753
  param.type = type;
2523
2754
  param.nullable = Boolean(param.type & 1);
@@ -2788,8 +3019,8 @@ function CalcBlr(blr, xsqlda) {
2788
3019
  blr.addByte(const_1.default.blr_end);
2789
3020
  blr.addByte(const_1.default.blr_eoc);
2790
3021
  }
2791
- function fetch_blob_async_transaction(statement, id, column, row) {
2792
- const infoValue = { row, column, value: '' };
3022
+ function fetch_blob_async_transaction(statement, id, column, row, meta) {
3023
+ const infoValue = { row, column, value: '', meta };
2793
3024
  return (transactionArg) => {
2794
3025
  const cacheKey = `${id.high}:${id.low}`;
2795
3026
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
@@ -324,10 +324,18 @@ declare const Const: Readonly<{
324
324
  isc_dpb_reset_icu: number;
325
325
  isc_dpb_map_attach: number;
326
326
  isc_dpb_session_time_zone: number;
327
+ isc_dpb_set_db_replica: number;
328
+ isc_dpb_set_bind: number;
329
+ isc_dpb_decfloat_round: number;
330
+ isc_dpb_decfloat_traps: number;
331
+ isc_dpb_clear_map: number;
332
+ isc_dpb_upgrade_db: number;
327
333
  isc_dpb_parallel_workers: number;
334
+ isc_dpb_worker_attach: number;
335
+ isc_dpb_owner: number;
336
+ isc_dpb_max_blob_cache_size: number;
328
337
  isc_dpb_max_inline_blob_size: number;
329
338
  isc_dpb_search_path: number;
330
- isc_dpb_default_schema: number;
331
339
  CNCT_user: number;
332
340
  CNCT_passwd: number;
333
341
  CNCT_host: number;
package/lib/wire/const.js CHANGED
@@ -236,7 +236,8 @@ const SUPPORTED_PROTOCOL = [
236
236
  [protocol.PROTOCOL_VERSION16, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 7],
237
237
  [protocol.PROTOCOL_VERSION17, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 8],
238
238
  [protocol.PROTOCOL_VERSION18, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 9],
239
- [protocol.PROTOCOL_VERSION19, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 10]
239
+ [protocol.PROTOCOL_VERSION19, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 10],
240
+ [protocol.PROTOCOL_VERSION20, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 11]
240
241
  ];
241
242
  const authPlugin = {
242
243
  AUTH_PLUGIN_LEGACY: 'Legacy_Auth',
@@ -430,16 +431,27 @@ const dpb = {
430
431
  isc_dpb_reset_icu: 89,
431
432
  isc_dpb_map_attach: 90,
432
433
  isc_dpb_session_time_zone: 91,
433
- isc_dpb_parallel_workers: 92,
434
- isc_dpb_max_inline_blob_size: 93,
435
- // Firebird 6.0 SQL Schema parameters
434
+ // Firebird 4.0
435
+ isc_dpb_set_db_replica: 92,
436
+ isc_dpb_set_bind: 93,
437
+ isc_dpb_decfloat_round: 94,
438
+ isc_dpb_decfloat_traps: 95,
439
+ isc_dpb_clear_map: 96,
440
+ // Firebird 5.0
441
+ isc_dpb_upgrade_db: 97,
442
+ isc_dpb_parallel_workers: 100,
443
+ isc_dpb_worker_attach: 101,
444
+ // Firebird 6.0
445
+ // isc_dpb_owner sets the owner of a newly created database to a user
446
+ // other than the connecting one (create only; requires superuser).
447
+ isc_dpb_owner: 102,
448
+ isc_dpb_max_blob_cache_size: 103,
449
+ isc_dpb_max_inline_blob_size: 104, // Firebird 5.0.3+
436
450
  // isc_dpb_search_path sets the comma-separated ordered list of schema
437
451
  // names to search for unqualified object names (similar to PostgreSQL
438
- // search_path). Available on Protocol 20+ (Firebird 6.0).
439
- isc_dpb_search_path: 94,
440
- // isc_dpb_default_schema overrides the session's current schema
441
- // (CURRENT_SCHEMA) at connection time. Available on Protocol 20+.
442
- isc_dpb_default_schema: 95,
452
+ // search_path). CURRENT_SCHEMA is the first existing schema of the list.
453
+ // Available on Protocol 20+ (Firebird 6.0).
454
+ isc_dpb_search_path: 105,
443
455
  };
444
456
  const cnct = {
445
457
  CNCT_user: 1, // User name