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.
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;
@@ -127,7 +129,70 @@ export type QueryOptions = {
127
129
  * unaffected.
128
130
  */
129
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;
130
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
+ }
131
196
  export type QueryStreamOptions = QueryOptions & {
132
197
  /**
133
198
  * Rows buffered internally before fetching pauses (object-mode
@@ -138,6 +203,13 @@ export type QueryStreamOptions = QueryOptions & {
138
203
  asObject?: boolean;
139
204
  };
140
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;
141
213
  detach(callback?: SimpleCallback): Database;
142
214
  transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
143
215
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
@@ -160,7 +232,13 @@ export interface Database {
160
232
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
161
233
  dropTablespace(name: string, callback?: QueryCallback): Database;
162
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>>;
163
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>>;
164
242
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
165
243
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
166
244
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -183,6 +261,14 @@ export interface Database {
183
261
  cancelAsync(kind?: number): Promise<void>;
184
262
  }
185
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>;
186
272
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
187
273
  query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
188
274
  execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
@@ -199,7 +285,13 @@ export interface Transaction {
199
285
  commitRetaining(callback?: SimpleCallback): void;
200
286
  rollback(callback?: SimpleCallback): void;
201
287
  rollbackRetaining(callback?: SimpleCallback): void;
288
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
289
+ withMeta: true;
290
+ }): Promise<QueryResult<T>>;
202
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>>;
203
295
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
204
296
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
205
297
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -276,6 +368,14 @@ export interface Options {
276
368
  * array rows (execute) are unaffected. Overridable per query.
277
369
  */
278
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);
279
379
  /**
280
380
  * TCP keepalive probing to detect dead/stale connections (same option
281
381
  * names as mysql2). On by default; set false to disable.
@@ -314,6 +414,20 @@ export interface Options {
314
414
  * Default 0 (idle connections are kept forever).
315
415
  */
316
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;
317
431
  /**
318
432
  * **Firebird 6.0+ only (Protocol 20+)**
319
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;
@@ -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 || {};
@@ -2106,7 +2119,8 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
2106
2119
  data.frows = data.frows || [];
2107
2120
  if (custom.asObject && !data.fcols) {
2108
2121
  const nest = Xsql.resolveNestTables(custom, cnx.options);
2109
- const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys);
2122
+ const transform = Xsql.resolveKeyTransform(custom, cnx.options);
2123
+ const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
2110
2124
  data.fcols = columnKeys.map((k) => k.key);
2111
2125
  if (nest === true) {
2112
2126
  // computeColumnKeys always sets table when nesting
@@ -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;
package/lib/wire/const.js CHANGED
@@ -540,6 +540,12 @@ const sqlInfo = {
540
540
  isc_info_sql_stmt_type: 21,
541
541
  isc_info_sql_get_plan: 22,
542
542
  isc_info_sql_records: 23,
543
+ // per-verb row counts nested inside an isc_info_sql_records cluster
544
+ // (inf_pub.h isc_info_req_*)
545
+ isc_info_req_select_count: 13,
546
+ isc_info_req_insert_count: 14,
547
+ isc_info_req_update_count: 15,
548
+ isc_info_req_delete_count: 16,
543
549
  isc_info_sql_batch_fetch: 24,
544
550
  isc_info_sql_relation_alias: 25, // >: 2.0
545
551
  isc_info_sql_explain_plan: 26, // >= 3.0
@@ -618,6 +624,11 @@ const DESCRIBE_WITH_SCHEMA = [
618
624
  sqlInfo.isc_info_sql_length,
619
625
  sqlInfo.isc_info_sql_describe_end
620
626
  ];
627
+ // op_info_sql request for the per-verb DML row counts of an executed
628
+ // statement (withMeta / affectedRows).
629
+ const RECORDS_INFO = [
630
+ sqlInfo.isc_info_sql_records,
631
+ ];
621
632
  /***********************/
622
633
  /* ISC Services */
623
634
  /***********************/
