node-firebird 2.10.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  - [Promises and async/await](#promises-and-asyncawait) — the `*Async` API plus `withConnection` / `withTransaction` helpers
14
14
  - [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
- - [Examples](#examples) — parametrized queries, named placeholders, custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
16
+ - [Examples](#examples) — parametrized queries, named placeholders, nested result tables (nestTables), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
17
  - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
@@ -185,6 +185,7 @@ options.searchPath = undefined; // optional; ordered list/array of schemas to re
185
185
  options.owner = undefined; // optional; owner of a newly created database — lets a superuser create a database for another user (create only, FB >= 6.0)
186
186
  options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
187
187
  options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
188
+ options.nestTables = false; // true nests object rows by source table (row[table][column]); a string separator flattens to 'table<sep>column' keys — see Nested result tables (nestTables). Overridable per query
188
189
  options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
189
190
  options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)
190
191
  ```
@@ -492,6 +493,50 @@ the per-query option:
492
493
  await db.queryAsync(execBlockSql, [], { namedPlaceholders: false });
493
494
  ```
494
495
 
496
+ ### Nested result tables (nestTables)
497
+
498
+ In a `JOIN`, columns sharing a name overwrite each other in object rows —
499
+ `SELECT EMP.ID, DEPT.ID ...` leaves only one `ID` key. The `nestTables`
500
+ option (same as mysql2's) qualifies row keys by source table instead. It is
501
+ accepted at connection level and per query; the per-query value wins.
502
+
503
+ With `nestTables: true` each row nests one sub-object per table:
504
+
505
+ ```js
506
+ const rows = await db.queryAsync(
507
+ 'SELECT EMP.ID, EMP.NAME, DEPT.ID, DEPT.NAME FROM EMP JOIN DEPT ON DEPT.ID = EMP.DEPT_ID',
508
+ [], { nestTables: true });
509
+ // rows[0] = { EMP: { ID: 10, NAME: 'Ada' }, DEPT: { ID: 1, NAME: 'Engineering' } }
510
+ ```
511
+
512
+ With a string separator the keys stay flat but qualified:
513
+
514
+ ```js
515
+ const rows = await db.queryAsync(sql, [], { nestTables: '_' });
516
+ // rows[0] = { EMP_ID: 10, EMP_NAME: 'Ada', DEPT_ID: 1, DEPT_NAME: 'Engineering' }
517
+ ```
518
+
519
+ The table qualifier is the query's relation alias when one is used, the
520
+ table name otherwise — so self-joins nest cleanly:
521
+
522
+ ```js
523
+ const rows = await db.queryAsync(
524
+ 'SELECT E.NAME, B.NAME FROM EMP E LEFT JOIN EMP B ON B.ID = E.BOSS_ID',
525
+ [], { nestTables: true });
526
+ // rows[0] = { E: { NAME: 'Grace' }, B: { NAME: 'Ada' } }
527
+ ```
528
+
529
+ Expression columns (no source table) qualify as `''`, exactly like mysql2:
530
+ they land under the `''` key when nesting (`row[''].ANSWER`) and get the
531
+ bare separator prefix in separator mode (`row._ANSWER`) — always prefixing
532
+ keeps qualified keys collision-free (a bare expression alias could
533
+ otherwise collide with a real `table<sep>column` key). Keys honour
534
+ `lowercase_keys`, and the option composes with
535
+ `typeCast`, `blobAsText` and `queryStream`. Object rows only: `db.execute`
536
+ array rows are positional and need no qualification. Works on every
537
+ supported Firebird version (the source-table metadata comes from the
538
+ statement describe, available since Firebird 2.0).
539
+
495
540
  ### Custom type parsers (typeCast)
496
541
 
497
542
  The `typeCast` connection option lets you override how column values are
package/lib/types.d.ts CHANGED
@@ -115,6 +115,18 @@ export type QueryOptions = {
115
115
  * it cancels whatever is currently executing on the connection.
116
116
  */
117
117
  signal?: AbortSignal;
118
+ /**
119
+ * Per-query override of the `nestTables` connection option (mysql2
120
+ * semantics). `true` nests each object row by source table:
121
+ * `row[table][column]` — the table key is the query's relation alias
122
+ * when one is used (`FROM emp e` → `row.E`), the table name otherwise,
123
+ * and `''` for expression columns. A string separator flattens keys
124
+ * instead: `nestTables: '_'` → `row.EMP_NAME`; expression columns get
125
+ * the bare separator prefix (`row._ANSWER`, as in mysql2). Keys honour
126
+ * `lowercase_keys`. Object rows only — `db.execute` array rows are
127
+ * unaffected.
128
+ */
129
+ nestTables?: boolean | string;
118
130
  };
119
131
  export type QueryStreamOptions = QueryOptions & {
120
132
  /**
@@ -254,6 +266,16 @@ export interface Options {
254
266
  * per-query `namedPlaceholders: false` override.
255
267
  */
256
268
  namedPlaceholders?: boolean;
269
+ /**
270
+ * Qualify object-row keys by source table (same option as mysql2), so
271
+ * JOINed columns with the same name stop overwriting each other:
272
+ * `true` nests each row as `row[table][column]`; a string separator
273
+ * flattens to `row['table' + sep + 'column']`. See
274
+ * `QueryOptions.nestTables` for the exact key rules. Applies wherever
275
+ * object rows are produced (query / sequentially / queryStream);
276
+ * array rows (execute) are unaffected. Overridable per query.
277
+ */
278
+ nestTables?: boolean | string;
257
279
  /**
258
280
  * TCP keepalive probing to detect dead/stale connections (same option
259
281
  * names as mysql2). On by default; set false to disable.
@@ -150,6 +150,6 @@ declare function decodeResponse(data: XdrReader, callback: QueueCallback | undef
150
150
  error: Error;
151
151
  };
152
152
  declare function describe(buff: Buffer, statement: Statement): void;
153
- declare function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase): (transactionArg: any) => Promise<unknown>;
153
+ declare function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string): (transactionArg: any) => Promise<unknown>;
154
154
  declare function fetch_blob_async(statement: Statement, id: Quad, name: string | number, row: number): (transaction: Transaction, callback: any) => void;
155
155
  export = Connection;
@@ -1699,7 +1699,9 @@ class Connection {
1699
1699
  readBlobsSequentially(0, []).then((arrBlob) => {
1700
1700
  for (let i = 0; i < arrBlob.length; i++) {
1701
1701
  const blob = arrBlob[i];
1702
- ret.data[blob.row][blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, 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));
1703
1705
  }
1704
1706
  doSynchronousLoop(ret.data, (row, _i, next) => {
1705
1707
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -2085,6 +2087,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2085
2087
  delete data.frow;
2086
2088
  delete data.frows;
2087
2089
  delete data.fcols;
2090
+ delete data.ftables;
2088
2091
  if (isOpFetch && data.fop) { // could be set when a packet is not complete
2089
2092
  data.readBuffer(68); // ??
2090
2093
  op = data.readInt(); // ??
@@ -2102,11 +2105,12 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2102
2105
  data.frow = data.frow || (custom.asObject ? {} : new Array(output.length));
2103
2106
  data.frows = data.frows || [];
2104
2107
  if (custom.asObject && !data.fcols) {
2105
- if (lowercase_keys) {
2106
- data.fcols = output.map((column) => column.alias.toLowerCase());
2107
- }
2108
- else {
2109
- 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);
2110
2114
  }
2111
2115
  }
2112
2116
  const arrBlob = [];
@@ -2129,7 +2133,10 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2129
2133
  item = output[data.fcolumn];
2130
2134
  if (!lowerV13 && nullBitSet.get(data.fcolumn)) {
2131
2135
  const nullKey = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
2132
- data.frow[nullKey] = applyTypeCast(cnx.options, item, null);
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);
2133
2140
  continue;
2134
2141
  }
2135
2142
  try {
@@ -2143,7 +2150,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2143
2150
  let pendingTextBlob = false;
2144
2151
  if (item.type === const_1.default.SQL_BLOB && value !== null) {
2145
2152
  if (item.subType === const_1.default.isc_blob_text && cnx.options.blobAsText) {
2146
- value = fetch_blob_async_transaction(statement, value, key, row, item);
2153
+ value = fetch_blob_async_transaction(statement, value, key, row, item, data.ftables && data.ftables[data.fcolumn]);
2147
2154
  arrBlob.push(value);
2148
2155
  pendingTextBlob = true;
2149
2156
  }
@@ -2151,7 +2158,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2151
2158
  value = fetch_blob_async(statement, value, key, row);
2152
2159
  }
2153
2160
  }
2154
- data.frow[key] = pendingTextBlob
2161
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn]) : data.frow)[key] = pendingTextBlob
2155
2162
  ? value
2156
2163
  : applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
2157
2164
  }
@@ -3019,8 +3026,8 @@ function CalcBlr(blr, xsqlda) {
3019
3026
  blr.addByte(const_1.default.blr_end);
3020
3027
  blr.addByte(const_1.default.blr_eoc);
3021
3028
  }
3022
- function fetch_blob_async_transaction(statement, id, column, row, meta) {
3023
- const infoValue = { row, column, value: '', meta };
3029
+ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3030
+ const infoValue = { row, column, value: '', meta, table };
3024
3031
  return (transactionArg) => {
3025
3032
  const cacheKey = `${id.high}:${id.low}`;
3026
3033
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
package/lib/wire/const.js CHANGED
@@ -577,6 +577,7 @@ const DESCRIBE = [
577
577
  sqlInfo.isc_info_sql_length,
578
578
  sqlInfo.isc_info_sql_field,
579
579
  sqlInfo.isc_info_sql_relation,
580
+ sqlInfo.isc_info_sql_relation_alias, // FB 2.0+: query alias of the source relation (nestTables)
580
581
  //isc_info_sql_owner,
581
582
  sqlInfo.isc_info_sql_alias,
582
583
  sqlInfo.isc_info_sql_describe_end,
@@ -604,6 +605,7 @@ const DESCRIBE_WITH_SCHEMA = [
604
605
  sqlInfo.isc_info_sql_field,
605
606
  sqlInfo.isc_info_sql_relation,
606
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)
607
609
  //isc_info_sql_owner,
608
610
  sqlInfo.isc_info_sql_alias,
609
611
  sqlInfo.isc_info_sql_describe_end,
@@ -6,6 +6,7 @@ const events_1 = __importDefault(require("events"));
6
6
  const callback_1 = require("../callback");
7
7
  const utils_1 = require("../utils");
8
8
  const const_1 = __importDefault(require("./const"));
9
+ const xsqlvar_1 = require("./xsqlvar");
9
10
  const eventConnection_1 = __importDefault(require("./eventConnection"));
10
11
  const fbEventManager_1 = __importDefault(require("./fbEventManager"));
11
12
  const query_stream_1 = __importDefault(require("./query-stream"));
@@ -75,30 +76,42 @@ function readblob(blob, callback) {
75
76
  });
76
77
  });
77
78
  }
78
- function fetchBlobSyncRow(row, meta, callback) {
79
- if (!row || !meta || !meta.length) {
79
+ function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, callback) {
80
+ if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === const_1.default.SQL_BLOB)) {
80
81
  callback(null, row);
81
82
  return;
82
83
  }
83
- const rowKeys = Object.keys(row);
84
- const blobColumns = [];
84
+ // locate blob cells by the same key computation the fetch decoder used,
85
+ // rather than assuming Object.keys(row) is index-aligned with meta —
86
+ // duplicate JOIN column names (and nested rows) break that alignment.
87
+ // Array rows (sequentially's legacy boolean form) are keyed by index.
88
+ const isArrayRow = Array.isArray(row);
89
+ const keys = isArrayRow ? null : (0, xsqlvar_1.computeColumnKeys)(meta, nestTables, lowercaseKeys);
90
+ const blobCells = [];
85
91
  for (let i = 0; i < meta.length; i++) {
86
- if (meta[i] && meta[i].type === const_1.default.SQL_BLOB && rowKeys[i] !== undefined) {
87
- blobColumns.push(rowKeys[i]);
92
+ if (!meta[i] || meta[i].type !== const_1.default.SQL_BLOB) {
93
+ continue;
94
+ }
95
+ const target = keys ? (0, xsqlvar_1.nestCell)(row, keys[i].table) : row;
96
+ const key = keys ? keys[i].key : i;
97
+ // duplicate aliases collapse onto one cell — read it only once
98
+ if (typeof target[key] === 'function' &&
99
+ !blobCells.some((cell) => cell.target === target && cell.key === key)) {
100
+ blobCells.push({ target, key });
88
101
  }
89
102
  }
90
- if (!blobColumns.length) {
103
+ if (!blobCells.length) {
91
104
  callback(null, row);
92
105
  return;
93
106
  }
94
- let pending = blobColumns.length;
107
+ let pending = blobCells.length;
95
108
  let blobErr;
96
- blobColumns.forEach(function (columnName) {
97
- readblob(row[columnName], function (err, data) {
109
+ blobCells.forEach(function (cell) {
110
+ readblob(cell.target[cell.key], function (err, data) {
98
111
  if (err && !blobErr) {
99
112
  blobErr = err;
100
113
  }
101
- row[columnName] = data;
114
+ cell.target[cell.key] = data;
102
115
  pending--;
103
116
  if (pending === 0) {
104
117
  callback(blobErr, row);
@@ -259,7 +272,9 @@ class Database extends events_1.default.EventEmitter {
259
272
  done = true;
260
273
  next(err);
261
274
  };
262
- fetchBlobSyncRow(row, meta, function (blobErr) {
275
+ // options is read at call time, after the normalization below
276
+ const nest = (0, xsqlvar_1.resolveNestTables)(options, self.connection.options);
277
+ fetchBlobSyncRow(row, meta, nest, self.connection._lowercase_keys, function (blobErr) {
263
278
  if (blobErr) {
264
279
  finish(blobErr);
265
280
  return;
@@ -83,8 +83,10 @@ export declare class XdrReader {
83
83
  frow?: any;
84
84
  /** rows decoded so far in this call */
85
85
  frows?: any[];
86
- /** cached object-row keys (column aliases) */
86
+ /** cached object-row keys (column aliases, qualified when nestTables is set) */
87
87
  fcols?: string[];
88
+ /** cached per-column table keys when nestTables === true */
89
+ ftables?: string[];
88
90
  constructor(buffer: Buffer);
89
91
  readInt(): number;
90
92
  readUInt(): number;
@@ -21,6 +21,45 @@ export declare abstract class SQLVarBase {
21
21
  abstract decode(data: XdrReader, lowerV13: boolean, options?: any): any;
22
22
  abstract calcBlr(blr: BlrWriter): void;
23
23
  }
24
+ /** Effective object-row key(s) of one output column (see computeColumnKeys). */
25
+ export interface ColumnKey {
26
+ /** Top-level table key when nestTables === true; undefined otherwise. */
27
+ table?: string;
28
+ /** Property key: the column alias, or 'table<sep>alias' in separator mode. */
29
+ key: string;
30
+ }
31
+ /**
32
+ * Compute the object-row property keys for a statement's output columns,
33
+ * honouring the nestTables and lowercase_keys options. The table qualifier
34
+ * is the query's relation alias when one is used (relationAlias, requested
35
+ * via isc_info_sql_relation_alias), the relation name otherwise, so
36
+ * self-joins nest under their query aliases. Expression columns (no source
37
+ * relation) qualify as '' exactly like mysql2: they nest under the '' key,
38
+ * and in separator mode become '<sep>alias' — always prefixing keeps
39
+ * qualified keys collision-free (a bare expression alias could otherwise
40
+ * collide with a real column's 'table<sep>column' key). Used by the fetch
41
+ * decoder and by fetchBlobSyncRow, which must agree on where each column
42
+ * landed in the row.
43
+ */
44
+ export declare function computeColumnKeys(output: SQLVarBase[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined): ColumnKey[];
45
+ /**
46
+ * Resolve the effective nestTables value: the per-query option wins over
47
+ * the connection option. The decoder and fetchBlobSyncRow both use this —
48
+ * they must agree on whether nesting is active or blob cells are looked
49
+ * up in the wrong place.
50
+ */
51
+ export declare function resolveNestTables(queryOptions: {
52
+ nestTables?: boolean | string;
53
+ } | undefined, connectionOptions: {
54
+ nestTables?: boolean | string;
55
+ } | undefined): boolean | string | undefined;
56
+ /**
57
+ * The object a column's value lives in: the row itself, or — when the
58
+ * column carries a nestTables table qualifier — the row's per-table
59
+ * sub-object, created on first use. Every site that reads or writes a
60
+ * cell by ColumnKey must resolve it through here.
61
+ */
62
+ export declare function nestCell(row: any, table: string | undefined): any;
24
63
  export declare class SQLVarText extends SQLVarBase {
25
64
  decode(data: XdrReader, lowerV13: boolean, options?: any): string | Buffer<ArrayBufferLike> | null | undefined;
26
65
  calcBlr(blr: BlrWriter): void;
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SQLParamBool = exports.SQLParamDate = exports.SQLParamQuad = exports.SQLParamBuffer = exports.SQLParamString = exports.SQLParamDouble = exports.SQLParamDecFloat34 = exports.SQLParamDecFloat16 = exports.SQLParamInt128 = exports.SQLParamInt64 = exports.SQLParamInt = exports.SQLVarBoolean = exports.SQLVarTimeStampTzEx = exports.SQLVarTimeStampTz = exports.SQLVarTimeTzEx = exports.SQLVarTimeTz = exports.SQLVarTimeStamp = exports.SQLVarTime = exports.SQLVarDate = exports.SQLVarDouble = exports.SQLVarFloat = exports.SQLVarDecFloat34 = exports.SQLVarDecFloat16 = exports.SQLVarInt128 = exports.SQLVarInt64 = exports.SQLVarShort = exports.SQLVarInt = exports.SQLVarArray = exports.SQLVarBlob = exports.SQLVarQuad = exports.SQLVarString = exports.SQLVarNull = exports.SQLVarText = exports.SQLVarBase = void 0;
7
+ exports.computeColumnKeys = computeColumnKeys;
8
+ exports.resolveNestTables = resolveNestTables;
9
+ exports.nestCell = nestCell;
7
10
  exports.encodeDateTimeParts = encodeDateTimeParts;
8
11
  const const_1 = __importDefault(require("./const"));
9
12
  /***************************************
@@ -68,6 +71,62 @@ function resolveTextEncoding(options) {
68
71
  class SQLVarBase {
69
72
  }
70
73
  exports.SQLVarBase = SQLVarBase;
74
+ /**
75
+ * Compute the object-row property keys for a statement's output columns,
76
+ * honouring the nestTables and lowercase_keys options. The table qualifier
77
+ * is the query's relation alias when one is used (relationAlias, requested
78
+ * via isc_info_sql_relation_alias), the relation name otherwise, so
79
+ * self-joins nest under their query aliases. Expression columns (no source
80
+ * relation) qualify as '' exactly like mysql2: they nest under the '' key,
81
+ * and in separator mode become '<sep>alias' — always prefixing keeps
82
+ * qualified keys collision-free (a bare expression alias could otherwise
83
+ * collide with a real column's 'table<sep>column' key). Used by the fetch
84
+ * decoder and by fetchBlobSyncRow, which must agree on where each column
85
+ * landed in the row.
86
+ */
87
+ function computeColumnKeys(output, nestTables, lowercaseKeys) {
88
+ return output.map((column) => {
89
+ let key = column.alias || '';
90
+ if (lowercaseKeys) {
91
+ key = key.toLowerCase();
92
+ }
93
+ if (nestTables !== true && typeof nestTables !== 'string') {
94
+ return { key };
95
+ }
96
+ let table = column.relationAlias || column.relation || '';
97
+ if (lowercaseKeys) {
98
+ table = table.toLowerCase();
99
+ }
100
+ if (nestTables === true) {
101
+ return { table, key };
102
+ }
103
+ return { key: table + nestTables + key };
104
+ });
105
+ }
106
+ /**
107
+ * Resolve the effective nestTables value: the per-query option wins over
108
+ * the connection option. The decoder and fetchBlobSyncRow both use this —
109
+ * they must agree on whether nesting is active or blob cells are looked
110
+ * up in the wrong place.
111
+ */
112
+ function resolveNestTables(queryOptions, connectionOptions) {
113
+ if (queryOptions && queryOptions.nestTables !== undefined) {
114
+ return queryOptions.nestTables;
115
+ }
116
+ return connectionOptions && connectionOptions.nestTables;
117
+ }
118
+ /**
119
+ * The object a column's value lives in: the row itself, or — when the
120
+ * column carries a nestTables table qualifier — the row's per-table
121
+ * sub-object, created on first use. Every site that reads or writes a
122
+ * cell by ColumnKey must resolve it through here.
123
+ */
124
+ function nestCell(row, table) {
125
+ if (table === undefined) {
126
+ return row;
127
+ }
128
+ return row[table] || (row[table] = {});
129
+ }
71
130
  //------------------------------------------------------
72
131
  class SQLVarText extends SQLVarBase {
73
132
  decode(data, lowerV13, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/types.ts CHANGED
@@ -127,6 +127,18 @@ export type QueryOptions = {
127
127
  * it cancels whatever is currently executing on the connection.
128
128
  */
129
129
  signal?: AbortSignal;
130
+ /**
131
+ * Per-query override of the `nestTables` connection option (mysql2
132
+ * semantics). `true` nests each object row by source table:
133
+ * `row[table][column]` — the table key is the query's relation alias
134
+ * when one is used (`FROM emp e` → `row.E`), the table name otherwise,
135
+ * and `''` for expression columns. A string separator flattens keys
136
+ * instead: `nestTables: '_'` → `row.EMP_NAME`; expression columns get
137
+ * the bare separator prefix (`row._ANSWER`, as in mysql2). Keys honour
138
+ * `lowercase_keys`. Object rows only — `db.execute` array rows are
139
+ * unaffected.
140
+ */
141
+ nestTables?: boolean | string;
130
142
  }
131
143
 
132
144
  export type QueryStreamOptions = QueryOptions & {
@@ -314,6 +326,16 @@ export interface Options {
314
326
  * per-query `namedPlaceholders: false` override.
315
327
  */
316
328
  namedPlaceholders?: boolean;
329
+ /**
330
+ * Qualify object-row keys by source table (same option as mysql2), so
331
+ * JOINed columns with the same name stop overwriting each other:
332
+ * `true` nests each row as `row[table][column]`; a string separator
333
+ * flattens to `row['table' + sep + 'column']`. See
334
+ * `QueryOptions.nestTables` for the exact key rules. Applies wherever
335
+ * object rows are produced (query / sequentially / queryStream);
336
+ * array rows (execute) are unaffected. Overridable per query.
337
+ */
338
+ nestTables?: boolean | string;
317
339
  /**
318
340
  * TCP keepalive probing to detect dead/stale connections (same option
319
341
  * names as mysql2). On by default; set false to disable.
@@ -2015,7 +2015,9 @@ class Connection {
2015
2015
  readBlobsSequentially(0, []).then((arrBlob: any) => {
2016
2016
  for (let i = 0; i < arrBlob.length; i++) {
2017
2017
  const blob = arrBlob[i];
2018
- ret.data[blob.row][blob.column] = applyTypeCast(
2018
+ // nestTables === true rows: the value lives in the
2019
+ // per-table sub-object, not on the row itself
2020
+ Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
2019
2021
  statement.connection.options, blob.meta || {},
2020
2022
  parseValueIfJson(blob.value, statement.connection.options));
2021
2023
  }
@@ -2469,6 +2471,7 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2469
2471
  delete data.frow;
2470
2472
  delete data.frows;
2471
2473
  delete data.fcols;
2474
+ delete data.ftables;
2472
2475
 
2473
2476
  if (isOpFetch && data.fop) { // could be set when a packet is not complete
2474
2477
  data.readBuffer(68); // ??
@@ -2490,10 +2493,12 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2490
2493
  data.frows = data.frows || [];
2491
2494
 
2492
2495
  if (custom.asObject && !data.fcols) {
2493
- if (lowercase_keys) {
2494
- data.fcols = output.map((column: any) => column.alias.toLowerCase());
2495
- } else {
2496
- data.fcols = output.map((column: any) => column.alias);
2496
+ const nest = Xsql.resolveNestTables(custom, cnx.options);
2497
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2498
+ data.fcols = columnKeys.map((k) => k.key);
2499
+ if (nest === true) {
2500
+ // computeColumnKeys always sets table when nesting
2501
+ data.ftables = columnKeys.map((k) => k.table!);
2497
2502
  }
2498
2503
  }
2499
2504
 
@@ -2521,7 +2526,10 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2521
2526
 
2522
2527
  if (!lowerV13 && nullBitSet!.get(data.fcolumn)) {
2523
2528
  const nullKey = custom.asObject ? data.fcols![data.fcolumn!] : data.fcolumn;
2524
- data.frow[nullKey] = applyTypeCast(cnx.options, item, null);
2529
+ // ftables is only set when nestTables === true, so
2530
+ // the default path writes straight into the row
2531
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn!]) : data.frow)[nullKey] =
2532
+ applyTypeCast(cnx.options, item, null);
2525
2533
 
2526
2534
  continue;
2527
2535
  }
@@ -2538,7 +2546,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2538
2546
 
2539
2547
  if (item.type === Const.SQL_BLOB && value !== null) {
2540
2548
  if (item.subType === Const.isc_blob_text && cnx.options.blobAsText) {
2541
- value = fetch_blob_async_transaction(statement, value, key, row, item);
2549
+ value = fetch_blob_async_transaction(statement, value, key, row, item,
2550
+ data.ftables && data.ftables[data.fcolumn!]);
2542
2551
  arrBlob.push(value);
2543
2552
  pendingTextBlob = true;
2544
2553
  } else {
@@ -2546,7 +2555,7 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2546
2555
  }
2547
2556
  }
2548
2557
 
2549
- data.frow[key] = pendingTextBlob
2558
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn!]) : data.frow)[key] = pendingTextBlob
2550
2559
  ? value
2551
2560
  : applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
2552
2561
  } catch (e) {
@@ -3475,8 +3484,8 @@ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
3475
3484
  blr.addByte(Const.blr_eoc);
3476
3485
  }
3477
3486
 
3478
- function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase) {
3479
- const infoValue = { row, column, value: '', meta };
3487
+ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string) {
3488
+ const infoValue = { row, column, value: '', meta, table };
3480
3489
 
3481
3490
  return (transactionArg: any) => {
3482
3491
  const cacheKey = `${id.high}:${id.low}`;
package/src/wire/const.ts CHANGED
@@ -626,6 +626,7 @@ const DESCRIBE = [
626
626
  sqlInfo.isc_info_sql_length,
627
627
  sqlInfo.isc_info_sql_field,
628
628
  sqlInfo.isc_info_sql_relation,
629
+ sqlInfo.isc_info_sql_relation_alias, // FB 2.0+: query alias of the source relation (nestTables)
629
630
  //isc_info_sql_owner,
630
631
  sqlInfo.isc_info_sql_alias,
631
632
  sqlInfo.isc_info_sql_describe_end,
@@ -654,6 +655,7 @@ const DESCRIBE_WITH_SCHEMA = [
654
655
  sqlInfo.isc_info_sql_field,
655
656
  sqlInfo.isc_info_sql_relation,
656
657
  sqlInfo.isc_info_sql_relation_schema, // FB 6.0: schema of source relation
658
+ sqlInfo.isc_info_sql_relation_alias, // query alias of the source relation (nestTables)
657
659
  //isc_info_sql_owner,
658
660
  sqlInfo.isc_info_sql_alias,
659
661
  sqlInfo.isc_info_sql_describe_end,
@@ -2,6 +2,7 @@ import Events from 'events';
2
2
  import { doError, fromCallback, type Callback, type SimpleCallback } from '../callback';
3
3
  import { escape } from '../utils';
4
4
  import Const from './const';
5
+ import { computeColumnKeys, nestCell, resolveNestTables } from './xsqlvar';
5
6
  import EventConnection from './eventConnection';
6
7
  import FbEventManager from './fbEventManager';
7
8
  import makeQueryStream from './query-stream';
@@ -94,35 +95,47 @@ function readblob(blob: any, callback: Callback): void {
94
95
  });
95
96
  }
96
97
 
97
- function fetchBlobSyncRow(row: any, meta: any[], callback: Callback): void {
98
- if (!row || !meta || !meta.length) {
98
+ function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, callback: Callback): void {
99
+ if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === Const.SQL_BLOB)) {
99
100
  callback(null, row);
100
101
  return;
101
102
  }
102
103
 
103
- const rowKeys = Object.keys(row);
104
- const blobColumns: string[] = [];
104
+ // locate blob cells by the same key computation the fetch decoder used,
105
+ // rather than assuming Object.keys(row) is index-aligned with meta —
106
+ // duplicate JOIN column names (and nested rows) break that alignment.
107
+ // Array rows (sequentially's legacy boolean form) are keyed by index.
108
+ const isArrayRow = Array.isArray(row);
109
+ const keys = isArrayRow ? null : computeColumnKeys(meta, nestTables, lowercaseKeys);
110
+ const blobCells: { target: any; key: string | number }[] = [];
105
111
 
106
112
  for (let i = 0; i < meta.length; i++) {
107
- if (meta[i] && meta[i].type === Const.SQL_BLOB && rowKeys[i] !== undefined) {
108
- blobColumns.push(rowKeys[i]);
113
+ if (!meta[i] || meta[i].type !== Const.SQL_BLOB) {
114
+ continue;
115
+ }
116
+ const target = keys ? nestCell(row, keys[i].table) : row;
117
+ const key = keys ? keys[i].key : i;
118
+ // duplicate aliases collapse onto one cell — read it only once
119
+ if (typeof target[key] === 'function' &&
120
+ !blobCells.some((cell) => cell.target === target && cell.key === key)) {
121
+ blobCells.push({ target, key });
109
122
  }
110
123
  }
111
124
 
112
- if (!blobColumns.length) {
125
+ if (!blobCells.length) {
113
126
  callback(null, row);
114
127
  return;
115
128
  }
116
129
 
117
- let pending = blobColumns.length;
130
+ let pending = blobCells.length;
118
131
  let blobErr: any;
119
132
 
120
- blobColumns.forEach(function(columnName) {
121
- readblob(row[columnName], function(err: any, data: any) {
133
+ blobCells.forEach(function(cell) {
134
+ readblob(cell.target[cell.key], function(err: any, data: any) {
122
135
  if (err && !blobErr) {
123
136
  blobErr = err;
124
137
  }
125
- row[columnName] = data;
138
+ cell.target[cell.key] = data;
126
139
  pending--;
127
140
  if (pending === 0) {
128
141
  callback(blobErr, row);
@@ -322,7 +335,9 @@ class Database extends Events.EventEmitter {
322
335
  next(err);
323
336
  };
324
337
 
325
- fetchBlobSyncRow(row, meta, function(blobErr: any) {
338
+ // options is read at call time, after the normalization below
339
+ const nest = resolveNestTables(options as any, self.connection.options);
340
+ fetchBlobSyncRow(row, meta, nest, self.connection._lowercase_keys, function(blobErr: any) {
326
341
  if (blobErr) {
327
342
  finish(blobErr);
328
343
  return;
@@ -440,8 +440,10 @@ export class XdrReader {
440
440
  frow?: any;
441
441
  /** rows decoded so far in this call */
442
442
  frows?: any[];
443
- /** cached object-row keys (column aliases) */
443
+ /** cached object-row keys (column aliases, qualified when nestTables is set) */
444
444
  fcols?: string[];
445
+ /** cached per-column table keys when nestTables === true */
446
+ ftables?: string[];
445
447
 
446
448
  constructor(buffer: Buffer) {
447
449
  this.buffer = buffer;
@@ -93,6 +93,82 @@ export abstract class SQLVarBase {
93
93
 
94
94
  //------------------------------------------------------
95
95
 
96
+ /** Effective object-row key(s) of one output column (see computeColumnKeys). */
97
+ export interface ColumnKey {
98
+ /** Top-level table key when nestTables === true; undefined otherwise. */
99
+ table?: string;
100
+ /** Property key: the column alias, or 'table<sep>alias' in separator mode. */
101
+ key: string;
102
+ }
103
+
104
+ /**
105
+ * Compute the object-row property keys for a statement's output columns,
106
+ * honouring the nestTables and lowercase_keys options. The table qualifier
107
+ * is the query's relation alias when one is used (relationAlias, requested
108
+ * via isc_info_sql_relation_alias), the relation name otherwise, so
109
+ * self-joins nest under their query aliases. Expression columns (no source
110
+ * relation) qualify as '' exactly like mysql2: they nest under the '' key,
111
+ * and in separator mode become '<sep>alias' — always prefixing keeps
112
+ * qualified keys collision-free (a bare expression alias could otherwise
113
+ * collide with a real column's 'table<sep>column' key). Used by the fetch
114
+ * decoder and by fetchBlobSyncRow, which must agree on where each column
115
+ * landed in the row.
116
+ */
117
+ export function computeColumnKeys(
118
+ output: SQLVarBase[],
119
+ nestTables: boolean | string | undefined,
120
+ lowercaseKeys: boolean | undefined
121
+ ): ColumnKey[] {
122
+ return output.map((column) => {
123
+ let key = column.alias || '';
124
+ if (lowercaseKeys) {
125
+ key = key.toLowerCase();
126
+ }
127
+ if (nestTables !== true && typeof nestTables !== 'string') {
128
+ return { key };
129
+ }
130
+ let table = column.relationAlias || column.relation || '';
131
+ if (lowercaseKeys) {
132
+ table = table.toLowerCase();
133
+ }
134
+ if (nestTables === true) {
135
+ return { table, key };
136
+ }
137
+ return { key: table + nestTables + key };
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Resolve the effective nestTables value: the per-query option wins over
143
+ * the connection option. The decoder and fetchBlobSyncRow both use this —
144
+ * they must agree on whether nesting is active or blob cells are looked
145
+ * up in the wrong place.
146
+ */
147
+ export function resolveNestTables(
148
+ queryOptions: { nestTables?: boolean | string } | undefined,
149
+ connectionOptions: { nestTables?: boolean | string } | undefined
150
+ ): boolean | string | undefined {
151
+ if (queryOptions && queryOptions.nestTables !== undefined) {
152
+ return queryOptions.nestTables;
153
+ }
154
+ return connectionOptions && connectionOptions.nestTables;
155
+ }
156
+
157
+ /**
158
+ * The object a column's value lives in: the row itself, or — when the
159
+ * column carries a nestTables table qualifier — the row's per-table
160
+ * sub-object, created on first use. Every site that reads or writes a
161
+ * cell by ColumnKey must resolve it through here.
162
+ */
163
+ export function nestCell(row: any, table: string | undefined) {
164
+ if (table === undefined) {
165
+ return row;
166
+ }
167
+ return row[table] || (row[table] = {});
168
+ }
169
+
170
+ //------------------------------------------------------
171
+
96
172
  export class SQLVarText extends SQLVarBase {
97
173
  decode(data: XdrReader, lowerV13: boolean, options?: any) {
98
174
  let ret;