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/README.md +191 -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 +114 -0
- package/lib/uri.js +53 -2
- package/lib/wire/connection.d.ts +6 -0
- package/lib/wire/connection.js +50 -36
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +12 -0
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +27 -7
- 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 +62 -1
- package/lib/wire/xsqlvar.js +165 -6
- package/package.json +1 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +113 -1
- package/src/uri.ts +54 -2
- package/src/wire/connection.ts +59 -37
- package/src/wire/const.ts +13 -0
- package/src/wire/database.ts +31 -8
- package/src/wire/serialize.ts +5 -1
- package/src/wire/transaction.ts +140 -19
- package/src/wire/xsqlvar.ts +170 -5
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Tagged-template query API (Postgres.js-style)
|
|
4
|
+
*
|
|
5
|
+
* db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
|
|
6
|
+
* db.sql('COLUMN NAME') → quoted identifier
|
|
7
|
+
*
|
|
8
|
+
* Interpolated values become positional `?` parameters — never string
|
|
9
|
+
* concatenation — so the API is injection-safe by construction. A query
|
|
10
|
+
* embedded inside another tag is treated as a fragment: its text and
|
|
11
|
+
* parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
|
|
12
|
+
* IN clauses. Execution is lazy (on await/then) and happens exactly once.
|
|
13
|
+
*
|
|
14
|
+
***************************************/
|
|
15
|
+
|
|
16
|
+
import type { QueryOptions, QueryResult } from './types';
|
|
17
|
+
|
|
18
|
+
/** Executor provided by Database/Transaction: runs text+params, resolves rows
|
|
19
|
+
* (or the full QueryResult when options.withMeta is set). */
|
|
20
|
+
export type SqlExecutor = (text: string, params: any[], options?: QueryOptions) => Promise<any>;
|
|
21
|
+
|
|
22
|
+
/** A dynamically quoted identifier produced by sql('name'). */
|
|
23
|
+
export class SqlIdentifier {
|
|
24
|
+
name: string;
|
|
25
|
+
constructor(name: string) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Quote a (possibly dot-qualified) identifier for dialect 3: each part is
|
|
32
|
+
* wrapped in double quotes with embedded quotes doubled, so user input can
|
|
33
|
+
* never break out of the identifier position.
|
|
34
|
+
*/
|
|
35
|
+
export function quoteIdentifier(name: string): string {
|
|
36
|
+
return String(name)
|
|
37
|
+
.split('.')
|
|
38
|
+
.map((part) => '"' + part.replace(/"/g, '""') + '"')
|
|
39
|
+
.join('.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compiled form of a tagged query: SQL text with `?` placeholders + params. */
|
|
43
|
+
export interface CompiledQuery {
|
|
44
|
+
text: string;
|
|
45
|
+
params: any[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compile(strings: readonly string[], values: any[], active?: Set<any>): CompiledQuery {
|
|
49
|
+
let text = '';
|
|
50
|
+
const params: any[] = [];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < strings.length; i++) {
|
|
53
|
+
text += strings[i];
|
|
54
|
+
if (i >= values.length) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const value = values[i];
|
|
58
|
+
|
|
59
|
+
if (value instanceof SqlIdentifier) {
|
|
60
|
+
text += quoteIdentifier(value.name);
|
|
61
|
+
} else if (value instanceof SqlQuery) {
|
|
62
|
+
// embedded fragment: splice its text and params in place. The
|
|
63
|
+
// same fragment may appear several times (a DAG), but a fragment
|
|
64
|
+
// containing itself would recurse forever — track the expansion
|
|
65
|
+
// stack and reject cycles with a diagnosable error.
|
|
66
|
+
active = active || new Set();
|
|
67
|
+
if (active.has(value)) {
|
|
68
|
+
throw new Error('circular sql fragment: a query is embedded (transitively) inside itself');
|
|
69
|
+
}
|
|
70
|
+
active.add(value);
|
|
71
|
+
const inner = compile(value.strings, value.values, active);
|
|
72
|
+
active.delete(value);
|
|
73
|
+
text += inner.text;
|
|
74
|
+
params.push(...inner.params);
|
|
75
|
+
} else if (Array.isArray(value)) {
|
|
76
|
+
// IN (${[1, 2, 3]}) → IN (?, ?, ?)
|
|
77
|
+
if (!value.length) {
|
|
78
|
+
// '' would compile to `IN ()` — invalid SQL raising a server
|
|
79
|
+
// syntax error the caller never wrote; fail early instead
|
|
80
|
+
throw new Error('cannot interpolate an empty array (would compile to invalid SQL like "IN ()")');
|
|
81
|
+
}
|
|
82
|
+
text += value.map(() => '?').join(', ');
|
|
83
|
+
params.push(...value);
|
|
84
|
+
} else {
|
|
85
|
+
text += '?';
|
|
86
|
+
params.push(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { text, params };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A lazily executed tagged query. Awaiting it (or calling then/catch/
|
|
95
|
+
* finally) runs it through the owning Database/Transaction exactly once;
|
|
96
|
+
* embedding it in another tag uses it as a fragment instead and never
|
|
97
|
+
* executes it.
|
|
98
|
+
*/
|
|
99
|
+
export class SqlQuery<T = any> implements PromiseLike<T[]> {
|
|
100
|
+
readonly strings: readonly string[];
|
|
101
|
+
readonly values: any[];
|
|
102
|
+
private executor: SqlExecutor;
|
|
103
|
+
private queryOptions?: QueryOptions;
|
|
104
|
+
private executed?: Promise<any>;
|
|
105
|
+
private executedMeta?: boolean;
|
|
106
|
+
|
|
107
|
+
constructor(executor: SqlExecutor, strings: readonly string[], values: any[]) {
|
|
108
|
+
this.executor = executor;
|
|
109
|
+
this.strings = strings;
|
|
110
|
+
this.values = values;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The compiled SQL text (`?` placeholders) and parameter array. */
|
|
114
|
+
toQuery(): CompiledQuery {
|
|
115
|
+
return compile(this.strings, this.values);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Attach per-query options (timeout, signal, nestTables, …). Must be
|
|
120
|
+
* called before the query executes — options attached afterwards would
|
|
121
|
+
* be silently ignored, so that throws instead.
|
|
122
|
+
*/
|
|
123
|
+
options(queryOptions: QueryOptions): this {
|
|
124
|
+
if (this.executed) {
|
|
125
|
+
throw new Error('sql query already executed — call .options() before awaiting it');
|
|
126
|
+
}
|
|
127
|
+
this.queryOptions = { ...this.queryOptions, ...queryOptions };
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Execute resolving the full { rows, fields, affectedRows, … } result. */
|
|
132
|
+
withMeta(): Promise<QueryResult<T>> {
|
|
133
|
+
return this.run(true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A query executes exactly once, in the shape of its first consumer
|
|
138
|
+
* (plain rows via then/await, or the full result via withMeta).
|
|
139
|
+
* Consuming it again in the OTHER shape cannot be honoured from the
|
|
140
|
+
* cached promise, so it throws rather than silently returning the
|
|
141
|
+
* wrong shape.
|
|
142
|
+
*/
|
|
143
|
+
private run(withMeta: boolean): Promise<any> {
|
|
144
|
+
if (this.executed) {
|
|
145
|
+
if (withMeta !== this.executedMeta) {
|
|
146
|
+
throw new Error(this.executedMeta
|
|
147
|
+
? 'sql query already executed via .withMeta() — await that result instead of the query'
|
|
148
|
+
: 'sql query already executed as plain rows — call .withMeta() first, or build a new query');
|
|
149
|
+
}
|
|
150
|
+
return this.executed;
|
|
151
|
+
}
|
|
152
|
+
this.executedMeta = withMeta;
|
|
153
|
+
const { text, params } = compile(this.strings, this.values);
|
|
154
|
+
const options = withMeta ? { ...this.queryOptions, withMeta: true } : this.queryOptions;
|
|
155
|
+
this.executed = this.executor(text, params, options);
|
|
156
|
+
return this.executed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
then<R1 = T[], R2 = never>(
|
|
160
|
+
onfulfilled?: ((value: T[]) => R1 | PromiseLike<R1>) | null,
|
|
161
|
+
onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
|
|
162
|
+
): Promise<R1 | R2> {
|
|
163
|
+
return this.run(false).then(onfulfilled, onrejected);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
catch<R = never>(onrejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T[] | R> {
|
|
167
|
+
return this.then(undefined, onrejected);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
finally(onfinally?: (() => void) | null): Promise<T[]> {
|
|
171
|
+
return this.run(false).finally(onfinally) as Promise<T[]>;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The dual-use tag: template tag executes, string call quotes an identifier. */
|
|
176
|
+
export interface SqlTag {
|
|
177
|
+
<T = any>(strings: TemplateStringsArray, ...values: any[]): SqlQuery<T>;
|
|
178
|
+
(identifier: string): SqlIdentifier;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build the `sql` tag for a Database/Transaction. `executor` receives the
|
|
183
|
+
* compiled text, params and per-query options and must return a promise
|
|
184
|
+
* (Database/Transaction pass their queryAsync).
|
|
185
|
+
*/
|
|
186
|
+
export function makeSqlTag(executor: SqlExecutor): SqlTag {
|
|
187
|
+
return function sql(first: any, ...values: any[]): any {
|
|
188
|
+
if (Array.isArray(first) && Object.prototype.hasOwnProperty.call(first, 'raw')) {
|
|
189
|
+
return new SqlQuery(executor, first, values);
|
|
190
|
+
}
|
|
191
|
+
if (typeof first === 'string') {
|
|
192
|
+
return new SqlIdentifier(first);
|
|
193
|
+
}
|
|
194
|
+
throw new Error('sql must be used as a template tag (sql`...`) or called with an identifier string (sql(\'NAME\'))');
|
|
195
|
+
} as SqlTag;
|
|
196
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// published declaration files.
|
|
7
7
|
|
|
8
8
|
import type { Readable } from 'stream';
|
|
9
|
+
import type { SqlTag } from './sql-template';
|
|
10
|
+
|
|
11
|
+
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
9
12
|
|
|
10
13
|
export type DatabaseCallback = (err: any, db: Database) => void;
|
|
11
14
|
export type TransactionCallback = (err: any, transaction: Transaction) => void;
|
|
@@ -139,6 +142,73 @@ export type QueryOptions = {
|
|
|
139
142
|
* unaffected.
|
|
140
143
|
*/
|
|
141
144
|
nestTables?: boolean | string;
|
|
145
|
+
/**
|
|
146
|
+
* Per-query override of the `transformKeys` connection option: rewrite
|
|
147
|
+
* object-row keys — `'camel'` turns `FIRST_NAME` into `firstName`, or
|
|
148
|
+
* pass a custom `(key) => key` mapper. Applied after `lowercase_keys`
|
|
149
|
+
* and to both parts of `nestTables` keys. Column metadata (`fields`,
|
|
150
|
+
* typeCast) keeps the raw server aliases.
|
|
151
|
+
*/
|
|
152
|
+
transformKeys?: 'camel' | ((key: string) => string);
|
|
153
|
+
/**
|
|
154
|
+
* Deliver a full result object `{ rows, fields, affectedRows,
|
|
155
|
+
* recordCounts, warnings }` instead of the bare rows (callback and
|
|
156
|
+
* promise APIs). For DML, `affectedRows` is what the server actually
|
|
157
|
+
* changed (`isc_info_sql_records`, one extra lightweight info request
|
|
158
|
+
* per statement — hence opt-in) and `recordCounts` breaks it down per
|
|
159
|
+
* verb; for SELECT it is the number of rows returned (pg's `rowCount`
|
|
160
|
+
* convention) with no extra round-trip. `warnings` carries any
|
|
161
|
+
* `isc_arg_warning` entries from the execute response. Honoured by
|
|
162
|
+
* query/execute and their *Async wrappers only — ignored by the
|
|
163
|
+
* streaming APIs (sequentially/queryStream, where rows bypass the
|
|
164
|
+
* result) and executeBatch (which has its own completion shape).
|
|
165
|
+
*/
|
|
166
|
+
withMeta?: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Column metadata delivered in withMeta results (`fields`) — the same
|
|
170
|
+
* vocabulary the typeCast hook receives, plus nullable and the relation
|
|
171
|
+
* alias/schema. */
|
|
172
|
+
export interface FieldMetadata {
|
|
173
|
+
type: number;
|
|
174
|
+
typeName: string;
|
|
175
|
+
subType?: number;
|
|
176
|
+
scale?: number;
|
|
177
|
+
length?: number;
|
|
178
|
+
nullable?: boolean;
|
|
179
|
+
field?: string;
|
|
180
|
+
relation?: string;
|
|
181
|
+
relationAlias?: string;
|
|
182
|
+
relationSchema?: string;
|
|
183
|
+
alias?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Per-verb server row counts of an executed DML statement. */
|
|
187
|
+
export interface RecordCounts {
|
|
188
|
+
selectCount: number;
|
|
189
|
+
insertCount: number;
|
|
190
|
+
updateCount: number;
|
|
191
|
+
deleteCount: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** An isc_arg_warning entry from a server response ('warning' driver event
|
|
195
|
+
* and withMeta `warnings`). */
|
|
196
|
+
export interface ServerWarning {
|
|
197
|
+
gdscode: number;
|
|
198
|
+
params?: (string | number)[];
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Full result shape delivered when `withMeta: true` is set. */
|
|
203
|
+
export interface QueryResult<T = any> {
|
|
204
|
+
/** Rows array (SELECT), single row object (RETURNING / procedures), or undefined (plain DML). */
|
|
205
|
+
rows: T[] | T | undefined;
|
|
206
|
+
fields: FieldMetadata[];
|
|
207
|
+
/** DML: rows the server changed; SELECT: rows returned. */
|
|
208
|
+
affectedRows: number;
|
|
209
|
+
/** Set for DML statements only. */
|
|
210
|
+
recordCounts?: RecordCounts;
|
|
211
|
+
warnings: ServerWarning[];
|
|
142
212
|
}
|
|
143
213
|
|
|
144
214
|
export type QueryStreamOptions = QueryOptions & {
|
|
@@ -152,6 +222,13 @@ export type QueryStreamOptions = QueryOptions & {
|
|
|
152
222
|
}
|
|
153
223
|
|
|
154
224
|
export interface Database {
|
|
225
|
+
/**
|
|
226
|
+
* Tagged-template query API (Postgres.js-style): interpolated values
|
|
227
|
+
* become positional parameters, `sql('NAME')` quotes an identifier,
|
|
228
|
+
* embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
|
|
229
|
+
* The returned query is a lazy thenable — it executes once, on await.
|
|
230
|
+
*/
|
|
231
|
+
sql: SqlTag;
|
|
155
232
|
detach(callback?: SimpleCallback): Database;
|
|
156
233
|
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
|
|
157
234
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
@@ -176,8 +253,11 @@ export interface Database {
|
|
|
176
253
|
createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
|
|
177
254
|
|
|
178
255
|
// Promise / async-await API (see README § Promises / async–await).
|
|
179
|
-
//
|
|
256
|
+
// Pass { withMeta: true } to resolve with the full QueryResult
|
|
257
|
+
// (rows + fields + affectedRows + warnings) instead of bare rows.
|
|
258
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
180
259
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
260
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
181
261
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
182
262
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
183
263
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -201,6 +281,14 @@ export interface Database {
|
|
|
201
281
|
}
|
|
202
282
|
|
|
203
283
|
export interface Transaction {
|
|
284
|
+
/** Tagged-template query API running inside this transaction (see Database.sql). */
|
|
285
|
+
sql: SqlTag;
|
|
286
|
+
/**
|
|
287
|
+
* Run `work` inside a savepoint: released on resolve, rolled back TO
|
|
288
|
+
* (undoing only work's changes) on reject — the transaction stays
|
|
289
|
+
* usable either way. Nestable.
|
|
290
|
+
*/
|
|
291
|
+
savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
|
|
204
292
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
205
293
|
query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
206
294
|
execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
@@ -219,7 +307,9 @@ export interface Transaction {
|
|
|
219
307
|
rollbackRetaining(callback?: SimpleCallback): void;
|
|
220
308
|
|
|
221
309
|
// Promise / async-await API
|
|
310
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
222
311
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
312
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
223
313
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
224
314
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
225
315
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -336,6 +426,14 @@ export interface Options {
|
|
|
336
426
|
* array rows (execute) are unaffected. Overridable per query.
|
|
337
427
|
*/
|
|
338
428
|
nestTables?: boolean | string;
|
|
429
|
+
/**
|
|
430
|
+
* Rewrite object-row keys (Postgres.js `transform` counterpart):
|
|
431
|
+
* `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
|
|
432
|
+
* `(key) => key` mapper. Applied after `lowercase_keys` and to both
|
|
433
|
+
* parts of `nestTables` keys; column metadata keeps raw aliases.
|
|
434
|
+
* Overridable per query.
|
|
435
|
+
*/
|
|
436
|
+
transformKeys?: 'camel' | ((key: string) => string);
|
|
339
437
|
/**
|
|
340
438
|
* TCP keepalive probing to detect dead/stale connections (same option
|
|
341
439
|
* names as mysql2). On by default; set false to disable.
|
|
@@ -374,6 +472,20 @@ export interface Options {
|
|
|
374
472
|
* Default 0 (idle connections are kept forever).
|
|
375
473
|
*/
|
|
376
474
|
idleTimeoutMillis?: number;
|
|
475
|
+
/**
|
|
476
|
+
* Pool only: retire a physical connection after this many checkouts
|
|
477
|
+
* (pg's `maxUses`) — it is closed for good when returned to the pool
|
|
478
|
+
* and replaced on demand. Bounds server-side resource drift on
|
|
479
|
+
* long-lived connections. Default 0 (unlimited uses).
|
|
480
|
+
*/
|
|
481
|
+
maxUses?: number;
|
|
482
|
+
/**
|
|
483
|
+
* Pool only: retire a physical connection this many milliseconds after
|
|
484
|
+
* it was created (Postgres.js's `max_lifetime`), on return to the pool
|
|
485
|
+
* or by the idle sweep — even below `min`; replacements are created on
|
|
486
|
+
* demand. Default 0 (unlimited lifetime).
|
|
487
|
+
*/
|
|
488
|
+
maxLifetimeMillis?: number;
|
|
377
489
|
/**
|
|
378
490
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
379
491
|
*
|
package/src/uri.ts
CHANGED
|
@@ -198,7 +198,59 @@ export function parseConnectionString(str: string): Options {
|
|
|
198
198
|
*/
|
|
199
199
|
export function normalizeOptions<T>(options: T | string): T {
|
|
200
200
|
if (typeof options === 'string') {
|
|
201
|
-
|
|
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 || {};
|
|
@@ -2494,7 +2515,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2494
2515
|
|
|
2495
2516
|
if (custom.asObject && !data.fcols) {
|
|
2496
2517
|
const nest = Xsql.resolveNestTables(custom, cnx.options);
|
|
2497
|
-
const
|
|
2518
|
+
const transform = Xsql.resolveKeyTransform(custom, cnx.options);
|
|
2519
|
+
const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
|
|
2498
2520
|
data.fcols = columnKeys.map((k) => k.key);
|
|
2499
2521
|
if (nest === true) {
|
|
2500
2522
|
// computeColumnKeys always sets table when nesting
|
package/src/wire/const.ts
CHANGED
|
@@ -587,6 +587,12 @@ const sqlInfo = {
|
|
|
587
587
|
isc_info_sql_stmt_type : 21,
|
|
588
588
|
isc_info_sql_get_plan : 22,
|
|
589
589
|
isc_info_sql_records : 23,
|
|
590
|
+
// per-verb row counts nested inside an isc_info_sql_records cluster
|
|
591
|
+
// (inf_pub.h isc_info_req_*)
|
|
592
|
+
isc_info_req_select_count : 13,
|
|
593
|
+
isc_info_req_insert_count : 14,
|
|
594
|
+
isc_info_req_update_count : 15,
|
|
595
|
+
isc_info_req_delete_count : 16,
|
|
590
596
|
isc_info_sql_batch_fetch : 24,
|
|
591
597
|
isc_info_sql_relation_alias : 25, // >: 2.0
|
|
592
598
|
isc_info_sql_explain_plan : 26, // >= 3.0
|
|
@@ -669,6 +675,12 @@ const DESCRIBE_WITH_SCHEMA = [
|
|
|
669
675
|
sqlInfo.isc_info_sql_describe_end
|
|
670
676
|
];
|
|
671
677
|
|
|
678
|
+
// op_info_sql request for the per-verb DML row counts of an executed
|
|
679
|
+
// statement (withMeta / affectedRows).
|
|
680
|
+
const RECORDS_INFO = [
|
|
681
|
+
sqlInfo.isc_info_sql_records,
|
|
682
|
+
];
|
|
683
|
+
|
|
672
684
|
/***********************/
|
|
673
685
|
/* ISC Services */
|
|
674
686
|
/***********************/
|
|
@@ -910,6 +922,7 @@ const Const = Object.freeze({
|
|
|
910
922
|
...defaultOptions,
|
|
911
923
|
DESCRIBE,
|
|
912
924
|
DESCRIBE_WITH_SCHEMA,
|
|
925
|
+
RECORDS_INFO,
|
|
913
926
|
...dpb,
|
|
914
927
|
...dsql,
|
|
915
928
|
...fetchOp,
|