@@ -843,6 +854,7 @@ const Const = Object.freeze({
843
854
  ...defaultOptions,
844
855
  DESCRIBE,
845
856
  DESCRIBE_WITH_SCHEMA,
857
+ RECORDS_INFO,
846
858
  ...dpb,
847
859
  ...dsql,
848
860
  ...fetchOp,
@@ -1,5 +1,6 @@
1
1
  import Events from 'events';
2
2
  import { type Callback, type SimpleCallback } from '../callback';
3
+ import { type SqlTag } from '../sql-template';
3
4
  import FbEventManager from './fbEventManager';
4
5
  import type Connection from './connection';
5
6
  import type Transaction from './transaction';
@@ -15,7 +16,15 @@ type TransactionArg = TransactionOptions | Isolation | TransactionCb | undefined
15
16
  declare class Database extends Events.EventEmitter {
16
17
  connection: Connection;
17
18
  eventid: number;
19
+ private _sql?;
18
20
  constructor(connection: Connection);
21
+ /**
22
+ * Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
23
+ * Built lazily on first access; the compiled text is positional-only,
24
+ * so the namedPlaceholders rewriter is disabled — any `:token` in the
25
+ * template is PSQL (EXECUTE BLOCK), not a placeholder.
26
+ */
27
+ get sql(): SqlTag;
19
28
  escape(value: any): string;
20
29
  detach(callback?: Callback, force?: boolean): this;
21
30
  transaction(options: TransactionArg, callback?: TransactionCb): this;
@@ -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 sql_template_1 = require("../sql-template");
9
10
  const xsqlvar_1 = require("./xsqlvar");
10
11
  const eventConnection_1 = __importDefault(require("./eventConnection"));
11
12
  const fbEventManager_1 = __importDefault(require("./fbEventManager"));
@@ -76,7 +77,7 @@ function readblob(blob, callback) {
76
77
  });
77
78
  });
78
79
  }
