node-firebird 2.9.0 → 2.11.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 (50) hide show
  1. package/README.md +211 -6
  2. package/lib/index.d.ts +5 -0
  3. package/lib/index.js +32 -1
  4. package/lib/pool.js +1 -1
  5. package/lib/srp.d.ts +3 -3
  6. package/lib/types.d.ts +165 -5
  7. package/lib/uri.js +2 -2
  8. package/lib/wire/connection.d.ts +92 -59
  9. package/lib/wire/connection.js +279 -58
  10. package/lib/wire/const.d.ts +9 -1
  11. package/lib/wire/const.js +23 -9
  12. package/lib/wire/database.d.ts +51 -26
  13. package/lib/wire/database.js +53 -20
  14. package/lib/wire/eventConnection.js +5 -3
  15. package/lib/wire/query-stream.d.ts +18 -0
  16. package/lib/wire/query-stream.js +73 -0
  17. package/lib/wire/serialize.d.ts +20 -2
  18. package/lib/wire/serialize.js +7 -0
  19. package/lib/wire/service.d.ts +42 -0
  20. package/lib/wire/service.js +145 -0
  21. package/lib/wire/socket.d.ts +3 -1
  22. package/lib/wire/socket.js +5 -2
  23. package/lib/wire/statement.d.ts +31 -19
  24. package/lib/wire/statement.js +1 -5
  25. package/lib/wire/transaction.d.ts +30 -18
  26. package/lib/wire/transaction.js +23 -4
  27. package/lib/wire/wire-types.d.ts +116 -0
  28. package/lib/wire/wire-types.js +10 -0
  29. package/lib/wire/xsqlvar.d.ts +57 -18
  30. package/lib/wire/xsqlvar.js +59 -0
  31. package/package.json +1 -1
  32. package/src/index.ts +36 -4
  33. package/src/messages.ts +1 -1
  34. package/src/pool.ts +1 -1
  35. package/src/srp.ts +6 -6
  36. package/src/types.ts +162 -5
  37. package/src/unix-crypt.ts +9 -9
  38. package/src/uri.ts +2 -2
  39. package/src/wire/connection.ts +475 -234
  40. package/src/wire/const.ts +23 -9
  41. package/src/wire/database.ts +101 -54
  42. package/src/wire/eventConnection.ts +8 -5
  43. package/src/wire/query-stream.ts +80 -0
  44. package/src/wire/serialize.ts +31 -0
  45. package/src/wire/service.ts +188 -6
  46. package/src/wire/socket.ts +17 -8
  47. package/src/wire/statement.ts +37 -29
  48. package/src/wire/transaction.ts +57 -32
  49. package/src/wire/wire-types.ts +127 -0
  50. package/src/wire/xsqlvar.ts +85 -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,7 +185,7 @@ 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;
@@ -92,28 +200,63 @@ class Connection {
92
200
  options.user = const_1.default.DEFAULT_USER;
93
201
  if (options && !options.password)
94
202
  options.password = const_1.default.DEFAULT_PASSWORD;
95
- if (options && options.blobChunkSize > 65535)
203
+ if (options && options.blobChunkSize && options.blobChunkSize > 65535)
96
204
  options.blobChunkSize = 65535;
97
- if (options && options.blobReadChunkSize > 65535)
205
+ if (options && options.blobReadChunkSize && options.blobReadChunkSize > 65535)
98
206
  options.blobReadChunkSize = 65535;
99
207
  this.options = options;
100
208
  this._bind_events(host, port, callback);
101
209
  this.error;
102
210
  this._retry_connection_id;
103
211
  this._retry_connection_interval = options.retryConnectionInterval || 1000;
104
- this._max_cached_query = options.maxCachedQuery || -1;
105
- this._cache_query = options.cacheQuery ? {} : null;
212
+ this._statementCacheSize = statementCacheLimit(options);
213
+ this._statementCache = this._statementCacheSize > 0 ? new Map() : null;
106
214
  this._messageFile = options.messageFile || path_1.default.join(__dirname, 'firebird.msg');
107
215
  }
