node-firebird 2.10.0 → 2.12.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.
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /***************************************
3
+ *
4
+ * Tagged-template query API (Postgres.js-style)
5
+ *
6
+ * db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
7
+ * db.sql('COLUMN NAME') → quoted identifier
8
+ *
9
+ * Interpolated values become positional `?` parameters — never string
10
+ * concatenation — so the API is injection-safe by construction. A query
11
+ * embedded inside another tag is treated as a fragment: its text and
12
+ * parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
13
+ * IN clauses. Execution is lazy (on await/then) and happens exactly once.
14
+ *
15
+ ***************************************/
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.SqlQuery = exports.SqlIdentifier = void 0;
18
+ exports.quoteIdentifier = quoteIdentifier;
19
+ exports.makeSqlTag = makeSqlTag;
20
+ /** A dynamically quoted identifier produced by sql('name'). */
21
+ class SqlIdentifier {
22
+ constructor(name) {
23
+ this.name = name;
24
+ }
25
+ }
26
+ exports.SqlIdentifier = SqlIdentifier;
27
+ /**
28
+ * Quote a (possibly dot-qualified) identifier for dialect 3: each part is
29
+ * wrapped in double quotes with embedded quotes doubled, so user input can
30
+ * never break out of the identifier position.
31
+ */
32
+ function quoteIdentifier(name) {
33
+ return String(name)
34
+ .split('.')
35
+ .map((part) => '"' + part.replace(/"/g, '""') + '"')
36
+ .join('.');
37
+ }
38
+ function compile(strings, values, active) {
39
+ let text = '';
40
+ const params = [];
41
+ for (let i = 0; i < strings.length; i++) {
42
+ text += strings[i];
43
+ if (i >= values.length) {
44
+ continue;
45
+ }
46
+ const value = values[i];
47
+ if (value instanceof SqlIdentifier) {
48
+ text += quoteIdentifier(value.name);
49
+ }
50
+ else if (value instanceof SqlQuery) {
51
+ // embedded fragment: splice its text and params in place. The
52
+ // same fragment may appear several times (a DAG), but a fragment
53
+ // containing itself would recurse forever — track the expansion
54
+ // stack and reject cycles with a diagnosable error.
55
+ active = active || new Set();
56
+ if (active.has(value)) {
57
+ throw new Error('circular sql fragment: a query is embedded (transitively) inside itself');
58
+ }
59
+ active.add(value);
60
+ const inner = compile(value.strings, value.values, active);
61
+ active.delete(value);
62
+ text += inner.text;
63
+ params.push(...inner.params);
64
+ }
65
+ else if (Array.isArray(value)) {
66
+ // IN (${[1, 2, 3]}) → IN (?, ?, ?)
67
+ if (!value.length) {
68
+ // '' would compile to `IN ()` — invalid SQL raising a server
69
+ // syntax error the caller never wrote; fail early instead
70
+ throw new Error('cannot interpolate an empty array (would compile to invalid SQL like "IN ()")');
71
+ }
72
+ text += value.map(() => '?').join(', ');
73
+ params.push(...value);
74
+ }
75
+ else {
76
+ text += '?';
77
+ params.push(value);
78
+ }
79
+ }
80
+ return { text, params };
81
+ }
82
+ /**
83
+ * A lazily executed tagged query. Awaiting it (or calling then/catch/
84
+ * finally) runs it through the owning Database/Transaction exactly once;
85
+ * embedding it in another tag uses it as a fragment instead and never
86
+ * executes it.
87
+ */
88
+ class SqlQuery {
89
+ constructor(executor, strings, values) {
90
+ this.executor = executor;
91
+ this.strings = strings;
92
+ this.values = values;
93
+ }
94
+ /** The compiled SQL text (`?` placeholders) and parameter array. */
95
+ toQuery() {
96
+ return compile(this.strings, this.values);
97
+ }
98
+ /**
99
+ * Attach per-query options (timeout, signal, nestTables, …). Must be
100
+ * called before the query executes — options attached afterwards would
101
+ * be silently ignored, so that throws instead.
102
+ */
103
+ options(queryOptions) {
104
+ if (this.executed) {
105
+ throw new Error('sql query already executed — call .options() before awaiting it');
106
+ }
107
+ this.queryOptions = { ...this.queryOptions, ...queryOptions };
108
+ return this;
109
+ }
110
+ /** Execute resolving the full { rows, fields, affectedRows, … } result. */
111
+ withMeta() {
112
+ return this.run(true);
113
+ }
114
+ /**
115
+ * A query executes exactly once, in the shape of its first consumer
116
+ * (plain rows via then/await, or the full result via withMeta).
117
+ * Consuming it again in the OTHER shape cannot be honoured from the
118
+ * cached promise, so it throws rather than silently returning the
119
+ * wrong shape.
120
+ */
121
+ run(withMeta) {
122
+ if (this.executed) {
123
+ if (withMeta !== this.executedMeta) {
124
+ throw new Error(this.executedMeta
125
+ ? 'sql query already executed via .withMeta() — await that result instead of the query'
126
+ : 'sql query already executed as plain rows — call .withMeta() first, or build a new query');
127
+ }
128
+ return this.executed;
129
+ }
130
+ this.executedMeta = withMeta;
131
+ const { text, params } = compile(this.strings, this.values);
132
+ const options = withMeta ? { ...this.queryOptions, withMeta: true } : this.queryOptions;
133
+ this.executed = this.executor(text, params, options);
134
+ return this.executed;
135
+ }
136
+ then(onfulfilled, onrejected) {
137
+ return this.run(false).then(onfulfilled, onrejected);
138
+ }
139
+ catch(onrejected) {
140
+ return this.then(undefined, onrejected);
141
+ }
142
+ finally(onfinally) {
143
+ return this.run(false).finally(onfinally);
144
+ }
145
+ }
146
+ exports.SqlQuery = SqlQuery;
147
+ /**
148
+ * Build the `sql` tag for a Database/Transaction. `executor` receives the
149
+ * compiled text, params and per-query options and must return a promise
150
+ * (Database/Transaction pass their queryAsync).
151
+ */
152
+ function makeSqlTag(executor) {
153
+ return function sql(first, ...values) {
154
+ if (Array.isArray(first) && Object.prototype.hasOwnProperty.call(first, 'raw')) {
155
+ return new SqlQuery(executor, first, values);
156
+ }
157
+ if (typeof first === 'string') {
158
+ return new SqlIdentifier(first);
159
+ }
160
+ throw new Error('sql must be used as a template tag (sql`...`) or called with an identifier string (sql(\'NAME\'))');
161
+ };
162
+ }
package/lib/types.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Readable } from 'stream';
2
+ import type { SqlTag } from './sql-template';
3
+ export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
2
4
  export type DatabaseCallback = (err: any, db: Database) => void;
3
5
  export type TransactionCallback = (err: any, transaction: Transaction) => void;
4
6
  export type QueryCallback = (err: any, result: any[]) => void;
@@ -115,7 +117,82 @@ export type QueryOptions = {
115
117
  * it cancels whatever is currently executing on the connection.
116
118
  */
117
119
  signal?: AbortSignal;
120
+ /**
121
+ * Per-query override of the `nestTables` connection option (mysql2
122
+ * semantics). `true` nests each object row by source table:
123
+ * `row[table][column]` — the table key is the query's relation alias
124
+ * when one is used (`FROM emp e` → `row.E`), the table name otherwise,
125
+ * and `''` for expression columns. A string separator flattens keys
126
+ * instead: `nestTables: '_'` → `row.EMP_NAME`; expression columns get
127
+ * the bare separator prefix (`row._ANSWER`, as in mysql2). Keys honour
128
+ * `lowercase_keys`. Object rows only — `db.execute` array rows are
129
+ * unaffected.
130
+ */
131
+ nestTables?: boolean | string;
132
+ /**
133
+ * Per-query override of the `transformKeys` connection option: rewrite
134
+ * object-row keys — `'camel'` turns `FIRST_NAME` into `firstName`, or
135
+ * pass a custom `(key) => key` mapper. Applied after `lowercase_keys`
136
+ * and to both parts of `nestTables` keys. Column metadata (`fields`,
137
+ * typeCast) keeps the raw server aliases.
138
+ */
139
+ transformKeys?: 'camel' | ((key: string) => string);
140
+ /**
141
+ * Deliver a full result object `{ rows, fields, affectedRows,
142
+ * recordCounts, warnings }` instead of the bare rows (callback and
143
+ * promise APIs). For DML, `affectedRows` is what the server actually
144
+ * changed (`isc_info_sql_records`, one extra lightweight info request
145
+ * per statement — hence opt-in) and `recordCounts` breaks it down per
146
+ * verb; for SELECT it is the number of rows returned (pg's `rowCount`
147
+ * convention) with no extra round-trip. `warnings` carries any
148
+ * `isc_arg_warning` entries from the execute response. Honoured by
149
+ * query/execute and their *Async wrappers only — ignored by the
150
+ * streaming APIs (sequentially/queryStream, where rows bypass the
151
+ * result) and executeBatch (which has its own completion shape).
152
+ */
153
+ withMeta?: boolean;
118
154
  };
155
+ /** Column metadata delivered in withMeta results (`fields`) — the same
156
+ * vocabulary the typeCast hook receives, plus nullable and the relation
157
+ * alias/schema. */
158
+ export interface FieldMetadata {
159
+ type: number;
160
+ typeName: string;
161
+ subType?: number;
162
+ scale?: number;
163
+ length?: number;
164
+ nullable?: boolean;
165
+ field?: string;
166
+ relation?: string;
167
+ relationAlias?: string;
168
+ relationSchema?: string;
169
+ alias?: string;
170
+ }
171
+ /** Per-verb server row counts of an executed DML statement. */
172
+ export interface RecordCounts {
173
+ selectCount: number;
174
+ insertCount: number;
175
+ updateCount: number;
176
+ deleteCount: number;
177
+ }
178
+ /** An isc_arg_warning entry from a server response ('warning' driver event
179
+ * and withMeta `warnings`). */
180
+ export interface ServerWarning {
181
+ gdscode: number;
182
+ params?: (string | number)[];
183
+ message: string;
184
+ }
185
+ /** Full result shape delivered when `withMeta: true` is set. */
186
+ export interface QueryResult<T = any> {
187
+ /** Rows array (SELECT), single row object (RETURNING / procedures), or undefined (plain DML). */
188
+ rows: T[] | T | undefined;
189
+ fields: FieldMetadata[];
190
+ /** DML: rows the server changed; SELECT: rows returned. */
191
+ affectedRows: number;
192
+ /** Set for DML statements only. */
193
+ recordCounts?: RecordCounts;
194
+ warnings: ServerWarning[];
195
+ }
119
196
  export type QueryStreamOptions = QueryOptions & {
120
197
  /**
121
198
  * Rows buffered internally before fetching pauses (object-mode
@@ -126,6 +203,13 @@ export type QueryStreamOptions = QueryOptions & {
126
203
  asObject?: boolean;
127
204
  };
128
205
  export interface Database {
206
+ /**
207
+ * Tagged-template query API (Postgres.js-style): interpolated values
208
+ * become positional parameters, `sql('NAME')` quotes an identifier,
209
+ * embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
210
+ * The returned query is a lazy thenable — it executes once, on await.
211
+ */
212
+ sql: SqlTag;
129
213
  detach(callback?: SimpleCallback): Database;
130
214
  transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
131
215
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
@@ -148,7 +232,13 @@ export interface Database {
148
232
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
149
233
  dropTablespace(name: string, callback?: QueryCallback): Database;
150
234
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
235
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
236
+ withMeta: true;
237
+ }): Promise<QueryResult<T>>;
151
238
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
239
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
240
+ withMeta: true;
241
+ }): Promise<QueryResult<T>>;
152
242
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
153
243
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
154
244
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -171,6 +261,14 @@ export interface Database {
171
261
  cancelAsync(kind?: number): Promise<void>;
172
262
  }
