node-firebird 2.12.0 → 2.14.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 +138 -6
- package/lib/callback.d.ts +7 -0
- package/lib/callback.js +16 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +13 -0
- package/lib/pool-cluster.d.ts +89 -0
- package/lib/pool-cluster.js +266 -0
- package/lib/pool.js +2 -10
- package/lib/types.d.ts +38 -5
- 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 +38 -4
- package/lib/wire/connection.js +294 -44
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +13 -6
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +10 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +6 -0
- package/lib/wire/transaction.js +27 -2
- package/lib/wire/xsqlvar.d.ts +56 -1
- package/lib/wire/xsqlvar.js +107 -29
- package/package.json +19 -1
- package/src/callback.ts +15 -0
- package/src/index.ts +15 -0
- package/src/pool-cluster.ts +319 -0
- package/src/pool.ts +3 -10
- package/src/types.ts +40 -5
- 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 +315 -49
- package/src/wire/database.ts +15 -7
- package/src/wire/serialize.ts +11 -0
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +28 -2
- package/src/wire/xsqlvar.ts +129 -30
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* PoolCluster — multi-host pooling (primaries/replicas, failover)
|
|
4
|
+
*
|
|
5
|
+
* The mysql2 PoolCluster model on top of this driver's Pool: named
|
|
6
|
+
* nodes, each backed by a regular connection pool (health checks,
|
|
7
|
+
* recycling and metrics included), selected by glob pattern +
|
|
8
|
+
* selector. Consecutive connection failures take a node offline
|
|
9
|
+
* (with optional timed restoration), and get() fails over to the
|
|
10
|
+
* next matching online node.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
|
|
14
|
+
import Events from 'events';
|
|
15
|
+
import { fromCallback, withPooledConnection } from './callback';
|
|
16
|
+
import type { Callback } from './callback';
|
|
17
|
+
import { parseConnectionString } from './uri';
|
|
18
|
+
import Pool from './pool';
|
|
19
|
+
|
|
20
|
+
type AttachFn = (options: any, callback: Callback) => void;
|
|
21
|
+
|
|
22
|
+
export type ClusterSelector = 'rr' | 'random' | 'order';
|
|
23
|
+
|
|
24
|
+
export interface PoolClusterOptions {
|
|
25
|
+
/** Options shared by every node (user, password, database, …). */
|
|
26
|
+
defaults?: any;
|
|
27
|
+
/** name → per-node option overrides (host, port, …). */
|
|
28
|
+
nodes?: Record<string, any>;
|
|
29
|
+
/** Per-node pool size (default 4). */
|
|
30
|
+
max?: number;
|
|
31
|
+
/** Default selector for get()/of() (default 'rr'). */
|
|
32
|
+
selector?: ClusterSelector;
|
|
33
|
+
/**
|
|
34
|
+
* Consecutive connection failures after which a node goes offline
|
|
35
|
+
* (default 5; 0 disables offlining).
|
|
36
|
+
*/
|
|
37
|
+
removeNodeErrorCount?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Milliseconds after which an offline node is restored and probed
|
|
40
|
+
* again (default 30000; 0 = stay offline until restore()/remove()).
|
|
41
|
+
*/
|
|
42
|
+
restoreNodeTimeout?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ClusterNode {
|
|
46
|
+
name: string;
|
|
47
|
+
options: any;
|
|
48
|
+
pool: Pool;
|
|
49
|
+
online: boolean;
|
|
50
|
+
errorCount: number;
|
|
51
|
+
restoreTimer: NodeJS.Timeout | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function patternToRegExp(pattern: string): RegExp {
|
|
55
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
56
|
+
return new RegExp('^' + escaped + '$');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Events: 'online' (name) — node restored; 'offline' (name) — node taken
|
|
61
|
+
* out of rotation after too many connection failures; 'remove' (name) —
|
|
62
|
+
* node removed via remove().
|
|
63
|
+
*/
|
|
64
|
+
class PoolCluster extends Events.EventEmitter {
|
|
65
|
+
private attach: AttachFn;
|
|
66
|
+
private nodes = new Map<string, ClusterNode>();
|
|
67
|
+
private rrIndex = new Map<string, number>();
|
|
68
|
+
private max: number;
|
|
69
|
+
private defaults: any;
|
|
70
|
+
private selector: ClusterSelector;
|
|
71
|
+
private removeNodeErrorCount: number;
|
|
72
|
+
private restoreNodeTimeout: number;
|
|
73
|
+
private _destroyed = false;
|
|
74
|
+
|
|
75
|
+
constructor(attach: AttachFn, options?: PoolClusterOptions) {
|
|
76
|
+
super();
|
|
77
|
+
options = options || {};
|
|
78
|
+
this.attach = attach;
|
|
79
|
+
this.defaults = options.defaults || {};
|
|
80
|
+
this.max = options.max && options.max > 0 ? options.max : 4;
|
|
81
|
+
this.selector = options.selector || 'rr';
|
|
82
|
+
this.removeNodeErrorCount = options.removeNodeErrorCount !== undefined ? options.removeNodeErrorCount : 5;
|
|
83
|
+
this.restoreNodeTimeout = options.restoreNodeTimeout !== undefined ? options.restoreNodeTimeout : 30000;
|
|
84
|
+
|
|
85
|
+
for (const [name, overrides] of Object.entries(options.nodes || {})) {
|
|
86
|
+
this.add(name, overrides);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Register a node; its pool is created lazily-safe right away. */
|
|
91
|
+
add(name: string, overrides?: any): this {
|
|
92
|
+
if (this._destroyed) {
|
|
93
|
+
throw new Error('PoolCluster has been destroyed');
|
|
94
|
+
}
|
|
95
|
+
if (this.nodes.has(name)) {
|
|
96
|
+
throw new Error('PoolCluster node already exists: ' + name);
|
|
97
|
+
}
|
|
98
|
+
// a connection-string override must be parsed, not object-spread
|
|
99
|
+
// into character-indexed garbage
|
|
100
|
+
if (typeof overrides === 'string') {
|
|
101
|
+
overrides = parseConnectionString(overrides);
|
|
102
|
+
}
|
|
103
|
+
const nodeOptions = { ...this.defaults, ...(overrides || {}) };
|
|
104
|
+
this.nodes.set(name, {
|
|
105
|
+
name,
|
|
106
|
+
options: nodeOptions,
|
|
107
|
+
pool: new Pool(this.attach, nodeOptions.max || this.max, { ...nodeOptions, isPool: true }),
|
|
108
|
+
online: true,
|
|
109
|
+
errorCount: 0,
|
|
110
|
+
restoreTimer: null,
|
|
111
|
+
});
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Remove a node for good, destroying its pool. */
|
|
116
|
+
remove(name: string, callback?: (err?: any) => void): void {
|
|
117
|
+
const node = this.nodes.get(name);
|
|
118
|
+
if (!node) {
|
|
119
|
+
if (callback) callback();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.nodes.delete(name);
|
|
123
|
+
if (node.restoreTimer) {
|
|
124
|
+
clearTimeout(node.restoreTimer);
|
|
125
|
+
}
|
|
126
|
+
this.emit('remove', name);
|
|
127
|
+
node.pool.destroy(callback);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Bring an offline node back into rotation immediately. */
|
|
131
|
+
restore(name: string): void {
|
|
132
|
+
const node = this.nodes.get(name);
|
|
133
|
+
if (!node || node.online) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (node.restoreTimer) {
|
|
137
|
+
clearTimeout(node.restoreTimer);
|
|
138
|
+
node.restoreTimer = null;
|
|
139
|
+
}
|
|
140
|
+
node.online = true;
|
|
141
|
+
node.errorCount = 0;
|
|
142
|
+
this.emit('online', name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** name → { online, errorCount, pool metrics } for every node. */
|
|
146
|
+
status(): Record<string, any> {
|
|
147
|
+
const out: Record<string, any> = {};
|
|
148
|
+
for (const node of this.nodes.values()) {
|
|
149
|
+
out[node.name] = {
|
|
150
|
+
online: node.online,
|
|
151
|
+
errorCount: node.errorCount,
|
|
152
|
+
totalCount: node.pool.totalCount,
|
|
153
|
+
idleCount: node.pool.idleCount,
|
|
154
|
+
activeCount: node.pool.activeCount,
|
|
155
|
+
waitingCount: node.pool.waitingCount,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private matching(pattern: string): ClusterNode[] {
|
|
162
|
+
const re = patternToRegExp(pattern);
|
|
163
|
+
const out: ClusterNode[] = [];
|
|
164
|
+
for (const node of this.nodes.values()) {
|
|
165
|
+
if (re.test(node.name)) {
|
|
166
|
+
out.push(node);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private pick(pattern: string, selector: ClusterSelector, exclude: Set<string>): ClusterNode | null {
|
|
173
|
+
const candidates = this.matching(pattern).filter((n) => n.online && !exclude.has(n.name));
|
|
174
|
+
if (!candidates.length) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (selector === 'random') {
|
|
178
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
179
|
+
}
|
|
180
|
+
if (selector === 'order') {
|
|
181
|
+
return candidates[0];
|
|
182
|
+
}
|
|
183
|
+
// round-robin per pattern; only the FIRST pick of a get() advances
|
|
184
|
+
// the counter — failover re-picks reuse it, or a run of failovers
|
|
185
|
+
// would skew the distribution toward nodes after the failing ones
|
|
186
|
+
const index = this.rrIndex.get(pattern) || 0;
|
|
187
|
+
if (exclude.size === 0) {
|
|
188
|
+
this.rrIndex.set(pattern, index + 1);
|
|
189
|
+
}
|
|
190
|
+
return candidates[index % candidates.length];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private noteFailure(node: ClusterNode): void {
|
|
194
|
+
// a node removed while a get was in flight must not accumulate
|
|
195
|
+
// counters, emit 'offline', or arm a restore timer nobody clears
|
|
196
|
+
if (!this.nodes.has(node.name)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
node.errorCount++;
|
|
200
|
+
if (!this.removeNodeErrorCount || node.errorCount < this.removeNodeErrorCount || !node.online) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
node.online = false;
|
|
204
|
+
this.emit('offline', node.name);
|
|
205
|
+
if (this.restoreNodeTimeout > 0) {
|
|
206
|
+
node.restoreTimer = setTimeout(() => {
|
|
207
|
+
node.restoreTimer = null;
|
|
208
|
+
this.restore(node.name);
|
|
209
|
+
}, this.restoreNodeTimeout);
|
|
210
|
+
if (node.restoreTimer.unref) {
|
|
211
|
+
node.restoreTimer.unref();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Acquire a connection from a node matching `pattern` (default '*').
|
|
218
|
+
* Connection failures mark the node and FAIL OVER to the next
|
|
219
|
+
* matching online node; only when every candidate has failed does the
|
|
220
|
+
* callback receive the last error. Release connections with
|
|
221
|
+
* db.detach(), exactly like a plain pool.
|
|
222
|
+
*/
|
|
223
|
+
get(pattern: string | Callback, selector?: ClusterSelector | Callback, callback?: Callback): void {
|
|
224
|
+
if (typeof pattern === 'function') {
|
|
225
|
+
callback = pattern;
|
|
226
|
+
pattern = '*';
|
|
227
|
+
}
|
|
228
|
+
if (typeof selector === 'function') {
|
|
229
|
+
callback = selector;
|
|
230
|
+
selector = undefined;
|
|
231
|
+
}
|
|
232
|
+
if (this._destroyed) {
|
|
233
|
+
callback!(new Error('PoolCluster has been destroyed'), null);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sel = (selector as ClusterSelector) || this.selector;
|
|
238
|
+
const tried = new Set<string>();
|
|
239
|
+
const self = this;
|
|
240
|
+
|
|
241
|
+
const attempt = (lastError?: any) => {
|
|
242
|
+
const node = self.pick(pattern as string, sel, tried);
|
|
243
|
+
if (!node) {
|
|
244
|
+
callback!(lastError || new Error('PoolCluster: no online node matches pattern "' + pattern + '"'), null);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
tried.add(node.name);
|
|
248
|
+
node.pool.get((err: any, db: any) => {
|
|
249
|
+
if (err) {
|
|
250
|
+
self.noteFailure(node);
|
|
251
|
+
attempt(err);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
node.errorCount = 0;
|
|
255
|
+
callback!(null, db);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
attempt();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getAsync(pattern?: string, selector?: ClusterSelector): Promise<any> {
|
|
262
|
+
const self = this;
|
|
263
|
+
return fromCallback((cb) => self.get(pattern || '*', selector, cb));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* A pool-like facade bound to a pattern (mysql2's cluster.of):
|
|
268
|
+
* { get, getAsync, withConnection } routed through the cluster's
|
|
269
|
+
* selection and failover.
|
|
270
|
+
*/
|
|
271
|
+
of(pattern: string, selector?: ClusterSelector) {
|
|
272
|
+
const self = this;
|
|
273
|
+
return {
|
|
274
|
+
get(callback: Callback) {
|
|
275
|
+
self.get(pattern, selector, callback);
|
|
276
|
+
},
|
|
277
|
+
getAsync() {
|
|
278
|
+
return self.getAsync(pattern, selector);
|
|
279
|
+
},
|
|
280
|
+
withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T> {
|
|
281
|
+
return self.withConnection(pattern, work, selector);
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Run `work` with a connection from a matching node, always released. */
|
|
287
|
+
withConnection<T>(pattern: string, work: (db: any) => Promise<T> | T, selector?: ClusterSelector): Promise<T> {
|
|
288
|
+
return withPooledConnection(() => this.getAsync(pattern, selector), work);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Destroy every node's pool. */
|
|
292
|
+
destroy(callback?: (err?: any) => void): void {
|
|
293
|
+
this._destroyed = true;
|
|
294
|
+
const nodes = [...this.nodes.values()];
|
|
295
|
+
this.nodes.clear();
|
|
296
|
+
let remaining = nodes.length;
|
|
297
|
+
if (!remaining) {
|
|
298
|
+
if (callback) callback();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let firstError: any = null;
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
if (node.restoreTimer) {
|
|
304
|
+
clearTimeout(node.restoreTimer);
|
|
305
|
+
}
|
|
306
|
+
node.pool.destroy((err?: any) => {
|
|
307
|
+
if (err && !firstError) firstError = err;
|
|
308
|
+
if (--remaining === 0 && callback) callback(firstError);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
destroyAsync(): Promise<void> {
|
|
314
|
+
const self = this;
|
|
315
|
+
return fromCallback((cb) => self.destroy(cb));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default PoolCluster;
|
package/src/pool.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
***************************************/
|
|
6
6
|
|
|
7
7
|
import Events from 'events';
|
|
8
|
-
import { fromCallback } from './callback';
|
|
8
|
+
import { fromCallback, withPooledConnection } from './callback';
|
|
9
9
|
import type { Callback } from './callback';
|
|
10
10
|
|
|
11
11
|
type AttachFn = (options: any, callback: Callback) => void;
|
|
@@ -385,15 +385,8 @@ class Pool extends Events.EventEmitter {
|
|
|
385
385
|
* Run `work` with a connection from the pool, returning it to the pool
|
|
386
386
|
* (detach) when the returned promise settles — success or failure.
|
|
387
387
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
return await work(db);
|
|
392
|
-
} finally {
|
|
393
|
-
// A pooled detach only returns the connection to the pool; do not
|
|
394
|
-
// let a detach hiccup mask the outcome of `work`.
|
|
395
|
-
await new Promise<void>(function(resolve) { db.detach(function() { resolve(); }); });
|
|
396
|
-
}
|
|
388
|
+
withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T> {
|
|
389
|
+
return withPooledConnection(() => this.getAsync(), work);
|
|
397
390
|
}
|
|
398
391
|
}
|
|
399
392
|
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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
9
|
import type { SqlTag } from './sql-template';
|
|
10
10
|
|
|
11
11
|
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
@@ -211,6 +211,22 @@ export interface QueryResult<T = any> {
|
|
|
211
211
|
warnings: ServerWarning[];
|
|
212
212
|
}
|
|
213
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;
|
|
228
|
+
}
|
|
229
|
+
|
|
214
230
|
export type QueryStreamOptions = QueryOptions & {
|
|
215
231
|
/**
|
|
216
232
|
* Rows buffered internally before fetching pauses (object-mode
|
|
@@ -232,8 +248,8 @@ export interface Database {
|
|
|
232
248
|
detach(callback?: SimpleCallback): Database;
|
|
233
249
|
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
|
|
234
250
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
235
|
-
query(query: string, params: QueryParams, callback:
|
|
236
|
-
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;
|
|
237
253
|
/** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
|
|
238
254
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
|
|
239
255
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -244,6 +260,13 @@ export interface Database {
|
|
|
244
260
|
* fetch and releases the statement.
|
|
245
261
|
*/
|
|
246
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;
|
|
247
270
|
drop(callback: SimpleCallback): void;
|
|
248
271
|
escape(value: any): string;
|
|
249
272
|
attachEvent(callback: any): this;
|
|
@@ -290,8 +313,8 @@ export interface Transaction {
|
|
|
290
313
|
*/
|
|
291
314
|
savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
|
|
292
315
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
293
|
-
query(query: string, params: QueryParams, callback:
|
|
294
|
-
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;
|
|
295
318
|
/** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
|
|
296
319
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
297
320
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -301,6 +324,11 @@ export interface Transaction {
|
|
|
301
324
|
* transaction is NOT committed when the stream ends.
|
|
302
325
|
*/
|
|
303
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;
|
|
304
332
|
commit(callback?: SimpleCallback): void;
|
|
305
333
|
commitRetaining(callback?: SimpleCallback): void;
|
|
306
334
|
rollback(callback?: SimpleCallback): void;
|
|
@@ -416,6 +444,13 @@ export interface Options {
|
|
|
416
444
|
* per-query `namedPlaceholders: false` override.
|
|
417
445
|
*/
|
|
418
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;
|
|
419
454
|
/**
|
|
420
455
|
* Qualify object-row keys by source table (same option as mysql2), so
|
|
421
456
|
* JOINed columns with the same name stop overwriting each other:
|
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)
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
|
|
13
|
+
import { Writable } from 'stream';
|
|
14
|
+
import { fromCallback } from '../callback';
|
|
15
|
+
import { batchResultToError } from '../utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
19
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
20
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
21
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
22
|
+
* commit/rollback to the caller.
|
|
23
|
+
*
|
|
24
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
25
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
26
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
27
|
+
* stream.affectedRows carry the totals.
|
|
28
|
+
*/
|
|
29
|
+
function makeBatchStream(target: any, query: string, options: any, ownsTransaction: boolean): Writable {
|
|
30
|
+
options = options || {};
|
|
31
|
+
const flushRows = options.flushRows > 0 ? Math.floor(options.flushRows) : 1000;
|
|
32
|
+
|
|
33
|
+
const batchOptions = { ...options };
|
|
34
|
+
delete batchOptions.flushRows;
|
|
35
|
+
delete batchOptions.highWaterMark;
|
|
36
|
+
|
|
37
|
+
let transaction: any = null;
|
|
38
|
+
let statement: any = null;
|
|
39
|
+
let buffered: any[][] = [];
|
|
40
|
+
|
|
41
|
+
const init = async () => {
|
|
42
|
+
if (statement) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
transaction = ownsTransaction ? await target.transactionAsync() : target;
|
|
46
|
+
statement = await fromCallback((cb) => transaction.newStatement(query, cb));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const flush = async () => {
|
|
50
|
+
if (!buffered.length) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await init();
|
|
54
|
+
const chunk = buffered;
|
|
55
|
+
buffered = [];
|
|
56
|
+
const result: any = await fromCallback((cb) =>
|
|
57
|
+
statement.executeBatch(transaction, chunk, cb, batchOptions));
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
// the same all-or-nothing error shape database.executeBatch uses
|
|
60
|
+
throw batchResultToError(result);
|
|
61
|
+
}
|
|
62
|
+
(stream as any).recordCount += result.recordCount;
|
|
63
|
+
for (const count of result.updateCounts) {
|
|
64
|
+
(stream as any).affectedRows += count;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const cleanup = async (commit: boolean) => {
|
|
69
|
+
if (statement) {
|
|
70
|
+
const stmt = statement;
|
|
71
|
+
statement = null;
|
|
72
|
+
await new Promise<void>((resolve) => stmt.release(() => resolve()));
|
|
73
|
+
}
|
|
74
|
+
if (ownsTransaction && transaction) {
|
|
75
|
+
const tx = transaction;
|
|
76
|
+
transaction = null;
|
|
77
|
+
await (commit ? tx.commitAsync() : tx.rollbackAsync());
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const stream = new Writable({
|
|
82
|
+
objectMode: true,
|
|
83
|
+
highWaterMark: options.highWaterMark > 0 ? options.highWaterMark : flushRows,
|
|
84
|
+
|
|
85
|
+
write(row: any, _enc: any, cb: (err?: any) => void) {
|
|
86
|
+
if (!Array.isArray(row)) {
|
|
87
|
+
cb(new Error('batchStream expects parameter-array rows'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
buffered.push(row);
|
|
91
|
+
if (buffered.length >= flushRows) {
|
|
92
|
+
flush().then(() => cb(), cb);
|
|
93
|
+
} else {
|
|
94
|
+
cb();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
final(cb: (err?: any) => void) {
|
|
99
|
+
// an empty stream finishes without touching the server at all
|
|
100
|
+
// (flush() early-returns and init never runs)
|
|
101
|
+
flush()
|
|
102
|
+
.then(() => cleanup(true))
|
|
103
|
+
.then(() => cb(), (err) => {
|
|
104
|
+
// the failed stream must not commit half a bulk load
|
|
105
|
+
cleanup(false).catch(() => { /* rollback best-effort */ });
|
|
106
|
+
cb(err);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
destroy(err: any, cb: (err?: any) => void) {
|
|
111
|
+
cleanup(false)
|
|
112
|
+
.then(() => cb(err), () => cb(err));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
(stream as any).recordCount = 0;
|
|
117
|
+
(stream as any).affectedRows = 0;
|
|
118
|
+
return stream;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export = makeBatchStream;
|