node-firebird 2.11.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,196 @@
1
+ /***************************************
2
+ *
3
+ * Tagged-template query API (Postgres.js-style)
4
+ *
5
+ * db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
6
+ * db.sql('COLUMN NAME') → quoted identifier
7
+ *
8
+ * Interpolated values become positional `?` parameters — never string
9
+ * concatenation — so the API is injection-safe by construction. A query
10
+ * embedded inside another tag is treated as a fragment: its text and
11
+ * parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
12
+ * IN clauses. Execution is lazy (on await/then) and happens exactly once.
13
+ *
14
+ ***************************************/
15
+
16
+ import type { QueryOptions, QueryResult } from './types';
17
+
18
+ /** Executor provided by Database/Transaction: runs text+params, resolves rows
19
+ * (or the full QueryResult when options.withMeta is set). */
20
+ export type SqlExecutor = (text: string, params: any[], options?: QueryOptions) => Promise<any>;
21
+
22
+ /** A dynamically quoted identifier produced by sql('name'). */
23
+ export class SqlIdentifier {
24
+ name: string;
25
+ constructor(name: string) {
26
+ this.name = name;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Quote a (possibly dot-qualified) identifier for dialect 3: each part is
32
+ * wrapped in double quotes with embedded quotes doubled, so user input can
33
+ * never break out of the identifier position.
34
+ */
35
+ export function quoteIdentifier(name: string): string {
36
+ return String(name)
37
+ .split('.')
38
+ .map((part) => '"' + part.replace(/"/g, '""') + '"')
39
+ .join('.');
40
+ }
41
+
42
+ /** Compiled form of a tagged query: SQL text with `?` placeholders + params. */
43
+ export interface CompiledQuery {
44
+ text: string;
45
+ params: any[];
46
+ }
47
+
48
+ function compile(strings: readonly string[], values: any[], active?: Set<any>): CompiledQuery {
49
+ let text = '';
50
+ const params: any[] = [];
51
+
52
+ for (let i = 0; i < strings.length; i++) {
53
+ text += strings[i];
54
+ if (i >= values.length) {
55
+ continue;
56
+ }
57
+ const value = values[i];
58
+
59
+ if (value instanceof SqlIdentifier) {
60
+ text += quoteIdentifier(value.name);
61
+ } else if (value instanceof SqlQuery) {
62
+ // embedded fragment: splice its text and params in place. The
63
+ // same fragment may appear several times (a DAG), but a fragment
64
+ // containing itself would recurse forever — track the expansion
65
+ // stack and reject cycles with a diagnosable error.
66
+ active = active || new Set();
67
+ if (active.has(value)) {
68
+ throw new Error('circular sql fragment: a query is embedded (transitively) inside itself');
69
+ }
70
+ active.add(value);
71
+ const inner = compile(value.strings, value.values, active);
72
+ active.delete(value);
73
+ text += inner.text;
74
+ params.push(...inner.params);
75
+ } else if (Array.isArray(value)) {
76
+ // IN (${[1, 2, 3]}) → IN (?, ?, ?)
77
+ if (!value.length) {
78
+ // '' would compile to `IN ()` — invalid SQL raising a server
79
+ // syntax error the caller never wrote; fail early instead
80
+ throw new Error('cannot interpolate an empty array (would compile to invalid SQL like "IN ()")');
81
+ }
82
+ text += value.map(() => '?').join(', ');
83
+ params.push(...value);
84
+ } else {
85
+ text += '?';
86
+ params.push(value);
87
+ }
88
+ }
89
+
90
+ return { text, params };
91
+ }
92
+
93
+ /**
94
+ * A lazily executed tagged query. Awaiting it (or calling then/catch/
95
+ * finally) runs it through the owning Database/Transaction exactly once;
96
+ * embedding it in another tag uses it as a fragment instead and never
97
+ * executes it.
98
+ */
99
+ export class SqlQuery<T = any> implements PromiseLike<T[]> {
100
+ readonly strings: readonly string[];
101
+ readonly values: any[];
102
+ private executor: SqlExecutor;
103
+ private queryOptions?: QueryOptions;
104
+ private executed?: Promise<any>;
105
+ private executedMeta?: boolean;
106
+
107
+ constructor(executor: SqlExecutor, strings: readonly string[], values: any[]) {
108
+ this.executor = executor;
109
+ this.strings = strings;
110
+ this.values = values;
111
+ }
112
+
113
+ /** The compiled SQL text (`?` placeholders) and parameter array. */
114
+ toQuery(): CompiledQuery {
115
+ return compile(this.strings, this.values);
116
+ }
117
+
118
+ /**
119
+ * Attach per-query options (timeout, signal, nestTables, …). Must be
120
+ * called before the query executes — options attached afterwards would
121
+ * be silently ignored, so that throws instead.
122
+ */
123
+ options(queryOptions: QueryOptions): this {
124
+ if (this.executed) {
125
+ throw new Error('sql query already executed — call .options() before awaiting it');
126
+ }
127
+ this.queryOptions = { ...this.queryOptions, ...queryOptions };
128
+ return this;
129
+ }
130
+
131
+ /** Execute resolving the full { rows, fields, affectedRows, … } result. */
132
+ withMeta(): Promise<QueryResult<T>> {
133
+ return this.run(true);
134
+ }
135
+
136
+ /**
137
+ * A query executes exactly once, in the shape of its first consumer
138
+ * (plain rows via then/await, or the full result via withMeta).
139
+ * Consuming it again in the OTHER shape cannot be honoured from the
140
+ * cached promise, so it throws rather than silently returning the
141
+ * wrong shape.
142
+ */
143
+ private run(withMeta: boolean): Promise<any> {
144
+ if (this.executed) {
145
+ if (withMeta !== this.executedMeta) {
146
+ throw new Error(this.executedMeta
147
+ ? 'sql query already executed via .withMeta() — await that result instead of the query'
148
+ : 'sql query already executed as plain rows — call .withMeta() first, or build a new query');
149
+ }
150
+ return this.executed;
151
+ }
152
+ this.executedMeta = withMeta;
153
+ const { text, params } = compile(this.strings, this.values);
154
+ const options = withMeta ? { ...this.queryOptions, withMeta: true } : this.queryOptions;
155
+ this.executed = this.executor(text, params, options);
156
+ return this.executed;
157
+ }
158
+
159
+ then<R1 = T[], R2 = never>(
160
+ onfulfilled?: ((value: T[]) => R1 | PromiseLike<R1>) | null,
161
+ onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
162
+ ): Promise<R1 | R2> {
163
+ return this.run(false).then(onfulfilled, onrejected);
164
+ }
165
+
166
+ catch<R = never>(onrejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T[] | R> {
167
+ return this.then(undefined, onrejected);
168
+ }
169
+
170
+ finally(onfinally?: (() => void) | null): Promise<T[]> {
171
+ return this.run(false).finally(onfinally) as Promise<T[]>;
172
+ }
173
+ }
174
+
175
+ /** The dual-use tag: template tag executes, string call quotes an identifier. */
176
+ export interface SqlTag {
177
+ <T = any>(strings: TemplateStringsArray, ...values: any[]): SqlQuery<T>;
178
+ (identifier: string): SqlIdentifier;
179
+ }
180
+
181
+ /**
182
+ * Build the `sql` tag for a Database/Transaction. `executor` receives the
183
+ * compiled text, params and per-query options and must return a promise
184
+ * (Database/Transaction pass their queryAsync).
185
+ */
186
+ export function makeSqlTag(executor: SqlExecutor): SqlTag {
187
+ return function sql(first: any, ...values: any[]): any {
188
+ if (Array.isArray(first) && Object.prototype.hasOwnProperty.call(first, 'raw')) {
189
+ return new SqlQuery(executor, first, values);
190
+ }
191
+ if (typeof first === 'string') {
192
+ return new SqlIdentifier(first);
193
+ }
194
+ throw new Error('sql must be used as a template tag (sql`...`) or called with an identifier string (sql(\'NAME\'))');
195
+ } as SqlTag;
196
+ }
package/src/types.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  // published declaration files.
7
7
 
8
8
  import type { Readable } from 'stream';
9
+ import type { SqlTag } from './sql-template';
10
+
11
+ export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
9
12
 
10
13
  export type DatabaseCallback = (err: any, db: Database) => void;
11
14
  export type TransactionCallback = (err: any, transaction: Transaction) => void;
@@ -139,6 +142,73 @@ export type QueryOptions = {
139
142
  * unaffected.
140
143
  */
141
144
  nestTables?: boolean | string;
145
+ /**
146
+ * Per-query override of the `transformKeys` connection option: rewrite
147
+ * object-row keys — `'camel'` turns `FIRST_NAME` into `firstName`, or
148
+ * pass a custom `(key) => key` mapper. Applied after `lowercase_keys`
149
+ * and to both parts of `nestTables` keys. Column metadata (`fields`,
150
+ * typeCast) keeps the raw server aliases.
151
+ */
152
+ transformKeys?: 'camel' | ((key: string) => string);
153
+ /**
154
+ * Deliver a full result object `{ rows, fields, affectedRows,
155
+ * recordCounts, warnings }` instead of the bare rows (callback and
156
+ * promise APIs). For DML, `affectedRows` is what the server actually
157
+ * changed (`isc_info_sql_records`, one extra lightweight info request
158
+ * per statement — hence opt-in) and `recordCounts` breaks it down per
159
+ * verb; for SELECT it is the number of rows returned (pg's `rowCount`
160
+ * convention) with no extra round-trip. `warnings` carries any
161
+ * `isc_arg_warning` entries from the execute response. Honoured by
162
+ * query/execute and their *Async wrappers only — ignored by the
163
+ * streaming APIs (sequentially/queryStream, where rows bypass the
164
+ * result) and executeBatch (which has its own completion shape).
165
+ */
166
+ withMeta?: boolean;
167
+ }
168
+
169
+ /** Column metadata delivered in withMeta results (`fields`) — the same
170
+ * vocabulary the typeCast hook receives, plus nullable and the relation
171
+ * alias/schema. */
172
+ export interface FieldMetadata {
173
+ type: number;
174
+ typeName: string;
175
+ subType?: number;
176
+ scale?: number;
177
+ length?: number;
178
+ nullable?: boolean;
179
+ field?: string;
180
+ relation?: string;
181
+ relationAlias?: string;
182
+ relationSchema?: string;
183
+ alias?: string;
184
+ }
185
+
186
+ /** Per-verb server row counts of an executed DML statement. */
187
+ export interface RecordCounts {
188
+ selectCount: number;
189
+ insertCount: number;
190
+ updateCount: number;
191
+ deleteCount: number;
192
+ }
193
+
194
+ /** An isc_arg_warning entry from a server response ('warning' driver event
195
+ * and withMeta `warnings`). */
196
+ export interface ServerWarning {
197
+ gdscode: number;
198
+ params?: (string | number)[];
199
+ message: string;
200
+ }
201
+
202
+ /** Full result shape delivered when `withMeta: true` is set. */
203
+ export interface QueryResult<T = any> {
204
+ /** Rows array (SELECT), single row object (RETURNING / procedures), or undefined (plain DML). */
205
+ rows: T[] | T | undefined;
206
+ fields: FieldMetadata[];
207
+ /** DML: rows the server changed; SELECT: rows returned. */
208
+ affectedRows: number;
209
+ /** Set for DML statements only. */
210
+ recordCounts?: RecordCounts;
211
+ warnings: ServerWarning[];
142
212
  }
143
213
 
144
214
  export type QueryStreamOptions = QueryOptions & {
@@ -152,6 +222,13 @@ export type QueryStreamOptions = QueryOptions & {
152
222
  }
153
223
 
154
224
  export interface Database {
225
+ /**
226
+ * Tagged-template query API (Postgres.js-style): interpolated values
227
+ * become positional parameters, `sql('NAME')` quotes an identifier,
228
+ * embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
229
+ * The returned query is a lazy thenable — it executes once, on await.
230
+ */
231
+ sql: SqlTag;
155
232
  detach(callback?: SimpleCallback): Database;
156
233
  transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
157
234
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
@@ -176,8 +253,11 @@ export interface Database {
176
253
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
177
254
 
178
255
  // Promise / async-await API (see README § Promises / async–await).
179
- // Result metadata is only available through the callback API.
256
+ // Pass { withMeta: true } to resolve with the full QueryResult
257
+ // (rows + fields + affectedRows + warnings) instead of bare rows.
258
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
180
259
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
260
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
181
261
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
182
262
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
183
263
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -201,6 +281,14 @@ export interface Database {
201
281
  }
202
282
 
203
283
  export interface Transaction {
284
+ /** Tagged-template query API running inside this transaction (see Database.sql). */
285
+ sql: SqlTag;
286
+ /**
287
+ * Run `work` inside a savepoint: released on resolve, rolled back TO
288
+ * (undoing only work's changes) on reject — the transaction stays
289
+ * usable either way. Nestable.
290
+ */
291
+ savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
204
292
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
205
293
  query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
206
294
  execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
@@ -219,7 +307,9 @@ export interface Transaction {
219
307
  rollbackRetaining(callback?: SimpleCallback): void;
220
308
 
221
309
  // Promise / async-await API
310
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
222
311
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
312
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
223
313
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
224
314
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
225
315
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -336,6 +426,14 @@ export interface Options {
336
426
  * array rows (execute) are unaffected. Overridable per query.
337
427
  */
338
428
  nestTables?: boolean | string;
429
+ /**
430
+ * Rewrite object-row keys (Postgres.js `transform` counterpart):
431
+ * `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
432
+ * `(key) => key` mapper. Applied after `lowercase_keys` and to both
433
+ * parts of `nestTables` keys; column metadata keeps raw aliases.
434
+ * Overridable per query.
435
+ */
436
+ transformKeys?: 'camel' | ((key: string) => string);
339
437
  /**
340
438
  * TCP keepalive probing to detect dead/stale connections (same option
341
439
  * names as mysql2). On by default; set false to disable.
@@ -374,6 +472,20 @@ export interface Options {
374
472
  * Default 0 (idle connections are kept forever).
375
473
  */
376
474
  idleTimeoutMillis?: number;
475
+ /**
476
+ * Pool only: retire a physical connection after this many checkouts
477
+ * (pg's `maxUses`) — it is closed for good when returned to the pool
478
+ * and replaced on demand. Bounds server-side resource drift on
479
+ * long-lived connections. Default 0 (unlimited uses).
480
+ */
481
+ maxUses?: number;
482
+ /**
483
+ * Pool only: retire a physical connection this many milliseconds after
484
+ * it was created (Postgres.js's `max_lifetime`), on return to the pool
485
+ * or by the idle sweep — even below `min`; replacements are created on
486
+ * demand. Default 0 (unlimited lifetime).
487
+ */
488
+ maxLifetimeMillis?: number;
377
489
  /**
378
490
  * **Firebird 6.0+ only (Protocol 20+)**
379
491
  *
package/src/uri.ts CHANGED
@@ -198,7 +198,59 @@ export function parseConnectionString(str: string): Options {
198
198
  */
199
199
  export function normalizeOptions<T>(options: T | string): T {
200
200
  if (typeof options === 'string') {
201
- return parseConnectionString(options) as T;
201
+ options = parseConnectionString(options) as T;
202
202
  }
203
- return options;
203
+ return applyEnvDefaults(options as any);
204
+ }
205
+
206
+ /**
207
+ * Fall back to environment variables for connection settings the caller
208
+ * did not provide — the pg-style convention using Firebird's own names:
209
+ * ISC_USER / ISC_PASSWORD (honoured by isql and every official tool) plus
210
+ * FIREBIRD_HOST / FIREBIRD_PORT / FIREBIRD_DATABASE / FIREBIRD_ROLE.
211
+ * Explicit options always win; the driver's built-in defaults (SYSDBA /
212
+ * masterkey / 127.0.0.1) still apply when neither is set. A fresh object
213
+ * is returned so caller-owned options objects are never mutated.
214
+ */
215
+ const ENV_FALLBACKS: [string, string][] = [
216
+ ['user', 'ISC_USER'],
217
+ ['password', 'ISC_PASSWORD'],
218
+ ['host', 'FIREBIRD_HOST'],
219
+ ['port', 'FIREBIRD_PORT'],
220
+ ['database', 'FIREBIRD_DATABASE'],
221
+ ['role', 'FIREBIRD_ROLE'],
222
+ ];
223
+
224
+ function applyEnvDefaults<T extends Record<string, any>>(options: T): T {
225
+ let out: any = options;
226
+ for (const [key, envName] of ENV_FALLBACKS) {
227
+ const value = process.env[envName];
228
+ // empty-string env vars (common in CI: `export ISC_PASSWORD=`)
229
+ // count as unset
230
+ if (value === undefined || value === '') {
231
+ continue;
232
+ }
233
+ // a service-manager connection's `database` selects the TARGET of
234
+ // backup/restore — never let a leftover env var pick that silently
235
+ if (key === 'database' && (options as any).manager) {
236
+ continue;
237
+ }
238
+ if (out[key] === undefined || out[key] === null || out[key] === '') {
239
+ if (out === options) {
240
+ out = { ...(options as any) };
241
+ }
242
+ if (key === 'port') {
243
+ const port = Number(value);
244
+ if (!Number.isFinite(port) || port <= 0) {
245
+ // NaN is falsy: it would silently fall back to 3050
246
+ // downstream instead of surfacing the typo
247
+ throw new Error('Invalid FIREBIRD_PORT environment variable: ' + value);
248
+ }
249
+ out[key] = port;
250
+ } else {
251
+ out[key] = value;
252
+ }
253
+ }
254
+ }
255
+ return out;
204
256
  }
@@ -75,31 +75,8 @@ function statementCacheLimit(options: InternalOptions): number {
75
75
  return 0;
76
76
  }
77
77
 
78
- const SQL_TYPE_NAMES: Record<number, string> = {
79
- [Const.SQL_TEXT]: 'TEXT',
80
- [Const.SQL_VARYING]: 'VARYING',
81
- [Const.SQL_SHORT]: 'SHORT',
82
- [Const.SQL_LONG]: 'LONG',
83
- [Const.SQL_FLOAT]: 'FLOAT',
84
- [Const.SQL_DOUBLE]: 'DOUBLE',
85
- [Const.SQL_D_FLOAT]: 'D_FLOAT',
86
- [Const.SQL_TIMESTAMP]: 'TIMESTAMP',
87
- [Const.SQL_BLOB]: 'BLOB',
88
- [Const.SQL_ARRAY]: 'ARRAY',
89
- [Const.SQL_QUAD]: 'QUAD',
90
- [Const.SQL_TYPE_TIME]: 'TIME',
91
- [Const.SQL_TYPE_DATE]: 'DATE',
92
- [Const.SQL_INT64]: 'INT64',
93
- [Const.SQL_INT128]: 'INT128',
94
- [Const.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
95
- [Const.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
96
- [Const.SQL_TIME_TZ]: 'TIME_TZ',
97
- [Const.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
98
- [Const.SQL_DEC16]: 'DEC16',
99
- [Const.SQL_DEC34]: 'DEC34',
100
- [Const.SQL_BOOLEAN]: 'BOOLEAN',
101
- [Const.SQL_NULL]: 'NULL',
102
- };
78
+ // SQL type-code names live in xsqlvar.ts alongside the descriptors
79
+ const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
103
80
 
104
81
  /**
105
82
  * Run the user's typeCast hook (options.typeCast) for one column value.
@@ -114,16 +91,7 @@ function applyTypeCast(options: InternalOptions, meta: Partial<Xsql.SQLVarBase>,
114
91
  if (typeof typeCast !== 'function') {
115
92
  return defaultValue;
116
93
  }
117
- const column = {
118
- type: meta.type!,
119
- typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
120
- subType: meta.subType,
121
- scale: meta.scale,
122
- length: meta.length,
123
- field: meta.field,
124
- relation: meta.relation,
125
- alias: meta.alias,
126
- };
94
+ const column = Xsql.describeField(meta);
127
95
  // A hook exception must never escape into the row-decode loop: there it
128
96
  // would be mistaken for an incomplete packet and desync the response
129
97
  // queue (the same failure mode as issue #341). Fall back to the default
@@ -456,13 +424,38 @@ class Connection {
456
424
  self._queue.length, self._pending.length, xdr.pos);
457
425
  }
458
426
 
427
+ // Surface isc_arg_warning entries (parsed since 2.10.0 but
428
+ // dropped here): resolve their message text and emit them on
429
+ // the Database on the next tick, so a listener registered
430
+ // inside this very response's callback (e.g. right after
431
+ // attach) still receives them.
432
+ if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
433
+ const warnings = obj.warnings;
434
+ for (const w of warnings) {
435
+ if (w.message === undefined) {
436
+ w.message = lookupMessages([w]);
437
+ if (!w.message || w.message === 'Unknow error') {
438
+ // codes newer than the bundled firebird.msg:
439
+ // still say something actionable
440
+ w.message = 'Firebird warning ' + w.gdscode +
441
+ (w.params && w.params.length ? ': ' + w.params.join(', ') : '');
442
+ }
443
+ }
444
+ }
445
+ process.nextTick(function () {
446
+ for (const w of warnings) {
447
+ self.db.emit('warning', w);
448
+ }
449
+ });
450
+ }
451
+
459
452
  if (obj && obj.status) {
460
453
  obj.message = lookupMessages(obj.status);
461
454
  doCallback(obj, cb);
462
455
  } else {
463
456
  doCallback(obj, cb);
464
457
  }
465
-
458
+
466
459
  });
467
460
 
468
461
  if (xdr.pos === 0) {
@@ -1984,6 +1977,34 @@ class Connection {
1984
1977
  }
1985
1978
 
1986
1979
 
1980
+ /**
1981
+ * Query runtime information about a prepared statement via op_info_sql
1982
+ * (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
1983
+ * response is a plain op_response whose buffer holds the info clusters.
1984
+ */
1985
+ statementInfo(statement: Statement, items: number[], callback?: QueueCallback) {
1986
+ if (this._isClosed)
1987
+ return this.throwClosed(callback);
1988
+
1989
+ this._pending.push('statementInfo');
1990
+
1991
+ var msg = this._msg;
1992
+ var blr = this._blr;
1993
+ msg.pos = 0;
1994
+ blr.pos = 0;
1995
+
1996
+ blr.addBytes(items);
1997
+
1998
+ msg.addInt(Const.op_info_sql);
1999
+ msg.addInt(statement.handle);
2000
+ msg.addInt(0); // incarnation
2001
+ msg.addBlr(blr);
2002
+ msg.addInt(65535); // buffer_length
2003
+
2004
+ this._queueEvent(callback);
2005
+ }
2006
+
2007
+
1987
2008
  fetchAll(statement: Statement, transaction: Transaction, callback: Callback<any[]>) {
1988
2009
  const self = this;
1989
2010
  const custom = statement.options || {};
@@ -2494,7 +2515,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
2494
2515
 
2495
2516
  if (custom.asObject && !data.fcols) {
2496
2517
  const nest = Xsql.resolveNestTables(custom, cnx.options);
2497
- const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2518
+ const transform = Xsql.resolveKeyTransform(custom, cnx.options);
2519
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
2498
2520
  data.fcols = columnKeys.map((k) => k.key);
2499
2521
  if (nest === true) {
2500
2522
  // computeColumnKeys always sets table when nesting
package/src/wire/const.ts CHANGED
@@ -587,6 +587,12 @@ const sqlInfo = {
587
587
  isc_info_sql_stmt_type : 21,
588
588
  isc_info_sql_get_plan : 22,
589
589
  isc_info_sql_records : 23,
590
+ // per-verb row counts nested inside an isc_info_sql_records cluster
591
+ // (inf_pub.h isc_info_req_*)
592
+ isc_info_req_select_count : 13,
593
+ isc_info_req_insert_count : 14,
594
+ isc_info_req_update_count : 15,
595
+ isc_info_req_delete_count : 16,
590
596
  isc_info_sql_batch_fetch : 24,
591
597
  isc_info_sql_relation_alias : 25, // >: 2.0
592
598
  isc_info_sql_explain_plan : 26, // >= 3.0
@@ -669,6 +675,12 @@ const DESCRIBE_WITH_SCHEMA = [
669
675
  sqlInfo.isc_info_sql_describe_end
670
676
  ];
671
677
 
678
+ // op_info_sql request for the per-verb DML row counts of an executed
679
+ // statement (withMeta / affectedRows).
680
+ const RECORDS_INFO = [
681
+ sqlInfo.isc_info_sql_records,
682
+ ];
683
+
672
684
  /***********************/
673
685
  /* ISC Services */
674
686
  /***********************/
@@ -910,6 +922,7 @@ const Const = Object.freeze({
910
922
  ...defaultOptions,
911
923
  DESCRIBE,
912
924
  DESCRIBE_WITH_SCHEMA,
925
+ RECORDS_INFO,
913
926
  ...dpb,
914
927
  ...dsql,
915
928
  ...fetchOp,