173
263
  export interface Transaction {
264
+ /** Tagged-template query API running inside this transaction (see Database.sql). */
265
+ sql: SqlTag;
266
+ /**
267
+ * Run `work` inside a savepoint: released on resolve, rolled back TO
268
+ * (undoing only work's changes) on reject — the transaction stays
269
+ * usable either way. Nestable.
270
+ */
271
+ savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
174
272
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
175
273
  query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
176
274
  execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
@@ -187,7 +285,13 @@ export interface Transaction {
187
285
  commitRetaining(callback?: SimpleCallback): void;
188
286
  rollback(callback?: SimpleCallback): void;
189
287
  rollbackRetaining(callback?: SimpleCallback): void;
288
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
289
+ withMeta: true;
290
+ }): Promise<QueryResult<T>>;
190
291
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
292
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
293
+ withMeta: true;
294
+ }): Promise<QueryResult<T>>;
191
295
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
192
296
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
193
297
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -254,6 +358,24 @@ export interface Options {
254
358
  * per-query `namedPlaceholders: false` override.
255
359
  */
256
360
  namedPlaceholders?: boolean;
361
+ /**
362
+ * Qualify object-row keys by source table (same option as mysql2), so
363
+ * JOINed columns with the same name stop overwriting each other:
364
+ * `true` nests each row as `row[table][column]`; a string separator
365
+ * flattens to `row['table' + sep + 'column']`. See
366
+ * `QueryOptions.nestTables` for the exact key rules. Applies wherever
367
+ * object rows are produced (query / sequentially / queryStream);
368
+ * array rows (execute) are unaffected. Overridable per query.
369
+ */
370
+ nestTables?: boolean | string;
371
+ /**
372
+ * Rewrite object-row keys (Postgres.js `transform` counterpart):
373
+ * `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
374
+ * `(key) => key` mapper. Applied after `lowercase_keys` and to both
375
+ * parts of `nestTables` keys; column metadata keeps raw aliases.
376
+ * Overridable per query.
377
+ */
378
+ transformKeys?: 'camel' | ((key: string) => string);
257
379
  /**
258
380
  * TCP keepalive probing to detect dead/stale connections (same option
259
381
  * names as mysql2). On by default; set false to disable.
@@ -292,6 +414,20 @@ export interface Options {
292
414
  * Default 0 (idle connections are kept forever).
293
415
  */
294
416
  idleTimeoutMillis?: number;
417
+ /**
418
+ * Pool only: retire a physical connection after this many checkouts
419
+ * (pg's `maxUses`) — it is closed for good when returned to the pool
420
+ * and replaced on demand. Bounds server-side resource drift on
421
+ * long-lived connections. Default 0 (unlimited uses).
422
+ */
423
+ maxUses?: number;
424
+ /**
425
+ * Pool only: retire a physical connection this many milliseconds after
426
+ * it was created (Postgres.js's `max_lifetime`), on return to the pool
427
+ * or by the idle sweep — even below `min`; replacements are created on
428
+ * demand. Default 0 (unlimited lifetime).
429
+ */
430
+ maxLifetimeMillis?: number;
295
431
  /**
296
432
  * **Firebird 6.0+ only (Protocol 20+)**
297
433
  *
package/lib/uri.js CHANGED
@@ -187,7 +187,58 @@ function parseConnectionString(str) {
187
187
  */
188
188
  function normalizeOptions(options) {
189
189
  if (typeof options === 'string') {
190
- return parseConnectionString(options);
190
+ options = parseConnectionString(options);
191
191
  }
192
- return options;
192
+ return applyEnvDefaults(options);
193
+ }
194
+ /**
195
+ * Fall back to environment variables for connection settings the caller
196
+ * did not provide — the pg-style convention using Firebird's own names:
197
+ * ISC_USER / ISC_PASSWORD (honoured by isql and every official tool) plus
198
+ * FIREBIRD_HOST / FIREBIRD_PORT / FIREBIRD_DATABASE / FIREBIRD_ROLE.
199
+ * Explicit options always win; the driver's built-in defaults (SYSDBA /
200
+ * masterkey / 127.0.0.1) still apply when neither is set. A fresh object
201
+ * is returned so caller-owned options objects are never mutated.
202
+ */
203
+ const ENV_FALLBACKS = [
204
+ ['user', 'ISC_USER'],
205
+ ['password', 'ISC_PASSWORD'],
206
+ ['host', 'FIREBIRD_HOST'],
207
+ ['port', 'FIREBIRD_PORT'],
208
+ ['database', 'FIREBIRD_DATABASE'],
209
+ ['role', 'FIREBIRD_ROLE'],
210
+ ];
211
+ function applyEnvDefaults(options) {
212
+ let out = options;
213
+ for (const [key, envName] of ENV_FALLBACKS) {
214
+ const value = process.env[envName];
215
+ // empty-string env vars (common in CI: `export ISC_PASSWORD=`)
216
+ // count as unset
217
+ if (value === undefined || value === '') {
218
+ continue;
219
+ }
220
+ // a service-manager connection's `database` selects the TARGET of
221
+ // backup/restore — never let a leftover env var pick that silently
222
+ if (key === 'database' && options.manager) {
223
+ continue;
224
+ }
225
+ if (out[key] === undefined || out[key] === null || out[key] === '') {
226
+ if (out === options) {
227
+ out = { ...options };
228
+ }
229
+ if (key === 'port') {
230
+ const port = Number(value);
231
+ if (!Number.isFinite(port) || port <= 0) {
232
+ // NaN is falsy: it would silently fall back to 3050
233
+ // downstream instead of surfacing the typo
234
+ throw new Error('Invalid FIREBIRD_PORT environment variable: ' + value);
235
+ }
236
+ out[key] = port;
237
+ }
238
+ else {
239
+ out[key] = value;
240
+ }
241
+ }
242
+ }
243
+ return out;
193
244
  }
@@ -132,6 +132,12 @@ declare class Connection {
132
132
  /** `count` may be the callback itself when no fetch size is given. */
133
133
  fetch(statement: Statement, transaction: Transaction, count: any, callback?: QueueCallback): void;
134
134
  fetchScroll(statement: Statement, transaction: Transaction, direction: string | number, offset: any, count: any, callback?: QueueCallback): void;
135
+ /**
136
+ * Query runtime information about a prepared statement via op_info_sql
137
+ * (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
138
+ * response is a plain op_response whose buffer holds the info clusters.
139
+ */
140
+ statementInfo(statement: Statement, items: number[], callback?: QueueCallback): this | undefined;
135
141
  fetchAll(statement: Statement, transaction: Transaction, callback: Callback<any[]>): void;
136
142
  openBlob(blob: Quad, transaction: Transaction, callback: QueueCallback): void;
137
143
  closeBlob(blob: any, callback?: QueueCallback, defer?: boolean): void;
@@ -150,6 +156,6 @@ declare function decodeResponse(data: XdrReader, callback: QueueCallback | undef
150
156
  error: Error;
151
157
  };
152
158
  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>;
159
+ 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
160
  declare function fetch_blob_async(statement: Statement, id: Quad, name: string | number, row: number): (transaction: Transaction, callback: any) => void;
155
161
  export = Connection;
@@ -107,31 +107,8 @@ function statementCacheLimit(options) {
107
107
  }
108
108
  return 0;
109
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
- };
110
+ // SQL type-code names live in xsqlvar.ts alongside the descriptors
111
+ const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
135
112
  /**
136
113
  * Run the user's typeCast hook (options.typeCast) for one column value.
137
114
  * The hook receives the column metadata and a next() returning the value
@@ -145,16 +122,7 @@ function applyTypeCast(options, meta, defaultValue) {
145
122
  if (typeof typeCast !== 'function') {
146
123
  return defaultValue;
147
124
  }
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
- };
125
+ const column = Xsql.describeField(meta);
158
126
  // A hook exception must never escape into the row-decode loop: there it
159
127
  // would be mistaken for an incomplete packet and desync the response
160
128
  // queue (the same failure mode as issue #341). Fall back to the default
@@ -396,6 +364,30 @@ class Connection {
396
364
  if (process.env.FIREBIRD_DEBUG) {
397
365
  console.log('[fb-debug] response dispatched: queue remaining=%d pending remaining=%d xdr.pos=%d', self._queue.length, self._pending.length, xdr.pos);
398
366
  }
367
+ // Surface isc_arg_warning entries (parsed since 2.10.0 but
368
+ // dropped here): resolve their message text and emit them on
369
+ // the Database on the next tick, so a listener registered
370
+ // inside this very response's callback (e.g. right after
371
+ // attach) still receives them.
372
+ if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
373
+ const warnings = obj.warnings;
374
+ for (const w of warnings) {
375
+ if (w.message === undefined) {
376
+ w.message = (0, utils_1.lookupMessages)([w]);
377
+ if (!w.message || w.message === 'Unknow error') {
378
+ // codes newer than the bundled firebird.msg:
379
+ // still say something actionable
380
+ w.message = 'Firebird warning ' + w.gdscode +
381
+ (w.params && w.params.length ? ': ' + w.params.join(', ') : '');
382
+ }
383
+ }
384
+ }
385
+ process.nextTick(function () {
386
+ for (const w of warnings) {
387
+ self.db.emit('warning', w);
388
+ }
389
+ });
390
+ }
399
391
  if (obj && obj.status) {
400
392
  obj.message = (0, utils_1.lookupMessages)(obj.status);
401
393
  (0, callback_1.doCallback)(obj, cb);
@@ -1670,6 +1662,27 @@ class Connection {
1670
1662
  callback.statement = statement;
1671
1663
  this._queueEvent(callback);
1672
1664
  }
1665
+ /**
1666
+ * Query runtime information about a prepared statement via op_info_sql
1667
+ * (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
1668
+ * response is a plain op_response whose buffer holds the info clusters.
1669
+ */
1670
+ statementInfo(statement, items, callback) {
1671
+ if (this._isClosed)
1672
+ return this.throwClosed(callback);
1673
+ this._pending.push('statementInfo');
1674
+ var msg = this._msg;
1675
+ var blr = this._blr;
1676
+ msg.pos = 0;
1677
+ blr.pos = 0;
1678
+ blr.addBytes(items);
1679
+ msg.addInt(const_1.default.op_info_sql);
1680
+ msg.addInt(statement.handle);
1681
+ msg.addInt(0); // incarnation
1682
+ msg.addBlr(blr);
1683
+ msg.addInt(65535); // buffer_length
1684
+ this._queueEvent(callback);
1685
+ }
1673
1686
  fetchAll(statement, transaction, callback) {
1674
1687
  const self = this;
1675
1688
  const custom = statement.options || {};
@@ -1699,7 +1712,9 @@ class Connection {
1699
1712
  readBlobsSequentially(0, []).then((arrBlob) => {
1700
1713
  for (let i = 0; i < arrBlob.length; i++) {
1701
1714
  const blob = arrBlob[i];
1702
- ret.data[blob.row][blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
1715
+ // nestTables === true rows: the value lives in the
1716
+ // per-table sub-object, not on the row itself
1717
+ Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
1703
1718
  }
1704
1719
  doSynchronousLoop(ret.data, (row, _i, next) => {
1705
1720
  const pos = asStream ? streamIndex++ : (data.push(row) - 1);
@@ -2085,6 +2100,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2085
2100
  delete data.frow;
2086
2101
  delete data.frows;
2087
2102
  delete data.fcols;
2103
+ delete data.ftables;
2088
2104
  if (isOpFetch && data.fop) { // could be set when a packet is not complete
2089
2105
  data.readBuffer(68); // ??
2090
2106
  op = data.readInt(); // ??
@@ -2102,11 +2118,13 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2102
2118
  data.frow = data.frow || (custom.asObject ? {} : new Array(output.length));
2103
2119
  data.frows = data.frows || [];
2104
2120
  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);
2121
+ const nest = Xsql.resolveNestTables(custom, cnx.options);
2122
+ const transform = Xsql.resolveKeyTransform(custom, cnx.options);
2123
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
2124
+ data.fcols = columnKeys.map((k) => k.key);
2125
+ if (nest === true) {
2126
+ // computeColumnKeys always sets table when nesting
2127
+ data.ftables = columnKeys.map((k) => k.table);
2110
2128
  }
2111
2129
  }
2112
2130
  const arrBlob = [];
@@ -2129,7 +2147,10 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2129
2147
  item = output[data.fcolumn];
2130
2148
  if (!lowerV13 && nullBitSet.get(data.fcolumn)) {
2131
2149
  const nullKey = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
2132
- data.frow[nullKey] = applyTypeCast(cnx.options, item, null);
2150
+ // ftables is only set when nestTables === true, so
2151
+ // the default path writes straight into the row
2152
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn]) : data.frow)[nullKey] =
2153
+ applyTypeCast(cnx.options, item, null);
2133
2154
  continue;
2134
2155
  }
2135
2156
  try {
@@ -2143,7 +2164,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2143
2164
  let pendingTextBlob = false;
2144
2165
  if (item.type === const_1.default.SQL_BLOB && value !== null) {
2145
2166
  if (item.subType === const_1.default.isc_blob_text && cnx.options.blobAsText) {
2146
- value = fetch_blob_async_transaction(statement, value, key, row, item);
2167
+ value = fetch_blob_async_transaction(statement, value, key, row, item, data.ftables && data.ftables[data.fcolumn]);
2147
2168
  arrBlob.push(value);
2148
2169
  pendingTextBlob = true;
2149
2170
  }
@@ -2151,7 +2172,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2151
2172
  value = fetch_blob_async(statement, value, key, row);
2152
2173
  }
2153
2174
  }
2154
- data.frow[key] = pendingTextBlob
2175
+ (data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn]) : data.frow)[key] = pendingTextBlob
2155
2176
  ? value
2156
2177
  : applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
2157
2178
  }
@@ -3019,8 +3040,8 @@ function CalcBlr(blr, xsqlda) {
3019
3040
  blr.addByte(const_1.default.blr_end);
3020
3041
  blr.addByte(const_1.default.blr_eoc);
3021
3042
  }
3022
- function fetch_blob_async_transaction(statement, id, column, row, meta) {
3023
- const infoValue = { row, column, value: '', meta };
3043
+ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
3044
+ const infoValue = { row, column, value: '', meta, table };
3024
3045
  return (transactionArg) => {
3025
3046
  const cacheKey = `${id.high}:${id.low}`;
3026
3047
  if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
@@ -404,6 +404,10 @@ declare const Const: Readonly<{
404
404
  isc_info_sql_stmt_type: number;
405
405
  isc_info_sql_get_plan: number;
406
406
  isc_info_sql_records: number;
407
+ isc_info_req_select_count: number;
408
+ isc_info_req_insert_count: number;
409
+ isc_info_req_update_count: number;
410
+ isc_info_req_delete_count: number;
407
411
  isc_info_sql_batch_fetch: number;
408
412
  isc_info_sql_relation_alias: number;
409
413
  isc_info_sql_explain_plan: number;
@@ -589,6 +593,7 @@ declare const Const: Readonly<{
589
593
  isc_spb_trc_cfg: number;
590
594
  DESCRIBE: number[];
591
595
  DESCRIBE_WITH_SCHEMA: number[];
596
+ RECORDS_INFO: number[];
592
597
  SUPPORTED_PROTOCOL: number[][];
593
598
  }>;
594
599
  export = Const;