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
|
@@ -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
|
-
import type { Readable } from 'stream';
|
|
1
|
+
import type { Readable, Writable } from 'stream';
|
|
2
|
+
import type { SqlTag } from './sql-template';
|
|
3
|
+
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
2
4
|
export type DatabaseCallback = (err: any, db: Database) => void;
|
|
3
5
|
export type TransactionCallback = (err: any, transaction: Transaction) => void;
|
|
4
6
|
export type QueryCallback = (err: any, result: any[]) => void;
|
|
@@ -127,7 +129,84 @@ export type QueryOptions = {
|
|
|
127
129
|
* unaffected.
|
|
128
130
|
*/
|
|
129
131
|
nestTables?: boolean | string;
|
|
132
|
+
/**
|
|
133
|
+
* Per-query override of the `transformKeys` connection option: rewrite
|
|
134
|
+
* object-row keys — `'camel'` turns `FIRST_NAME` into `firstName`, or
|
|
135
|
+
* pass a custom `(key) => key` mapper. Applied after `lowercase_keys`
|
|
136
|
+
* and to both parts of `nestTables` keys. Column metadata (`fields`,
|
|
137
|
+
* typeCast) keeps the raw server aliases.
|
|
138
|
+
*/
|
|
139
|
+
transformKeys?: 'camel' | ((key: string) => string);
|
|
140
|
+
/**
|
|
141
|
+
* Deliver a full result object `{ rows, fields, affectedRows,
|
|
142
|
+
* recordCounts, warnings }` instead of the bare rows (callback and
|
|
143
|
+
* promise APIs). For DML, `affectedRows` is what the server actually
|
|
144
|
+
* changed (`isc_info_sql_records`, one extra lightweight info request
|
|
145
|
+
* per statement — hence opt-in) and `recordCounts` breaks it down per
|
|
146
|
+
* verb; for SELECT it is the number of rows returned (pg's `rowCount`
|
|
147
|
+
* convention) with no extra round-trip. `warnings` carries any
|
|
148
|
+
* `isc_arg_warning` entries from the execute response. Honoured by
|
|
149
|
+
* query/execute and their *Async wrappers only — ignored by the
|
|
150
|
+
* streaming APIs (sequentially/queryStream, where rows bypass the
|
|
151
|
+
* result) and executeBatch (which has its own completion shape).
|
|
152
|
+
*/
|
|
153
|
+
withMeta?: boolean;
|
|
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
|
+
}
|
|
196
|
+
/** Options for batchStream: the executeBatch options plus stream tuning. */
|
|
197
|
+
export type BatchStreamOptions = BatchOptions & {
|
|
198
|
+
/** Rows buffered per executeBatch flush (default 1000). */
|
|
199
|
+
flushRows?: number;
|
|
200
|
+
/** Writable highWaterMark in rows (default: flushRows). */
|
|
201
|
+
highWaterMark?: number;
|
|
130
202
|
};
|
|
203
|
+
/** The Writable returned by batchStream, with totals valid after 'finish'. */
|
|
204
|
+
export interface BatchStream extends Writable {
|
|
205
|
+
/** Records the server processed so far. */
|
|
206
|
+
recordCount: number;
|
|
207
|
+
/** Sum of per-record update counts so far. */
|
|
208
|
+
affectedRows: number;
|
|
209
|
+
}
|
|
131
210
|
export type QueryStreamOptions = QueryOptions & {
|
|
132
211
|
/**
|
|
133
212
|
* Rows buffered internally before fetching pauses (object-mode
|
|
@@ -138,11 +217,18 @@ export type QueryStreamOptions = QueryOptions & {
|
|
|
138
217
|
asObject?: boolean;
|
|
139
218
|
};
|
|
140
219
|
export interface Database {
|
|
220
|
+
/**
|
|
221
|
+
* Tagged-template query API (Postgres.js-style): interpolated values
|
|
222
|
+
* become positional parameters, `sql('NAME')` quotes an identifier,
|
|
223
|
+
* embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
|
|
224
|
+
* The returned query is a lazy thenable — it executes once, on await.
|
|
225
|
+
*/
|
|
226
|
+
sql: SqlTag;
|
|
141
227
|
detach(callback?: SimpleCallback): Database;
|
|
142
228
|
transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
|
|
143
229
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
144
|
-
query(query: string, params: QueryParams, callback:
|
|
145
|
-
execute(query: string, params: QueryParams, callback:
|
|
230
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
231
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
146
232
|
/** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
|
|
147
233
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
|
|
148
234
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -153,6 +239,13 @@ export interface Database {
|
|
|
153
239
|
* fetch and releases the statement.
|
|
154
240
|
*/
|
|
155
241
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
242
|
+
/**
|
|
243
|
+
* Bulk-insert Writable (COPY FROM analogue, Firebird 4.0+): write
|
|
244
|
+
* parameter-array rows; they are flushed in chunks through the batch
|
|
245
|
+
* API. Runs its own transaction — committed on finish, rolled back on
|
|
246
|
+
* error/destroy. BLOB columns accept Buffers/strings.
|
|
247
|
+
*/
|
|
248
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
156
249
|
drop(callback: SimpleCallback): void;
|
|
157
250
|
escape(value: any): string;
|
|
158
251
|
attachEvent(callback: any): this;
|
|
@@ -160,7 +253,13 @@ export interface Database {
|
|
|
160
253
|
alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
|
|
161
254
|
dropTablespace(name: string, callback?: QueryCallback): Database;
|
|
162
255
|
createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
|
|
256
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
|
|
257
|
+
withMeta: true;
|
|
258
|
+
}): Promise<QueryResult<T>>;
|
|
163
259
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
260
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
|
|
261
|
+
withMeta: true;
|
|
262
|
+
}): Promise<QueryResult<T>>;
|
|
164
263
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
165
264
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
166
265
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -183,9 +282,17 @@ export interface Database {
|
|
|
183
282
|
cancelAsync(kind?: number): Promise<void>;
|
|
184
283
|
}
|
|
185
284
|
export interface Transaction {
|
|
285
|
+
/** Tagged-template query API running inside this transaction (see Database.sql). */
|
|
286
|
+
sql: SqlTag;
|
|
287
|
+
/**
|
|
288
|
+
* Run `work` inside a savepoint: released on resolve, rolled back TO
|
|
289
|
+
* (undoing only work's changes) on reject — the transaction stays
|
|
290
|
+
* usable either way. Nestable.
|
|
291
|
+
*/
|
|
292
|
+
savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
|
|
186
293
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
187
|
-
query(query: string, params: QueryParams, callback:
|
|
188
|
-
execute(query: string, params: QueryParams, callback:
|
|
294
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
295
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
189
296
|
/** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
|
|
190
297
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
191
298
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -195,11 +302,22 @@ export interface Transaction {
|
|
|
195
302
|
* transaction is NOT committed when the stream ends.
|
|
196
303
|
*/
|
|
197
304
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
305
|
+
/**
|
|
306
|
+
* Bulk-insert Writable inside this transaction (see
|
|
307
|
+
* Database.batchStream); commit/rollback stays with the caller.
|
|
308
|
+
*/
|
|
309
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
198
310
|
commit(callback?: SimpleCallback): void;
|
|
199
311
|
commitRetaining(callback?: SimpleCallback): void;
|
|
200
312
|
rollback(callback?: SimpleCallback): void;
|
|
201
313
|
rollbackRetaining(callback?: SimpleCallback): void;
|
|
314
|
+
queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
|
|
315
|
+
withMeta: true;
|
|
316
|
+
}): Promise<QueryResult<T>>;
|
|
202
317
|
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
318
|
+
executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
|
|
319
|
+
withMeta: true;
|
|
320
|
+
}): Promise<QueryResult<T>>;
|
|
203
321
|
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
204
322
|
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
205
323
|
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
@@ -266,6 +384,13 @@ export interface Options {
|
|
|
266
384
|
* per-query `namedPlaceholders: false` override.
|
|
267
385
|
*/
|
|
268
386
|
namedPlaceholders?: boolean;
|
|
387
|
+
/**
|
|
388
|
+
* Default character set of a NEWLY CREATED database (create /
|
|
389
|
+
* attachOrCreate only). Falls back to the connection `encoding`, then
|
|
390
|
+
* UTF8 — pass e.g. `defaultCharset: 'UTF8'` to keep a modern database
|
|
391
|
+
* default while connecting with a legacy codepage `encoding`.
|
|
392
|
+
*/
|
|
393
|
+
defaultCharset?: string;
|
|
269
394
|
/**
|
|
270
395
|
* Qualify object-row keys by source table (same option as mysql2), so
|
|
271
396
|
* JOINed columns with the same name stop overwriting each other:
|
|
@@ -276,6 +401,14 @@ export interface Options {
|
|
|
276
401
|
* array rows (execute) are unaffected. Overridable per query.
|
|
277
402
|
*/
|
|
278
403
|
nestTables?: boolean | string;
|
|
404
|
+
/**
|
|
405
|
+
* Rewrite object-row keys (Postgres.js `transform` counterpart):
|
|
406
|
+
* `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
|
|
407
|
+
* `(key) => key` mapper. Applied after `lowercase_keys` and to both
|
|
408
|
+
* parts of `nestTables` keys; column metadata keeps raw aliases.
|
|
409
|
+
* Overridable per query.
|
|
410
|
+
*/
|
|
411
|
+
transformKeys?: 'camel' | ((key: string) => string);
|
|
279
412
|
/**
|
|
280
413
|
* TCP keepalive probing to detect dead/stale connections (same option
|
|
281
414
|
* names as mysql2). On by default; set false to disable.
|
|
@@ -314,6 +447,20 @@ export interface Options {
|
|
|
314
447
|
* Default 0 (idle connections are kept forever).
|
|
315
448
|
*/
|
|
316
449
|
idleTimeoutMillis?: number;
|
|
450
|
+
/**
|
|
451
|
+
* Pool only: retire a physical connection after this many checkouts
|
|
452
|
+
* (pg's `maxUses`) — it is closed for good when returned to the pool
|
|
453
|
+
* and replaced on demand. Bounds server-side resource drift on
|
|
454
|
+
* long-lived connections. Default 0 (unlimited uses).
|
|
455
|
+
*/
|
|
456
|
+
maxUses?: number;
|
|
457
|
+
/**
|
|
458
|
+
* Pool only: retire a physical connection this many milliseconds after
|
|
459
|
+
* it was created (Postgres.js's `max_lifetime`), on return to the pool
|
|
460
|
+
* or by the idle sweep — even below `min`; replacements are created on
|
|
461
|
+
* demand. Default 0 (unlimited lifetime).
|
|
462
|
+
*/
|
|
463
|
+
maxLifetimeMillis?: number;
|
|
317
464
|
/**
|
|
318
465
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
319
466
|
*
|
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/utils.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export declare const parseDate: (str: string) => Date;
|
|
|
6
6
|
/**
|
|
7
7
|
* Get Error Message per gdscode
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Turn a failed executeBatch completion into the all-or-nothing error
|
|
11
|
+
* shape shared by database.executeBatch and batchStream: the first
|
|
12
|
+
* record's own error (or a synthesized summary), with the full
|
|
13
|
+
* completion attached as err.batchCompletion.
|
|
14
|
+
*/
|
|
15
|
+
export declare const batchResultToError: (result: {
|
|
16
|
+
errors: {
|
|
17
|
+
error: any;
|
|
18
|
+
}[];
|
|
19
|
+
errorRecordNumbers: number[];
|
|
20
|
+
}) => any;
|
|
9
21
|
export declare const lookupMessages: (status: FbStatusItem[]) => string;
|
|
10
22
|
/**
|
|
11
23
|
* Escape value
|
package/lib/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.escape = exports.lookupMessages = exports.parseDate = void 0;
|
|
6
|
+
exports.escape = exports.lookupMessages = exports.batchResultToError = exports.parseDate = void 0;
|
|
7
7
|
exports.noop = noop;
|
|
8
8
|
const firebird_msg_json_1 = __importDefault(require("./firebird.msg.json"));
|
|
9
9
|
const const_1 = __importDefault(require("./wire/const"));
|
|
@@ -97,6 +97,21 @@ exports.parseDate = parseDate;
|
|
|
97
97
|
/**
|
|
98
98
|
* Get Error Message per gdscode
|
|
99
99
|
*/
|
|
100
|
+
/**
|
|
101
|
+
* Turn a failed executeBatch completion into the all-or-nothing error
|
|
102
|
+
* shape shared by database.executeBatch and batchStream: the first
|
|
103
|
+
* record's own error (or a synthesized summary), with the full
|
|
104
|
+
* completion attached as err.batchCompletion.
|
|
105
|
+
*/
|
|
106
|
+
const batchResultToError = (result) => {
|
|
107
|
+
const first = result.errors.length ? result.errors[0] : null;
|
|
108
|
+
const err = first
|
|
109
|
+
? first.error
|
|
110
|
+
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
111
|
+
err.batchCompletion = result;
|
|
112
|
+
return err;
|
|
113
|
+
};
|
|
114
|
+
exports.batchResultToError = batchResultToError;
|
|
100
115
|
const lookupMessages = (status) => {
|
|
101
116
|
const messages = status.map((item) => {
|
|
102
117
|
let text = MessagesError[item.gdscode];
|
|
@@ -130,7 +145,10 @@ const escape = function (value, protocolVersion) {
|
|
|
130
145
|
case 'number':
|
|
131
146
|
return value.toString();
|
|
132
147
|
case 'string':
|
|
133
|
-
|
|
148
|
+
// Firebird string literals have NO backslash escapes — only the
|
|
149
|
+
// quote is doubled. Doubling backslashes corrupted the data
|
|
150
|
+
// (issue #156: '\' arrived as '\\').
|
|
151
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
134
152
|
}
|
|
135
153
|
if (value instanceof Date)
|
|
136
154
|
return "'" + value.getFullYear() + '-' + (value.getMonth() + 1).toString().padStart(2, '0') + '-' + value.getDate().toString().padStart(2, '0') + ' ' + value.getHours().toString().padStart(2, '0') + ':' + value.getMinutes().toString().padStart(2, '0') + ':' + value.getSeconds().toString().padStart(2, '0') + '.' + value.getMilliseconds().toString().padStart(3, '0') + "'";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* batchStream — object-mode Writable over the Firebird 4 batch API
|
|
4
|
+
*
|
|
5
|
+
* The COPY FROM analogue: write parameter rows, they are flushed in
|
|
6
|
+
* chunks through statement.executeBatch (single prepared statement,
|
|
7
|
+
* protocol-level batching, BLOB values included). Backpressure is the
|
|
8
|
+
* Writable machinery itself: a write callback is held while a chunk
|
|
9
|
+
* is in flight.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
import { Writable } from 'stream';
|
|
13
|
+
/**
|
|
14
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
15
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
16
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
17
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
18
|
+
* commit/rollback to the caller.
|
|
19
|
+
*
|
|
20
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
21
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
22
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
23
|
+
* stream.affectedRows carry the totals.
|
|
24
|
+
*/
|
|
25
|
+
declare function makeBatchStream(target: any, query: string, options: any, ownsTransaction: boolean): Writable;
|
|
26
|
+
export = makeBatchStream;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/***************************************
|
|
3
|
+
*
|
|
4
|
+
* batchStream — object-mode Writable over the Firebird 4 batch API
|
|
5
|
+
*
|
|
6
|
+
* The COPY FROM analogue: write parameter rows, they are flushed in
|
|
7
|
+
* chunks through statement.executeBatch (single prepared statement,
|
|
8
|
+
* protocol-level batching, BLOB values included). Backpressure is the
|
|
9
|
+
* Writable machinery itself: a write callback is held while a chunk
|
|
10
|
+
* is in flight.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
const stream_1 = require("stream");
|
|
14
|
+
const callback_1 = require("../callback");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
16
|
+
/**
|
|
17
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
18
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
19
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
20
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
21
|
+
* commit/rollback to the caller.
|
|
22
|
+
*
|
|
23
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
24
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
25
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
26
|
+
* stream.affectedRows carry the totals.
|
|
27
|
+
*/
|
|
28
|
+
function makeBatchStream(target, query, options, ownsTransaction) {
|
|
29
|
+
options = options || {};
|
|
30
|
+
const flushRows = options.flushRows > 0 ? Math.floor(options.flushRows) : 1000;
|
|
31
|
+
const batchOptions = { ...options };
|
|
32
|
+
delete batchOptions.flushRows;
|
|
33
|
+
delete batchOptions.highWaterMark;
|
|
34
|
+
let transaction = null;
|
|
35
|
+
let statement = null;
|
|
36
|
+
let buffered = [];
|
|
37
|
+
const init = async () => {
|
|
38
|
+
if (statement) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
transaction = ownsTransaction ? await target.transactionAsync() : target;
|
|
42
|
+
statement = await (0, callback_1.fromCallback)((cb) => transaction.newStatement(query, cb));
|
|
43
|
+
};
|
|
44
|
+
const flush = async () => {
|
|
45
|
+
if (!buffered.length) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await init();
|
|
49
|
+
const chunk = buffered;
|
|
50
|
+
buffered = [];
|
|
51
|
+
const result = await (0, callback_1.fromCallback)((cb) => statement.executeBatch(transaction, chunk, cb, batchOptions));
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
// the same all-or-nothing error shape database.executeBatch uses
|
|
54
|
+
throw (0, utils_1.batchResultToError)(result);
|
|
55
|
+
}
|
|
56
|
+
stream.recordCount += result.recordCount;
|
|
57
|
+
for (const count of result.updateCounts) {
|
|
58
|
+
stream.affectedRows += count;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const cleanup = async (commit) => {
|
|
62
|
+
if (statement) {
|
|
63
|
+
const stmt = statement;
|
|
64
|
+
statement = null;
|
|
65
|
+
await new Promise((resolve) => stmt.release(() => resolve()));
|
|
66
|
+
}
|
|
67
|
+
if (ownsTransaction && transaction) {
|
|
68
|
+
const tx = transaction;
|
|
69
|
+
transaction = null;
|
|
70
|
+
await (commit ? tx.commitAsync() : tx.rollbackAsync());
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const stream = new stream_1.Writable({
|
|
74
|
+
objectMode: true,
|
|
75
|
+
highWaterMark: options.highWaterMark > 0 ? options.highWaterMark : flushRows,
|
|
76
|
+
write(row, _enc, cb) {
|
|
77
|
+
if (!Array.isArray(row)) {
|
|
78
|
+
cb(new Error('batchStream expects parameter-array rows'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
buffered.push(row);
|
|
82
|
+
if (buffered.length >= flushRows) {
|
|
83
|
+
flush().then(() => cb(), cb);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
cb();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
final(cb) {
|
|
90
|
+
// an empty stream finishes without touching the server at all
|
|
91
|
+
// (flush() early-returns and init never runs)
|
|
92
|
+
flush()
|
|
93
|
+
.then(() => cleanup(true))
|
|
94
|
+
.then(() => cb(), (err) => {
|
|
95
|
+
// the failed stream must not commit half a bulk load
|
|
96
|
+
cleanup(false).catch(() => { });
|
|
97
|
+
cb(err);
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
destroy(err, cb) {
|
|
101
|
+
cleanup(false)
|
|
102
|
+
.then(() => cb(err), () => cb(err));
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
stream.recordCount = 0;
|
|
106
|
+
stream.affectedRows = 0;
|
|
107
|
+
return stream;
|
|
108
|
+
}
|
|
109
|
+
module.exports = makeBatchStream;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Single-byte codepage codecs (WIN125x, ISO8859_x, KOI8, DOS866)
|
|
4
|
+
*
|
|
5
|
+
* Node's Buffer only decodes utf8/latin1/ascii natively. These
|
|
6
|
+
* codepages are decoded through the WHATWG TextDecoder (backed by
|
|
7
|
+
* ICU — present in every official Node build) and encoded through
|
|
8
|
+
* reverse tables built from the same decoder at first use, so the
|
|
9
|
+
* two directions can never disagree. Issues #319/#301/#422.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
export interface TextCodec {
|
|
13
|
+
/** Firebird charset name (upper case). */
|
|
14
|
+
name: string;
|
|
15
|
+
decode(buffer: Buffer): string;
|
|
16
|
+
encode(value: string): Buffer;
|
|
17
|
+
}
|
|
18
|
+
export declare function charsetWidthById(id: number | undefined): number;
|
|
19
|
+
/**
|
|
20
|
+
* Codec for a Firebird charset name, or null when the charset is unknown,
|
|
21
|
+
* natively handled by Buffer, or the ICU tables are unavailable. Cached.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getCodec(charsetName: string | undefined): TextCodec | null;
|