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
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/wire/connection.d.ts
CHANGED
|
@@ -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;
|
package/lib/wire/connection.js
CHANGED
|
@@ -107,31 +107,8 @@ function statementCacheLimit(options) {
|
|
|
107
107
|
}
|
|
108
108
|
return 0;
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
|
|
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)) {
|
package/lib/wire/const.d.ts
CHANGED
|
@@ -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;
|