79
- function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, callback) {
80
+ function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, transform, callback) {
80
81
  if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === const_1.default.SQL_BLOB)) {
81
82
  callback(null, row);
82
83
  return;
@@ -86,7 +87,7 @@ function fetchBlobSyncRow(row, meta, nestTables, lowercaseKeys, callback) {
86
87
  // duplicate JOIN column names (and nested rows) break that alignment.
87
88
  // Array rows (sequentially's legacy boolean form) are keyed by index.
88
89
  const isArrayRow = Array.isArray(row);
89
- const keys = isArrayRow ? null : (0, xsqlvar_1.computeColumnKeys)(meta, nestTables, lowercaseKeys);
90
+ const keys = isArrayRow ? null : (0, xsqlvar_1.computeColumnKeys)(meta, nestTables, lowercaseKeys, transform);
90
91
  const blobCells = [];
91
92
  for (let i = 0; i < meta.length; i++) {
92
93
  if (!meta[i] || meta[i].type !== const_1.default.SQL_BLOB) {
@@ -126,6 +127,15 @@ class Database extends events_1.default.EventEmitter {
126
127
  connection.db = this;
127
128
  this.eventid = 1;
128
129
  }
130
+ /**
131
+ * Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
132
+ * Built lazily on first access; the compiled text is positional-only,
133
+ * so the namedPlaceholders rewriter is disabled — any `:token` in the
134
+ * template is PSQL (EXECUTE BLOCK), not a placeholder.
135
+ */
136
+ get sql() {
137
+ return this._sql || (this._sql = (0, sql_template_1.makeSqlTag)((text, params, options) => this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
138
+ }
129
139
  escape(value) {
130
140
  return (0, utils_1.escape)(value, this.connection.accept.protocolVersion);
131
141
  }
@@ -263,6 +273,9 @@ class Database extends events_1.default.EventEmitter {
263
273
  callback = undefined;
264
274
  }
265
275
  var self = this;
276
+ var keyResolutionDone = false;
277
+ var resolvedNest;
278
+ var resolvedTransform;
266
279
  var _on = function (row, i, meta, next) {
267
280
  var done = false;
268
281
  var finish = function (err) {
@@ -272,9 +285,15 @@ class Database extends events_1.default.EventEmitter {
272
285
  done = true;
273
286
  next(err);
274
287
  };
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) {
288
+ // options is read at call time, after the normalization below;
289
+ // both values are query-invariant, so resolve them once on the
290
+ // first row instead of allocating per row
291
+ if (!keyResolutionDone) {
292
+ resolvedNest = (0, xsqlvar_1.resolveNestTables)(options, self.connection.options);
293
+ resolvedTransform = (0, xsqlvar_1.resolveKeyTransform)(options, self.connection.options);
294
+ keyResolutionDone = true;
295
+ }
296
+ fetchBlobSyncRow(row, meta, resolvedNest, self.connection._lowercase_keys, resolvedTransform, function (blobErr) {
278
297
  if (blobErr) {
279
298
  finish(blobErr);
280
299
  return;
@@ -468,8 +487,9 @@ class Database extends events_1.default.EventEmitter {
468
487
  /*
469
488
  * Promise / async-await API.
470
489
  * Each *Async method wraps its callback counterpart; the callback API
471
- * stays untouched. Result metadata is only available through the
472
- * callback API the promises resolve with the rows alone.
490
+ * stays untouched. The promises resolve with the rows alone unless
491
+ * { withMeta: true } is passed, which resolves the full
492
+ * { rows, fields, affectedRows, recordCounts, warnings } result.
473
493
  */
474
494
  queryAsync(query, params, options) {
475
495
  var self = this;
@@ -160,6 +160,10 @@ class BlrReader {
160
160
  break;
161
161
  case 4:
162
162
  value = this.buffer.readInt32LE(this.pos);
163
+ break;
164
+ case 8:
165
+ // e.g. record counts above 2^31 (isc_info_sql_records)
166
+ value = Number(this.buffer.readBigInt64LE(this.pos));
163
167
  }
164
168
  this.pos += len;
165
169
  return value;
@@ -1,4 +1,5 @@
1
1
  import { type Callback, type SimpleCallback } from '../callback';
2
+ import { type SqlTag } from '../sql-template';
2
3
  import type Connection from './connection';
3
4
  import type Database from './database';
4
5
  import type Statement from './statement';
@@ -8,7 +9,33 @@ declare class Transaction {
8
9
  connection: Connection;
9
10
  db: Database;
10
11
  handle: number;
12
+ private _sql?;
11
13
  constructor(connection: Connection);
14
+ /**
15
+ * Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
16
+ * Built lazily — transactions are created per-query internally, and
17
+ * those throwaway instances must not pay for the tag. The compiled text
18
+ * is positional-only, so the namedPlaceholders rewriter is disabled:
19
+ * any `:token` in the template is PSQL (EXECUTE BLOCK), not a
20
+ * placeholder.
21
+ */
22
+ get sql(): SqlTag;
23
+ /** Current savepoint nesting depth (names savepoints, see savepoint()). */
24
+ private _savepointDepth;
25
+ /**
26
+ * Run `work` inside a savepoint (Firebird 1.5+): on resolve the
27
+ * savepoint is released, on reject the transaction rolls back TO the
28
+ * savepoint — undoing only work's changes — and the error is rethrown,
29
+ * leaving the transaction itself usable. Nestable (each call generates
30
+ * a fresh NF_SP_n name), mirroring db.withTransaction's style and
31
+ * Postgres.js's sql.savepoint().
32
+ *
33
+ * Do NOT run sibling savepoints concurrently on one transaction
34
+ * (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
35
+ * savepoint created after it, so interleaved siblings release each
36
+ * other. Nested (awaited) savepoints are fine.
37
+ */
38
+ savepoint<T>(work: (transaction: this) => Promise<T> | T): Promise<T>;
12
39
  /** Per-call options.namedPlaceholders overrides the connection option. */
13
40
  private namedPlaceholdersEnabled;
14
41
  newStatement(query: string, callback: StatementCb, options?: InternalQueryOptions): void;