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.
- package/README.md +236 -3
- package/lib/pool.d.ts +14 -1
- package/lib/pool.js +59 -16
- package/lib/sql-template.d.ts +81 -0
- package/lib/sql-template.js +162 -0
- package/lib/types.d.ts +136 -0
- package/lib/uri.js +53 -2
- package/lib/wire/connection.d.ts +7 -1
- package/lib/wire/connection.js +67 -46
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +14 -0
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +49 -14
- package/lib/wire/serialize.d.ts +3 -1
- package/lib/wire/serialize.js +4 -0
- package/lib/wire/transaction.d.ts +27 -0
- package/lib/wire/transaction.js +131 -13
- package/lib/wire/xsqlvar.d.ts +100 -0
- package/lib/wire/xsqlvar.js +219 -1
- package/package.json +1 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +135 -1
- package/src/uri.ts +54 -2
- package/src/wire/connection.ts +77 -46
- package/src/wire/const.ts +15 -0
- package/src/wire/database.ts +52 -14
- package/src/wire/serialize.ts +8 -2
- package/src/wire/transaction.ts +140 -19
- package/src/wire/xsqlvar.ts +241 -0
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;
|
|
@@ -127,6 +130,85 @@ export type QueryOptions = {
|
|
|
127
130
|
* it cancels whatever is currently executing on the connection.
|
|
128
131
|
*/
|
|
129
132
|
signal?: AbortSignal;
|
|
133
|
+
/**
|
|
134
|
+
* Per-query override of the `nestTables` connection option (mysql2
|
|
135
|
+
* semantics). `true` nests each object row by source table:
|
|
136
|
+
* `row[table][column]` — the table key is the query's relation alias
|
|
137
|
+
* when one is used (`FROM emp e` → `row.E`), the table name otherwise,
|
|
138
|
+
* and `''` for expression columns. A string separator flattens keys
|
|
139
|
+
* instead: `nestTables: '_'` → `row.EMP_NAME`; expression columns get
|
|
140
|
+
* the bare separator prefix (`row._ANSWER`, as in mysql2). Keys honour
|
|
141
|
+
* `lowercase_keys`. Object rows only — `db.execute` array rows are
|
|
142
|
+
* unaffected.
|
|
143
|
+
*/
|
|
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[];
|
|
130
212
|
}
|
|
131
213
|
|
|
132
214
|
export type QueryStreamOptions = QueryOptions & {
|
|
@@ -140,6 +222,13 @@ export type QueryStreamOptions = QueryOptions & {
|
|
|
140
222
|
}
|
|
141
223
|
|
|
142
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;
|
|
143
232
|
detach(callback?: SimpleCallback): Database;
|
|
144
233
|
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
|
|
145
234
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
@@ -164,8 +253,11 @@ export interface Database {
|
|
|
164
253
|
createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
|
|
165
254
|
|
|
166
255
|
// Promise / async-await API (see README § Promises / async–await).
|
|
167
|
-
//
|
|
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>>;
|
|
168
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>>;
|
|
169
261
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
170
262
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
171
263
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -189,6 +281,14 @@ export interface Database {
|
|
|
189
281
|
}
|
|
190
282
|
|
|
191
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>;
|
|
192
292
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
193
293
|
query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
194
294
|
execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
@@ -207,7 +307,9 @@ export interface Transaction {
|
|
|
207
307
|
rollbackRetaining(callback?: SimpleCallback): void;
|
|
208
308
|
|
|
209
309
|
// Promise / async-await API
|
|
310
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
210
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>>;
|
|
211
313
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
212
314
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
213
315
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -314,6 +416,24 @@ export interface Options {
|
|
|
314
416
|
* per-query `namedPlaceholders: false` override.
|
|
315
417
|
*/
|
|
316
418
|
namedPlaceholders?: boolean;
|
|
419
|
+
/**
|
|
420
|
+
* Qualify object-row keys by source table (same option as mysql2), so
|
|
421
|
+
* JOINed columns with the same name stop overwriting each other:
|
|
422
|
+
* `true` nests each row as `row[table][column]`; a string separator
|
|
423
|
+
* flattens to `row['table' + sep + 'column']`. See
|
|
424
|
+
* `QueryOptions.nestTables` for the exact key rules. Applies wherever
|
|
425
|
+
* object rows are produced (query / sequentially / queryStream);
|
|
426
|
+
* array rows (execute) are unaffected. Overridable per query.
|
|
427
|
+
*/
|
|
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);
|
|
317
437
|
/**
|
|
318
438
|
* TCP keepalive probing to detect dead/stale connections (same option
|
|
319
439
|
* names as mysql2). On by default; set false to disable.
|
|
@@ -352,6 +472,20 @@ export interface Options {
|
|
|
352
472
|
* Default 0 (idle connections are kept forever).
|
|
353
473
|
*/
|
|
354
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;
|
|
355
489
|
/**
|
|
356
490
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
357
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
|
-
|
|
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
|
}
|
package/src/wire/connection.ts
CHANGED
|
@@ -75,31 +75,8 @@ function statementCacheLimit(options: InternalOptions): number {
|
|
|
75
75
|
return 0;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
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 || {};
|
|
@@ -2015,7 +2036,9 @@ class Connection {
|
|
|
2015
2036
|
readBlobsSequentially(0, []).then((arrBlob: any) => {
|
|
2016
2037
|
for (let i = 0; i < arrBlob.length; i++) {
|
|
2017
2038
|
const blob = arrBlob[i];
|
|
2018
|
-
|
|
2039
|
+
// nestTables === true rows: the value lives in the
|
|
2040
|
+
// per-table sub-object, not on the row itself
|
|
2041
|
+
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
|
|
2019
2042
|
statement.connection.options, blob.meta || {},
|
|
2020
2043
|
parseValueIfJson(blob.value, statement.connection.options));
|
|
2021
2044
|
}
|
|
@@ -2469,6 +2492,7 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2469
2492
|
delete data.frow;
|
|
2470
2493
|
delete data.frows;
|
|
2471
2494
|
delete data.fcols;
|
|
2495
|
+
delete data.ftables;
|
|
2472
2496
|
|
|
2473
2497
|
if (isOpFetch && data.fop) { // could be set when a packet is not complete
|
|
2474
2498
|
data.readBuffer(68); // ??
|
|
@@ -2490,10 +2514,13 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2490
2514
|
data.frows = data.frows || [];
|
|
2491
2515
|
|
|
2492
2516
|
if (custom.asObject && !data.fcols) {
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2517
|
+
const nest = Xsql.resolveNestTables(custom, cnx.options);
|
|
2518
|
+
const transform = Xsql.resolveKeyTransform(custom, cnx.options);
|
|
2519
|
+
const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
|
|
2520
|
+
data.fcols = columnKeys.map((k) => k.key);
|
|
2521
|
+
if (nest === true) {
|
|
2522
|
+
// computeColumnKeys always sets table when nesting
|
|
2523
|
+
data.ftables = columnKeys.map((k) => k.table!);
|
|
2497
2524
|
}
|
|
2498
2525
|
}
|
|
2499
2526
|
|
|
@@ -2521,7 +2548,10 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2521
2548
|
|
|
2522
2549
|
if (!lowerV13 && nullBitSet!.get(data.fcolumn)) {
|
|
2523
2550
|
const nullKey = custom.asObject ? data.fcols![data.fcolumn!] : data.fcolumn;
|
|
2524
|
-
|
|
2551
|
+
// ftables is only set when nestTables === true, so
|
|
2552
|
+
// the default path writes straight into the row
|
|
2553
|
+
(data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn!]) : data.frow)[nullKey] =
|
|
2554
|
+
applyTypeCast(cnx.options, item, null);
|
|
2525
2555
|
|
|
2526
2556
|
continue;
|
|
2527
2557
|
}
|
|
@@ -2538,7 +2568,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2538
2568
|
|
|
2539
2569
|
if (item.type === Const.SQL_BLOB && value !== null) {
|
|
2540
2570
|
if (item.subType === Const.isc_blob_text && cnx.options.blobAsText) {
|
|
2541
|
-
value = fetch_blob_async_transaction(statement, value, key, row, item
|
|
2571
|
+
value = fetch_blob_async_transaction(statement, value, key, row, item,
|
|
2572
|
+
data.ftables && data.ftables[data.fcolumn!]);
|
|
2542
2573
|
arrBlob.push(value);
|
|
2543
2574
|
pendingTextBlob = true;
|
|
2544
2575
|
} else {
|
|
@@ -2546,7 +2577,7 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2546
2577
|
}
|
|
2547
2578
|
}
|
|
2548
2579
|
|
|
2549
|
-
data.frow[key] = pendingTextBlob
|
|
2580
|
+
(data.ftables ? Xsql.nestCell(data.frow, data.ftables[data.fcolumn!]) : data.frow)[key] = pendingTextBlob
|
|
2550
2581
|
? value
|
|
2551
2582
|
: applyTypeCast(cnx.options, item, parseValueIfJson(value, cnx.options));
|
|
2552
2583
|
} catch (e) {
|
|
@@ -3475,8 +3506,8 @@ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
|
|
|
3475
3506
|
blr.addByte(Const.blr_eoc);
|
|
3476
3507
|
}
|
|
3477
3508
|
|
|
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 };
|
|
3509
|
+
function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string) {
|
|
3510
|
+
const infoValue = { row, column, value: '', meta, table };
|
|
3480
3511
|
|
|
3481
3512
|
return (transactionArg: any) => {
|
|
3482
3513
|
const cacheKey = `${id.high}:${id.low}`;
|
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
|
|
@@ -626,6 +632,7 @@ const DESCRIBE = [
|
|
|
626
632
|
sqlInfo.isc_info_sql_length,
|
|
627
633
|
sqlInfo.isc_info_sql_field,
|
|
628
634
|
sqlInfo.isc_info_sql_relation,
|
|
635
|
+
sqlInfo.isc_info_sql_relation_alias, // FB 2.0+: query alias of the source relation (nestTables)
|
|
629
636
|
//isc_info_sql_owner,
|
|
630
637
|
sqlInfo.isc_info_sql_alias,
|
|
631
638
|
sqlInfo.isc_info_sql_describe_end,
|
|
@@ -654,6 +661,7 @@ const DESCRIBE_WITH_SCHEMA = [
|
|
|
654
661
|
sqlInfo.isc_info_sql_field,
|
|
655
662
|
sqlInfo.isc_info_sql_relation,
|
|
656
663
|
sqlInfo.isc_info_sql_relation_schema, // FB 6.0: schema of source relation
|
|
664
|
+
sqlInfo.isc_info_sql_relation_alias, // query alias of the source relation (nestTables)
|
|
657
665
|
//isc_info_sql_owner,
|
|
658
666
|
sqlInfo.isc_info_sql_alias,
|
|
659
667
|
sqlInfo.isc_info_sql_describe_end,
|
|
@@ -667,6 +675,12 @@ const DESCRIBE_WITH_SCHEMA = [
|
|
|
667
675
|
sqlInfo.isc_info_sql_describe_end
|
|
668
676
|
];
|
|
669
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
|
+
|
|
670
684
|
/***********************/
|
|
671
685
|
/* ISC Services */
|
|
672
686
|
/***********************/
|
|
@@ -908,6 +922,7 @@ const Const = Object.freeze({
|
|
|
908
922
|
...defaultOptions,
|
|
909
923
|
DESCRIBE,
|
|
910
924
|
DESCRIBE_WITH_SCHEMA,
|
|
925
|
+
RECORDS_INFO,
|
|
911
926
|
...dpb,
|
|
912
927
|
...dsql,
|
|
913
928
|
...fetchOp,
|
package/src/wire/database.ts
CHANGED
|
@@ -2,6 +2,8 @@ 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 { makeSqlTag, type SqlTag } from '../sql-template';
|
|
6
|
+
import { computeColumnKeys, nestCell, resolveKeyTransform, resolveNestTables } from './xsqlvar';
|
|
5
7
|
import EventConnection from './eventConnection';
|
|
6
8
|
import FbEventManager from './fbEventManager';
|
|
7
9
|
import makeQueryStream from './query-stream';
|
|
@@ -94,35 +96,47 @@ function readblob(blob: any, callback: Callback): void {
|
|
|
94
96
|
});
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
function fetchBlobSyncRow(row: any, meta: any[], callback: Callback): void {
|
|
98
|
-
if (!row || !meta || !meta.length) {
|
|
99
|
+
function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, transform: ((key: string) => string) | undefined, callback: Callback): void {
|
|
100
|
+
if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === Const.SQL_BLOB)) {
|
|
99
101
|
callback(null, row);
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// locate blob cells by the same key computation the fetch decoder used,
|
|
106
|
+
// rather than assuming Object.keys(row) is index-aligned with meta —
|
|
107
|
+
// duplicate JOIN column names (and nested rows) break that alignment.
|
|
108
|
+
// Array rows (sequentially's legacy boolean form) are keyed by index.
|
|
109
|
+
const isArrayRow = Array.isArray(row);
|
|
110
|
+
const keys = isArrayRow ? null : computeColumnKeys(meta, nestTables, lowercaseKeys, transform);
|
|
111
|
+
const blobCells: { target: any; key: string | number }[] = [];
|
|
105
112
|
|
|
106
113
|
for (let i = 0; i < meta.length; i++) {
|
|
107
|
-
if (meta[i]
|
|
108
|
-
|
|
114
|
+
if (!meta[i] || meta[i].type !== Const.SQL_BLOB) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const target = keys ? nestCell(row, keys[i].table) : row;
|
|
118
|
+
const key = keys ? keys[i].key : i;
|
|
119
|
+
// duplicate aliases collapse onto one cell — read it only once
|
|
120
|
+
if (typeof target[key] === 'function' &&
|
|
121
|
+
!blobCells.some((cell) => cell.target === target && cell.key === key)) {
|
|
122
|
+
blobCells.push({ target, key });
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
if (!
|
|
126
|
+
if (!blobCells.length) {
|
|
113
127
|
callback(null, row);
|
|
114
128
|
return;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
let pending =
|
|
131
|
+
let pending = blobCells.length;
|
|
118
132
|
let blobErr: any;
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
readblob(
|
|
134
|
+
blobCells.forEach(function(cell) {
|
|
135
|
+
readblob(cell.target[cell.key], function(err: any, data: any) {
|
|
122
136
|
if (err && !blobErr) {
|
|
123
137
|
blobErr = err;
|
|
124
138
|
}
|
|
125
|
-
|
|
139
|
+
cell.target[cell.key] = data;
|
|
126
140
|
pending--;
|
|
127
141
|
if (pending === 0) {
|
|
128
142
|
callback(blobErr, row);
|
|
@@ -134,6 +148,7 @@ function fetchBlobSyncRow(row: any, meta: any[], callback: Callback): void {
|
|
|
134
148
|
class Database extends Events.EventEmitter {
|
|
135
149
|
connection: Connection;
|
|
136
150
|
eventid: number;
|
|
151
|
+
private _sql?: SqlTag;
|
|
137
152
|
|
|
138
153
|
constructor(connection: Connection) {
|
|
139
154
|
super();
|
|
@@ -142,6 +157,17 @@ class Database extends Events.EventEmitter {
|
|
|
142
157
|
this.eventid = 1;
|
|
143
158
|
}
|
|
144
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
|
|
162
|
+
* Built lazily on first access; the compiled text is positional-only,
|
|
163
|
+
* so the namedPlaceholders rewriter is disabled — any `:token` in the
|
|
164
|
+
* template is PSQL (EXECUTE BLOCK), not a placeholder.
|
|
165
|
+
*/
|
|
166
|
+
get sql(): SqlTag {
|
|
167
|
+
return this._sql || (this._sql = makeSqlTag((text, params, options) =>
|
|
168
|
+
this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
escape(value: any): string {
|
|
146
172
|
return escape(value, this.connection.accept.protocolVersion);
|
|
147
173
|
}
|
|
@@ -312,6 +338,9 @@ class Database extends Events.EventEmitter {
|
|
|
312
338
|
}
|
|
313
339
|
|
|
314
340
|
var self = this;
|
|
341
|
+
var keyResolutionDone = false;
|
|
342
|
+
var resolvedNest: boolean | string | undefined;
|
|
343
|
+
var resolvedTransform: ((key: string) => string) | undefined;
|
|
315
344
|
var _on = function(row: any, i: number, meta: any[], next: (err?: any) => void) {
|
|
316
345
|
var done = false;
|
|
317
346
|
var finish = function(err?: any) {
|
|
@@ -322,7 +351,15 @@ class Database extends Events.EventEmitter {
|
|
|
322
351
|
next(err);
|
|
323
352
|
};
|
|
324
353
|
|
|
325
|
-
|
|
354
|
+
// options is read at call time, after the normalization below;
|
|
355
|
+
// both values are query-invariant, so resolve them once on the
|
|
356
|
+
// first row instead of allocating per row
|
|
357
|
+
if (!keyResolutionDone) {
|
|
358
|
+
resolvedNest = resolveNestTables(options as any, self.connection.options);
|
|
359
|
+
resolvedTransform = resolveKeyTransform(options as any, self.connection.options);
|
|
360
|
+
keyResolutionDone = true;
|
|
361
|
+
}
|
|
362
|
+
fetchBlobSyncRow(row, meta, resolvedNest, self.connection._lowercase_keys, resolvedTransform, function(blobErr: any) {
|
|
326
363
|
if (blobErr) {
|
|
327
364
|
finish(blobErr);
|
|
328
365
|
return;
|
|
@@ -537,8 +574,9 @@ class Database extends Events.EventEmitter {
|
|
|
537
574
|
/*
|
|
538
575
|
* Promise / async-await API.
|
|
539
576
|
* Each *Async method wraps its callback counterpart; the callback API
|
|
540
|
-
* stays untouched.
|
|
541
|
-
*
|
|
577
|
+
* stays untouched. The promises resolve with the rows alone unless
|
|
578
|
+
* { withMeta: true } is passed, which resolves the full
|
|
579
|
+
* { rows, fields, affectedRows, recordCounts, warnings } result.
|
|
542
580
|
*/
|
|
543
581
|
|
|
544
582
|
queryAsync(query: string, params?: QueryParams, options?: InternalQueryOptions): Promise<any[]> {
|
package/src/wire/serialize.ts
CHANGED
|
@@ -197,7 +197,11 @@ export class BlrReader {
|
|
|
197
197
|
value = this.buffer.readInt16LE(this.pos);
|
|
198
198
|
break;
|
|
199
199
|
case 4:
|
|
200
|
-
value = this.buffer.readInt32LE(this.pos)
|
|
200
|
+
value = this.buffer.readInt32LE(this.pos);
|
|
201
|
+
break;
|
|
202
|
+
case 8:
|
|
203
|
+
// e.g. record counts above 2^31 (isc_info_sql_records)
|
|
204
|
+
value = Number(this.buffer.readBigInt64LE(this.pos));
|
|
201
205
|
}
|
|
202
206
|
this.pos += len;
|
|
203
207
|
return value;
|
|
@@ -440,8 +444,10 @@ export class XdrReader {
|
|
|
440
444
|
frow?: any;
|
|
441
445
|
/** rows decoded so far in this call */
|
|
442
446
|
frows?: any[];
|
|
443
|
-
/** cached object-row keys (column aliases) */
|
|
447
|
+
/** cached object-row keys (column aliases, qualified when nestTables is set) */
|
|
444
448
|
fcols?: string[];
|
|
449
|
+
/** cached per-column table keys when nestTables === true */
|
|
450
|
+
ftables?: string[];
|
|
445
451
|
|
|
446
452
|
constructor(buffer: Buffer) {
|
|
447
453
|
this.buffer = buffer;
|