node-firebird 2.11.0 → 2.13.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 +287 -9
- 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 +152 -5
- package/lib/uri.js +53 -2
- package/lib/utils.d.ts +12 -0
- package/lib/utils.js +20 -2
- package/lib/wire/batch-stream.d.ts +26 -0
- package/lib/wire/batch-stream.js +109 -0
- package/lib/wire/codepages.d.ts +23 -0
- package/lib/wire/codepages.js +137 -0
- package/lib/wire/connection.d.ts +44 -4
- package/lib/wire/connection.js +344 -80
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +12 -0
- package/lib/wire/database.d.ts +18 -0
- package/lib/wire/database.js +40 -13
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +14 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +33 -0
- package/lib/wire/transaction.js +156 -13
- package/lib/wire/xsqlvar.d.ts +118 -2
- package/lib/wire/xsqlvar.js +271 -34
- package/package.json +19 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +153 -6
- package/src/uri.ts +54 -2
- package/src/utils.ts +19 -1
- package/src/wire/batch-stream.ts +121 -0
- package/src/wire/codepages.ts +147 -0
- package/src/wire/connection.ts +374 -86
- package/src/wire/const.ts +13 -0
- package/src/wire/database.ts +46 -15
- package/src/wire/serialize.ts +16 -1
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +166 -19
- package/src/wire/xsqlvar.ts +298 -34
package/src/pool.ts
CHANGED
|
@@ -26,7 +26,12 @@ type AttachFn = (options: any, callback: Callback) => void;
|
|
|
26
26
|
*
|
|
27
27
|
* Options: max (factory argument), options.min (floor the reaper never
|
|
28
28
|
* shrinks below), options.idleTimeoutMillis (close idle connections after
|
|
29
|
-
* this many ms; 0/absent = never), options.connectTimeout
|
|
29
|
+
* this many ms; 0/absent = never), options.connectTimeout,
|
|
30
|
+
* options.maxUses (retire a connection after this many checkouts — pg's
|
|
31
|
+
* maxUses), options.maxLifetimeMillis (retire a connection this many ms
|
|
32
|
+
* after it was created — Postgres.js's max_lifetime). Retirement happens
|
|
33
|
+
* when the connection is returned to the pool, and the sweep also closes
|
|
34
|
+
* over-lifetime idle connections; a replacement is created on demand.
|
|
30
35
|
*/
|
|
31
36
|
class Pool extends Events.EventEmitter {
|
|
32
37
|
attach: AttachFn;
|
|
@@ -37,6 +42,8 @@ class Pool extends Events.EventEmitter {
|
|
|
37
42
|
max: number;
|
|
38
43
|
min: number;
|
|
39
44
|
idleTimeoutMillis: number;
|
|
45
|
+
maxUses: number;
|
|
46
|
+
maxLifetimeMillis: number;
|
|
40
47
|
pending: Callback[];
|
|
41
48
|
options: any;
|
|
42
49
|
_destroyed: boolean;
|
|
@@ -52,22 +59,50 @@ class Pool extends Events.EventEmitter {
|
|
|
52
59
|
this.max = max || 4;
|
|
53
60
|
this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
|
|
54
61
|
this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
|
|
62
|
+
this.maxUses = (options && options.maxUses > 0) ? options.maxUses : 0;
|
|
63
|
+
this.maxLifetimeMillis = (options && options.maxLifetimeMillis > 0) ? options.maxLifetimeMillis : 0;
|
|
55
64
|
this.pending = []; // callbacks waiting for a free slot
|
|
56
65
|
this.options = options;
|
|
57
66
|
this._destroyed = false; // true after destroy() — prevents further use
|
|
58
67
|
this._reaper = null;
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
// the sweep serves both idle eviction and lifetime retirement of
|
|
70
|
+
// idle connections; base its cadence on the tightest configured limit
|
|
71
|
+
var sweepBasis = Math.min(this.idleTimeoutMillis || Infinity, this.maxLifetimeMillis || Infinity);
|
|
72
|
+
if (sweepBasis !== Infinity) {
|
|
61
73
|
var self = this;
|
|
62
|
-
// Sweep at half the
|
|
63
|
-
// connection lives at most ~1.5x
|
|
74
|
+
// Sweep at half the basis (bounded to 100ms..30s) so a
|
|
75
|
+
// connection lives at most ~1.5x its limit. unref() keeps
|
|
64
76
|
// the timer from holding the process open.
|
|
65
|
-
var interval = Math.min(Math.max(
|
|
77
|
+
var interval = Math.min(Math.max(sweepBasis / 2, 100), 30000);
|
|
66
78
|
this._reaper = setInterval(function() { self._reap(); }, interval);
|
|
67
79
|
if (this._reaper.unref) this._reaper.unref();
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
/** True when the connection exceeded maxUses / maxLifetimeMillis.
|
|
84
|
+
* Both stamps are set unconditionally when the pool creates the
|
|
85
|
+
* connection, so they can be read bare here. */
|
|
86
|
+
_isExpired(db: any): boolean {
|
|
87
|
+
if (this.maxUses > 0 && db.__poolUseCount >= this.maxUses) return true;
|
|
88
|
+
if (this.maxLifetimeMillis > 0 && Date.now() - db.__poolCreatedAt >= this.maxLifetimeMillis) return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Close a healthy pooled connection for good (reaper/retirement path). */
|
|
93
|
+
_retire(db: any): void {
|
|
94
|
+
var self = this;
|
|
95
|
+
this._forget(db);
|
|
96
|
+
db.connection._pooled = false;
|
|
97
|
+
try {
|
|
98
|
+
db.detach(function(err?: any) {
|
|
99
|
+
if (err) self._emitError(err, db);
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
self._emitError(e, db);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
/** Physical connections owned by the pool (idle + in use). */
|
|
72
107
|
get totalCount(): number {
|
|
73
108
|
return this.internaldb.length;
|
|
@@ -124,19 +159,18 @@ class Pool extends Events.EventEmitter {
|
|
|
124
159
|
self._forget(db);
|
|
125
160
|
return;
|
|
126
161
|
}
|
|
162
|
+
// lifetime retirement applies even below min — recycling is the
|
|
163
|
+
// point; replacements are created on demand
|
|
164
|
+
if (self._isExpired(db)) {
|
|
165
|
+
self._retire(db);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!self.idleTimeoutMillis) return;
|
|
127
169
|
if (self.internaldb.length <= self.min) return;
|
|
128
170
|
var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
|
|
129
171
|
if (now - idleSince < self.idleTimeoutMillis) return;
|
|
130
172
|
|
|
131
|
-
self.
|
|
132
|
-
db.connection._pooled = false;
|
|
133
|
-
try {
|
|
134
|
-
db.detach(function(err?: any) {
|
|
135
|
-
if (err) self._emitError(err, db);
|
|
136
|
-
});
|
|
137
|
-
} catch (e) {
|
|
138
|
-
self._emitError(e, db);
|
|
139
|
-
}
|
|
173
|
+
self._retire(db);
|
|
140
174
|
});
|
|
141
175
|
}
|
|
142
176
|
|
|
@@ -175,6 +209,7 @@ class Pool extends Events.EventEmitter {
|
|
|
175
209
|
}
|
|
176
210
|
// Idle connection available — hand it out immediately.
|
|
177
211
|
self.dbinuse++;
|
|
212
|
+
db.__poolUseCount = (db.__poolUseCount || 0) + 1;
|
|
178
213
|
self.emit('acquire', db);
|
|
179
214
|
cb(null, db);
|
|
180
215
|
} else {
|
|
@@ -235,6 +270,8 @@ class Pool extends Events.EventEmitter {
|
|
|
235
270
|
|
|
236
271
|
if (!err) {
|
|
237
272
|
self.dbinuse++;
|
|
273
|
+
db.__poolCreatedAt = Date.now();
|
|
274
|
+
db.__poolUseCount = 1;
|
|
238
275
|
self.internaldb.push(db);
|
|
239
276
|
db.on('detach', function () {
|
|
240
277
|
// also in pool (could be a twice call to detach)
|
|
@@ -244,6 +281,12 @@ class Pool extends Events.EventEmitter {
|
|
|
244
281
|
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
|
|
245
282
|
self.internaldb.splice(self.internaldb.indexOf(db), 1);
|
|
246
283
|
self.emit('remove', db);
|
|
284
|
+
} else if (self._isExpired(db)) {
|
|
285
|
+
// worn out (maxUses / maxLifetimeMillis): close it
|
|
286
|
+
// for good instead of returning it to the idle
|
|
287
|
+
// pool. The re-fired detach event exits early via
|
|
288
|
+
// the internaldb guard above.
|
|
289
|
+
self._retire(db);
|
|
247
290
|
} else {
|
|
248
291
|
db.__poolIdleSince = Date.now();
|
|
249
292
|
self.pooldb.push(db);
|
|
@@ -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
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// They now live in the TypeScript source tree and are compiled into the
|
|
6
6
|
// published declaration files.
|
|
7
7
|
|
|
8
|
-
import type { Readable } from 'stream';
|
|
8
|
+
import type { Readable, Writable } 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,89 @@ 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[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Options for batchStream: the executeBatch options plus stream tuning. */
|
|
215
|
+
export type BatchStreamOptions = BatchOptions & {
|
|
216
|
+
/** Rows buffered per executeBatch flush (default 1000). */
|
|
217
|
+
flushRows?: number;
|
|
218
|
+
/** Writable highWaterMark in rows (default: flushRows). */
|
|
219
|
+
highWaterMark?: number;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** The Writable returned by batchStream, with totals valid after 'finish'. */
|
|
223
|
+
export interface BatchStream extends Writable {
|
|
224
|
+
/** Records the server processed so far. */
|
|
225
|
+
recordCount: number;
|
|
226
|
+
/** Sum of per-record update counts so far. */
|
|
227
|
+
affectedRows: number;
|
|
142
228
|
}
|
|
143
229
|
|
|
144
230
|
export type QueryStreamOptions = QueryOptions & {
|
|
@@ -152,11 +238,18 @@ export type QueryStreamOptions = QueryOptions & {
|
|
|
152
238
|
}
|
|
153
239
|
|
|
154
240
|
export interface Database {
|
|
241
|
+
/**
|
|
242
|
+
* Tagged-template query API (Postgres.js-style): interpolated values
|
|
243
|
+
* become positional parameters, `sql('NAME')` quotes an identifier,
|
|
244
|
+
* embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
|
|
245
|
+
* The returned query is a lazy thenable — it executes once, on await.
|
|
246
|
+
*/
|
|
247
|
+
sql: SqlTag;
|
|
155
248
|
detach(callback?: SimpleCallback): Database;
|
|
156
249
|
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
|
|
157
250
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
158
|
-
query(query: string, params: QueryParams, callback:
|
|
159
|
-
execute(query: string, params: QueryParams, callback:
|
|
251
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
252
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
160
253
|
/** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
|
|
161
254
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
|
|
162
255
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -167,6 +260,13 @@ export interface Database {
|
|
|
167
260
|
* fetch and releases the statement.
|
|
168
261
|
*/
|
|
169
262
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
263
|
+
/**
|
|
264
|
+
* Bulk-insert Writable (COPY FROM analogue, Firebird 4.0+): write
|
|
265
|
+
* parameter-array rows; they are flushed in chunks through the batch
|
|
266
|
+
* API. Runs its own transaction — committed on finish, rolled back on
|
|
267
|
+
* error/destroy. BLOB columns accept Buffers/strings.
|
|
268
|
+
*/
|
|
269
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
170
270
|
drop(callback: SimpleCallback): void;
|
|
171
271
|
escape(value: any): string;
|
|
172
272
|
attachEvent(callback: any): this;
|
|
@@ -176,8 +276,11 @@ export interface Database {
|
|
|
176
276
|
createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
|
|
177
277
|
|
|
178
278
|
// Promise / async-await API (see README § Promises / async–await).
|
|
179
|
-
//
|
|
279
|
+
// Pass { withMeta: true } to resolve with the full QueryResult
|
|
280
|
+
// (rows + fields + affectedRows + warnings) instead of bare rows.
|
|
281
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
180
282
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
283
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
181
284
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
182
285
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
183
286
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -201,9 +304,17 @@ export interface Database {
|
|
|
201
304
|
}
|
|
202
305
|
|
|
203
306
|
export interface Transaction {
|
|
307
|
+
/** Tagged-template query API running inside this transaction (see Database.sql). */
|
|
308
|
+
sql: SqlTag;
|
|
309
|
+
/**
|
|
310
|
+
* Run `work` inside a savepoint: released on resolve, rolled back TO
|
|
311
|
+
* (undoing only work's changes) on reject — the transaction stays
|
|
312
|
+
* usable either way. Nestable.
|
|
313
|
+
*/
|
|
314
|
+
savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
|
|
204
315
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
205
|
-
query(query: string, params: QueryParams, callback:
|
|
206
|
-
execute(query: string, params: QueryParams, callback:
|
|
316
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
317
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
207
318
|
/** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
|
|
208
319
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
209
320
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -213,13 +324,20 @@ export interface Transaction {
|
|
|
213
324
|
* transaction is NOT committed when the stream ends.
|
|
214
325
|
*/
|
|
215
326
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
327
|
+
/**
|
|
328
|
+
* Bulk-insert Writable inside this transaction (see
|
|
329
|
+
* Database.batchStream); commit/rollback stays with the caller.
|
|
330
|
+
*/
|
|
331
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
216
332
|
commit(callback?: SimpleCallback): void;
|
|
217
333
|
commitRetaining(callback?: SimpleCallback): void;
|
|
218
334
|
rollback(callback?: SimpleCallback): void;
|
|
219
335
|
rollbackRetaining(callback?: SimpleCallback): void;
|
|
220
336
|
|
|
221
337
|
// Promise / async-await API
|
|
338
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
222
339
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
340
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & { withMeta: true }): Promise<QueryResult<T>>;
|
|
223
341
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
224
342
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
225
343
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -326,6 +444,13 @@ export interface Options {
|
|
|
326
444
|
* per-query `namedPlaceholders: false` override.
|
|
327
445
|
*/
|
|
328
446
|
namedPlaceholders?: boolean;
|
|
447
|
+
/**
|
|
448
|
+
* Default character set of a NEWLY CREATED database (create /
|
|
449
|
+
* attachOrCreate only). Falls back to the connection `encoding`, then
|
|
450
|
+
* UTF8 — pass e.g. `defaultCharset: 'UTF8'` to keep a modern database
|
|
451
|
+
* default while connecting with a legacy codepage `encoding`.
|
|
452
|
+
*/
|
|
453
|
+
defaultCharset?: string;
|
|
329
454
|
/**
|
|
330
455
|
* Qualify object-row keys by source table (same option as mysql2), so
|
|
331
456
|
* JOINed columns with the same name stop overwriting each other:
|
|
@@ -336,6 +461,14 @@ export interface Options {
|
|
|
336
461
|
* array rows (execute) are unaffected. Overridable per query.
|
|
337
462
|
*/
|
|
338
463
|
nestTables?: boolean | string;
|
|
464
|
+
/**
|
|
465
|
+
* Rewrite object-row keys (Postgres.js `transform` counterpart):
|
|
466
|
+
* `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
|
|
467
|
+
* `(key) => key` mapper. Applied after `lowercase_keys` and to both
|
|
468
|
+
* parts of `nestTables` keys; column metadata keeps raw aliases.
|
|
469
|
+
* Overridable per query.
|
|
470
|
+
*/
|
|
471
|
+
transformKeys?: 'camel' | ((key: string) => string);
|
|
339
472
|
/**
|
|
340
473
|
* TCP keepalive probing to detect dead/stale connections (same option
|
|
341
474
|
* names as mysql2). On by default; set false to disable.
|
|
@@ -374,6 +507,20 @@ export interface Options {
|
|
|
374
507
|
* Default 0 (idle connections are kept forever).
|
|
375
508
|
*/
|
|
376
509
|
idleTimeoutMillis?: number;
|
|
510
|
+
/**
|
|
511
|
+
* Pool only: retire a physical connection after this many checkouts
|
|
512
|
+
* (pg's `maxUses`) — it is closed for good when returned to the pool
|
|
513
|
+
* and replaced on demand. Bounds server-side resource drift on
|
|
514
|
+
* long-lived connections. Default 0 (unlimited uses).
|
|
515
|
+
*/
|
|
516
|
+
maxUses?: number;
|
|
517
|
+
/**
|
|
518
|
+
* Pool only: retire a physical connection this many milliseconds after
|
|
519
|
+
* it was created (Postgres.js's `max_lifetime`), on return to the pool
|
|
520
|
+
* or by the idle sweep — even below `min`; replacements are created on
|
|
521
|
+
* demand. Default 0 (unlimited lifetime).
|
|
522
|
+
*/
|
|
523
|
+
maxLifetimeMillis?: number;
|
|
377
524
|
/**
|
|
378
525
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
379
526
|
*
|
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/utils.ts
CHANGED
|
@@ -108,6 +108,21 @@ export const parseDate = (str: string): Date => {
|
|
|
108
108
|
/**
|
|
109
109
|
* Get Error Message per gdscode
|
|
110
110
|
*/
|
|
111
|
+
/**
|
|
112
|
+
* Turn a failed executeBatch completion into the all-or-nothing error
|
|
113
|
+
* shape shared by database.executeBatch and batchStream: the first
|
|
114
|
+
* record's own error (or a synthesized summary), with the full
|
|
115
|
+
* completion attached as err.batchCompletion.
|
|
116
|
+
*/
|
|
117
|
+
export const batchResultToError = (result: { errors: { error: any }[]; errorRecordNumbers: number[] }): any => {
|
|
118
|
+
const first = result.errors.length ? result.errors[0] : null;
|
|
119
|
+
const err: any = first
|
|
120
|
+
? first.error
|
|
121
|
+
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
122
|
+
err.batchCompletion = result;
|
|
123
|
+
return err;
|
|
124
|
+
};
|
|
125
|
+
|
|
111
126
|
export const lookupMessages = (status: FbStatusItem[]): string => {
|
|
112
127
|
const messages = status.map((item) => {
|
|
113
128
|
let text = MessagesError[item.gdscode];
|
|
@@ -143,7 +158,10 @@ export const escape = function(value: any, protocolVersion?: number): string {
|
|
|
143
158
|
case 'number':
|
|
144
159
|
return value.toString();
|
|
145
160
|
case 'string':
|
|
146
|
-
|
|
161
|
+
// Firebird string literals have NO backslash escapes — only the
|
|
162
|
+
// quote is doubled. Doubling backslashes corrupted the data
|
|
163
|
+
// (issue #156: '\' arrived as '\\').
|
|
164
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
if (value instanceof Date)
|