108
- _setcachedquery(query, statement) {
109
- if (this._cache_query) {
110
- if (this._max_cached_query === -1 || this._max_cached_query > Object.keys(this._cache_query).length) {
111
- this._cache_query[query] = statement;
112
- }
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;
113
226
  }
227
+ const statement = cache.get(query);
228
+ if (!statement) {
229
+ return null;
230
+ }
231
+ cache.delete(query);
232
+ return statement;
114
233
  }
115
- getCachedQuery(query) {
116
- 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);
117
260
  }
118
261
  // Reject every request that is still awaiting a server response (e.g. a
119
262
  // transaction.commit() sent right before the server went away) instead of
@@ -147,6 +290,8 @@ class Connection {
147
290
  self._rejectPending(lostError);
148
291
  self._retry_connection_id = setTimeout(function () {
149
292
  self._socket.removeAllListeners();
293
+ // transiently null while the replacement Connection is built
294
+ // (restored by the Object.assign(self, ctx) below)
150
295
  self._socket = null;
151
296
  var ctx = new Connection(host, port, function (err) {
152
297
  ctx.connect(self.options, function (err) {
@@ -401,10 +546,17 @@ class Connection {
401
546
  msg.addInt(const_1.default.CONNECT_VERSION3);
402
547
  msg.addInt(const_1.default.ARCHITECTURE_GENERIC);
403
548
  msg.addString(options.database || options.filename, const_1.default.DEFAULT_ENCODING);
404
- 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;
405
554
  var protocolsToSend = const_1.default.SUPPORTED_PROTOCOL;
406
555
  if (protocolsToSend.length > maxProtocols) {
407
- 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);
408
560
  }
409
561
  msg.addInt(protocolsToSend.length); // Count of Protocol version understood count.
410
562
  msg.addBlr(this._blr);
@@ -540,21 +692,9 @@ class Connection {
540
692
  blr.addNumeric(const_1.default.isc_dpb_max_inline_blob_size, options.maxInlineBlobSize);
541
693
  }
542
694
  // Firebird 6.0 SQL Schema parameters (Protocol 20+).
543
- // These DPB tags configure the session's current schema and the
544
- // schema search path for unqualified object name resolution.
545
695
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
546
- if (options.defaultSchema) {
547
- // Sets CURRENT_SCHEMA for the session. Equivalent to issuing
548
- // SET SCHEMA <name> immediately after connecting.
549
- blr.addString(const_1.default.isc_dpb_default_schema, options.defaultSchema, const_1.default.DEFAULT_ENCODING);
550
- }
551
- if (options.searchPath) {
552
- // Comma-separated ordered schema name list, like PostgreSQL's
553
- // search_path. Unqualified object references are resolved by
554
- // scanning schemas in this order.
555
- const sp = Array.isArray(options.searchPath)
556
- ? options.searchPath.join(',')
557
- : String(options.searchPath);
696
+ const sp = buildSchemaSearchPath(options);
697
+ if (sp) {
558
698
  blr.addString(const_1.default.isc_dpb_search_path, sp, const_1.default.DEFAULT_ENCODING);
559
699
  }
560
700
  }
@@ -654,16 +794,17 @@ class Connection {
654
794
  }
655
795
  // Firebird 6.0 SQL Schema parameters (Protocol 20+).
656
796
  if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
657
- if (options.defaultSchema) {
658
- blr.addString(const_1.default.isc_dpb_default_schema, options.defaultSchema, const_1.default.DEFAULT_ENCODING);
659
- }
660
- if (options.searchPath) {
661
- const sp = Array.isArray(options.searchPath)
662
- ? options.searchPath.join(',')
663
- : String(options.searchPath);
797
+ const sp = buildSchemaSearchPath(options);
798
+ if (sp) {
664
799
  blr.addString(const_1.default.isc_dpb_search_path, sp, const_1.default.DEFAULT_ENCODING);
665
800
  }
666
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
+ }
667
808
  blr.addNumeric(const_1.default.isc_dpb_sql_dialect, 3);
668
809
  blr.addNumeric(const_1.default.isc_dpb_force_write, 1);
669
810
  blr.addNumeric(const_1.default.isc_dpb_overwrite, 1);
@@ -707,6 +848,8 @@ class Connection {
707
848
  callback(err);
708
849
  return this;
709
850
  }
851
+ /** `options` is a resolved options object, a bare isolation array, or
852
+ * the callback itself when no options are given. */
710
853
  startTransaction(options, callback) {
711
854
  if (typeof (options) === 'function') {
712
855
  var tmp = options;
@@ -868,7 +1011,6 @@ class Connection {
868
1011
  mainCallback.response.query = query;
869
1012
  self.db.emit('query', query);
870
1013
  ret = mainCallback.response;
871
- self._setcachedquery(query, ret);
872
1014
  }
873
1015
  if (callback)
874
1016
  callback(err, ret);
@@ -896,6 +1038,12 @@ class Connection {
896
1038
  msg.addString(query, const_1.default.DEFAULT_ENCODING);
897
1039
  msg.addBlr(blr);
898
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
+ }
899
1047
  mainCallback.lazy_count += 1;
900
1048
  mainCallback.response = new statement_1.default(this);
901
1049
  this._queueEvent(mainCallback);
@@ -915,6 +1063,7 @@ class Connection {
915
1063
  });
916
1064
  }
917
1065
  }
1066
+ /** `plan` may be the callback itself when no plan flag is given. */
918
1067
  prepareStatement(transaction, statement, query, plan, callback) {
919
1068
  if (this._isClosed)
920
1069
  return this.throwClosed(callback);
@@ -937,6 +1086,9 @@ class Connection {
937
1086
  msg.addString(query, const_1.default.DEFAULT_ENCODING);
938
1087
  msg.addBlr(blr);
939
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
+ }
940
1092
  var self = this;
941
1093
  this._queueEvent(function (err, ret) {
942
1094
  if (!err) {
@@ -944,7 +1096,6 @@ class Connection {
944
1096
  statement.query = query;
945
1097
  self.db.emit('query', query);
946
1098
  ret = statement;
947
- self._setcachedquery(query, ret);
948
1099
  }
949
1100
  if (callback)
950
1101
  callback(err, ret);
@@ -1002,7 +1153,7 @@ class Connection {
1002
1153
  }
1003
1154
  var self = this;
1004
1155
  var encoders = built.encoders;
1005
- var chunkSize = options.chunkSize > 0 ? options.chunkSize : 500;
1156
+ var chunkSize = options.chunkSize && options.chunkSize > 0 ? options.chunkSize : 500;
1006
1157
  var chunkCount = Math.ceil(rows.length / chunkSize);
1007
1158
  var failure = null;
1008
1159
  var completion = null;
@@ -1060,6 +1211,7 @@ class Connection {
1060
1211
  msg.addInt(statement.handle);
1061
1212
  msg.addInt(end - start);
1062
1213
  for (var i = start; i < end; i++) {
1214
+ // validated as an array of input.length values above
1063
1215
  var row = rows[i];
1064
1216
  var nullBits = new serialize_1.BitSet();
1065
1217
  for (var j = 0; j < input.length; j++) {
@@ -1131,6 +1283,7 @@ class Connection {
1131
1283
  }
1132
1284
  }
1133
1285
  }
1286
+ /** `params` may be the callback itself when the statement has no parameters. */
1134
1287
  executeStatement(transaction, statement, params, callback, custom) {
1135
1288
  if (this._isClosed)
1136
1289
  return this.throwClosed(callback);
@@ -1442,6 +1595,7 @@ class Connection {
1442
1595
  callback.statement = statement;
1443
1596
  this._queueEvent(callback);
1444
1597
  }
1598
+ /** `count` may be the callback itself when no fetch size is given. */
1445
1599
  fetch(statement, transaction, count, callback) {
1446
1600
  var msg = this._msg;
1447
1601
  var blr = this._blr;
@@ -1545,7 +1699,9 @@ class Connection {
1545
1699
  readBlobsSequentially(0, []).then((arrBlob) => {
1546
1700
  for (let i = 0; i < arrBlob.length; i++) {
1547
1701
  const blob = arrBlob[i];
1548
- ret.data[blob.row][blob.column] = parseValueIfJson(blob.value, statement.connection.options);
1702
+ // nestTables === true rows: the value lives in the
1703
+ // per-table sub-object, not on the row itself
1704
+ Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
1549
1705
  }
1550
1706
  doSynchronousLoop(ret.data, (row, _i, next) => {
1551
1707
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -1912,12 +2068,26 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1912
2068
  }
1913
2069
  case const_1.default.op_fetch_response:
1914
2070
  case const_1.default.op_sql_response:
2071
+ // fetch/sql_response entries always carry their statement
1915
2072
  var statement = callback.statement;
1916
2073
  var output = statement.output;
1917
2074
  var custom = statement.options || {};
1918
2075
  var isOpFetch = r === const_1.default.op_fetch_response;
1919
2076
  var _xdrpos;
1920
2077
  statement.nbrowsfetched = statement.nbrowsfetched || 0;
2078
+ // The f* decode state is only meaningful within a single
2079
+ // decode call: incomplete packets are re-decoded from scratch
2080
+ // on a fresh XdrReader (see the 'data' handler). State left by
2081
+ // an earlier packet in the same data event (e.g. fstatus=100 /
2082
+ // fcount=0 from a completed fetch) would make this decode
2083
+ // consume just the opcode and desync every later response.
2084
+ delete data.fstatus;
2085
+ delete data.fcount;
2086
+ delete data.fcolumn;
2087
+ delete data.frow;
2088
+ delete data.frows;
2089
+ delete data.fcols;
2090
+ delete data.ftables;
1921
2091
  if (isOpFetch && data.fop) { // could be set when a packet is not complete
1922
2092
  data.readBuffer(68); // ??
1923
2093
  op = data.readInt(); // ??
@@ -1935,15 +2105,23 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1935
2105
  data.frow = data.frow || (custom.asObject ? {} : new Array(output.length));
1936
2106
  data.frows = data.frows || [];
1937
2107
  if (custom.asObject && !data.fcols) {
1938
- if (lowercase_keys) {
1939
- data.fcols = output.map((column) => column.alias.toLowerCase());
1940
- }
1941
- else {
1942
- data.fcols = output.map((column) => column.alias);
2108
+ const nest = Xsql.resolveNestTables(custom, cnx.options);
2109
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2110
+ data.fcols = columnKeys.map((k) => k.key);
2111
+ if (nest === true) {
2112
+ // computeColumnKeys always sets table when nesting
2113
+ data.ftables = columnKeys.map((k) => k.table);
1943
2114
  }
1944
2115
  }
1945
2116
  const arrBlob = [];
1946
2117
  const lowerV13 = statement.connection.accept.protocolVersion < const_1.default.PROTOCOL_VERSION13;
2118
+ // op_sql_response (op_execute2) is always followed by an
2119
+ // op_response carrying the execute status vector. The row loop
2120
+ // below consumes it after the last row, but with zero rows
2121
+ // (e.g. INSERT ... RETURNING failing on a constraint) it stays
2122
+ // in the buffer, shifting every later response to the wrong
2123
+ // callback and poisoning the connection (issue #341).
2124
+ var sqlResponseTrailerPending = !isOpFetch && !data.fcount;
1947
2125
  while (data.fcount && (data.fstatus !== 100)) {
1948
2126
  let nullBitSet;
1949
2127
  if (!lowerV13) {
@@ -1954,12 +2132,11 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1954
2132
  for (let length = output.length; data.fcolumn < length; data.fcolumn++) {
1955
2133
  item = output[data.fcolumn];
1956
2134
  if (!lowerV13 && nullBitSet.get(data.fcolumn)) {
1957
- if (custom.asObject) {
1958
- data.frow[data.fcols[data.fcolumn]] = null;
1959
- }
1960
- else {
1961
- data.frow[data.fcolumn] = null;
1962
- }
2135
+ const nullKey = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
2136
+ // ftables is only set when nestTables === true, so
2137
+ // the default path writes straight into the row
2138
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn]) : data.frow)[nullKey] =
2139
+ applyTypeCast(cnx.options, item, null);
1963
2140
  continue;
1964
2141
  }
1965
2142
  try {
@@ -1967,16 +2144,23 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
1967
2144
  const key = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
1968
2145
  const row = data.frows.length;
1969
2146
  let value = item.decode(data, lowerV13, cnx.options);
2147
+ // text blobs resolved by blobAsText run through the
2148
+ // typeCast hook once the text arrives (see fetchAll),
2149
+ // not here where the value is still a pending fetch
2150
+ let pendingTextBlob = false;
1970
2151
  if (item.type === const_1.default.SQL_BLOB && value !== null) {
1971
2152
  if (item.subType === const_1.default.isc_blob_text && cnx.options.blobAsText) {
1972
- value = fetch_blob_async_transaction(statement, value, key, row);
2153
+ value = fetch_blob_async_transaction(statement, value, key, row, item, data.ftables && data.ftables[data.fcolumn]);
1973
2154
  arrBlob.push(value);
2155
+ pendingTextBlob = true;
1974
2156
  }
1975
2157
  else {
1976
2158
  value = fetch_blob_async(statement, value, key, row);
1977
2159
  }
1978
2160
  }
1979
- data.frow[key] = parseValueIfJson(value, cnx.options);
2161
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn]) : data.frow)[key] = pendingTextBlob
2162
+ ? value
2163
+ : applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
1980
2164
  }
1981
2165
  catch (e) {
1982
2166
  // uncomplete packet read
@@ -2023,6 +2207,16 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2023
2207
  }
2024
2208
  statement.nbrowsfetched++;
2025
2209
  }
2210
+ if (sqlResponseTrailerPending) {
2211
+ op = data.readInt();
2212
+ if (op === const_1.default.op_response) {
2213
+ response = {};
2214
+ parseOpResponse(data, response);
2215
+ if (response.status) {
2216
+ return cb(null, response);
2217
+ }
2218
+ }
2219
+ }
2026
2220
  // ToDo: emit "result" with blob subtype string decoded
2027
2221
  statement.connection.db.emit('result', data.frows, arrBlob);
2028
2222
  return cb(null, { data: data.frows, fetched: Boolean(!isOpFetch || data.fstatus === 100), arrBlob });
@@ -2357,6 +2551,17 @@ function readStatusVector(data) {
2357
2551
  result.sqlcode = n;
2358
2552
  }
2359
2553
  break;
2554
+ case const_1.default.isc_arg_warning:
2555
+ // A warning attached to a SUCCESS vector (e.g. "parallel
2556
+ // workers value capped"). Keep it out of `status` so the
2557
+ // operation is not mistaken for a failure; later string/
2558
+ // number items attach to the warning entry.
2559
+ var wnum = data.readInt();
2560
+ item = { gdscode: wnum };
2561
+ if (wnum) {
2562
+ (result.warnings = result.warnings || []).push(item);
2563
+ }
2564
+ break;
2360
2565
  default:
2361
2566
  throw new Error('Unexpected status vector item: ' + op);
2362
2567
  }
@@ -2417,13 +2622,25 @@ function parseOpResponse(data, response, cb) {
2417
2622
  response.sqlcode = num;
2418
2623
  }
2419
2624
  break;
2625
+ case const_1.default.isc_arg_warning:
2626
+ // A warning attached to a SUCCESS response (e.g. Firebird's
2627
+ // "parallel workers value capped" on attach). Keep it out of
2628
+ // `status` so the response is not mistaken for an error;
2629
+ // later string/number items attach to the warning entry.
2630
+ num = data.readInt();
2631
+ item = { gdscode: num };
2632
+ if (num) {
2633
+ (response.warnings = response.warnings || []).push(item);
2634
+ }
2635
+ break;
2420
2636
  default:
2637
+ // Stop parsing: continuing the loop after an unknown item
2638
+ // re-read the same bytes forever (the caller resets the
2639
+ // reader position when the error is delivered).
2421
2640
  if (cb) {
2422
- cb(new Error('Unexpected: ' + op));
2423
- }
2424
- else {
2425
- throw new Error('Unexpected: ' + op);
2641
+ return cb(new Error('Unexpected: ' + op));
2426
2642
  }
2643
+ throw new Error('Unexpected: ' + op);
2427
2644
  }
2428
2645
  }
2429
2646
  }
@@ -2460,6 +2677,8 @@ function describe(buff, statement) {
2460
2677
  case const_1.default.isc_info_sql_describe_end:
2461
2678
  break;
2462
2679
  case const_1.default.isc_info_sql_sqlda_seq:
2680
+ // describe output always encodes the sequence as a
2681
+ // 1/2/4-byte int, so readInt cannot return undefined
2463
2682
  var num = br.readInt();
2464
2683
  break;
2465
2684
  case const_1.default.isc_info_sql_type:
@@ -2535,6 +2754,8 @@ function describe(buff, statement) {
2535
2754
  default:
2536
2755
  throw new Error('Unexpected');
2537
2756
  }
2757
+ // isc_info_sql_sqlda_seq always precedes the type
2758
+ // item in the describe stream, so num is set here
2538
2759
  parameters[num - 1] = param;
2539
2760
  param.type = type;
2540
2761
  param.nullable = Boolean(param.type & 1);
@@ -2805,8 +3026,8 @@ function CalcBlr(blr, xsqlda) {
2805
3026
  blr.addByte(const_1.default.blr_end);
2806
3027
  blr.addByte(const_1.default.blr_eoc);
2807
3028
  }
2808
- function fetch_blob_async_transaction(statement, id, column, row) {
2809
- const infoValue = { row, column, value: '' };
3029
+ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3030
+ const infoValue = { row, column, value: '', meta, table };
2810
3031
  return (transactionArg) => {
2811
3032
  const cacheKey = `${id.high}:${id.low}`;
2812
3033
  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
@@ -565,6 +577,7 @@ const DESCRIBE = [
565
577
  sqlInfo.isc_info_sql_length,
566
578
  sqlInfo.isc_info_sql_field,
567
579
  sqlInfo.isc_info_sql_relation,
580
+ sqlInfo.isc_info_sql_relation_alias, // FB 2.0+: query alias of the source relation (nestTables)
568
581
  //isc_info_sql_owner,
569
582
  sqlInfo.isc_info_sql_alias,
570
583
  sqlInfo.isc_info_sql_describe_end,
@@ -592,6 +605,7 @@ const DESCRIBE_WITH_SCHEMA = [
592
605
  sqlInfo.isc_info_sql_field,
593
606
  sqlInfo.isc_info_sql_relation,
594
607
  sqlInfo.isc_info_sql_relation_schema, // FB 6.0: schema of source relation
608
+ sqlInfo.isc_info_sql_relation_alias, // query alias of the source relation (nestTables)
595
609
  //isc_info_sql_owner,
596
610
  sqlInfo.isc_info_sql_alias,
597
611
  sqlInfo.isc_info_sql_describe_end,