node-firebird 2.5.0 → 2.7.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 CHANGED
@@ -14,7 +14,7 @@
14
14
  - [Connection types](#connection-types) — connection options, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
16
  - [Examples](#examples) — parametrized queries, BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
- - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
17
+ - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
20
20
  - [Contributing](#contributing) · [Contributors](#contributors)
@@ -133,9 +133,9 @@ Available wrappers:
133
133
 
134
134
  - **module** — `Firebird.attachAsync(options)`, `createAsync`, `attachOrCreateAsync`, `dropAsync`; resolve with a `Database` (or a `ServiceManager` when `options.manager` is `true`)
135
135
  - **pool** — `pool.getAsync()`, `pool.destroyAsync()`, `pool.withConnection(work)`
136
- - **database** — `db.queryAsync(sql, params?, options?)`, `executeAsync`, `sequentiallyAsync(sql, params?, onRow, options?)`, `transactionAsync(options?)`, `newStatementAsync(sql)`, `attachEventAsync()`, `detachAsync()`, `dropAsync()`, `db.withTransaction(work, options?)`
137
- - **transaction** — `queryAsync`, `executeAsync`, `sequentiallyAsync`, `newStatementAsync`, `commitAsync`, `rollbackAsync`, `commitRetainingAsync`, `rollbackRetainingAsync`
138
- - **statement** — `executeAsync(transaction, params?, options?)`, `fetchAsync`, `fetchScrollAsync`, `fetchAllAsync`, `closeAsync`, `dropAsync`, `releaseAsync`
136
+ - **database** — `db.queryAsync(sql, params?, options?)`, `executeAsync`, `executeBatchAsync(sql, rows, options?)`, `sequentiallyAsync(sql, params?, onRow, options?)`, `transactionAsync(options?)`, `newStatementAsync(sql)`, `attachEventAsync()`, `detachAsync()`, `dropAsync()`, `db.withTransaction(work, options?)`
137
+ - **transaction** — `queryAsync`, `executeAsync`, `executeBatchAsync`, `sequentiallyAsync`, `newStatementAsync`, `commitAsync`, `rollbackAsync`, `commitRetainingAsync`, `rollbackRetainingAsync`
138
+ - **statement** — `executeAsync(transaction, params?, options?)`, `executeBatchAsync(transaction, rows, options?)`, `fetchAsync`, `fetchScrollAsync`, `fetchAllAsync`, `closeAsync`, `dropAsync`, `releaseAsync`
139
139
 
140
140
  Notes:
141
141
 
@@ -277,6 +277,7 @@ sequenceDiagram
277
277
 
278
278
  - `db.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
279
279
  - `db.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
280
+ - `db.executeBatch(query, rows, function(err, result), options)` - bulk execution in one round-trip, all-or-nothing (FB >= 4.0, see [Batch Execution](#batch-execution-firebird-40))
280
281
  - `db.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
281
282
  - `db.detach(function(err))` detach a database
282
283
  - `db.transaction(options, function(err, transaction))` create transaction
@@ -303,6 +304,7 @@ const options = {
303
304
 
304
305
  - `transaction.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
305
306
  - `transaction.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
307
+ - `transaction.executeBatch(query, rows, function(err, result), options)` - bulk execution with per-record errors (FB >= 4.0, see [Batch Execution](#batch-execution-firebird-40))
306
308
  - `transaction.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
307
309
  - `transaction.commit(function(err))` commit current transaction
308
310
  - `transaction.rollback(function(err))` rollback current transaction
@@ -1151,6 +1153,101 @@ Firebird.attach({
1151
1153
  });
1152
1154
  ```
1153
1155
 
1156
+ ### Query Cancellation with AbortSignal (Firebird 2.5+)
1157
+
1158
+ Any query can be cancelled while it is executing on the server. Pass an
1159
+ `AbortSignal` in the query options — when it fires, the driver sends an
1160
+ out-of-band `op_cancel` packet and the running statement fails with
1161
+ `err.gdscode === GDSCode.CANCELLED`. The connection itself stays healthy and
1162
+ can run further queries immediately.
1163
+
1164
+ ```js
1165
+ const { GDSCode } = require('node-firebird/lib/gdscodes');
1166
+
1167
+ const controller = new AbortController();
1168
+ setTimeout(() => controller.abort(), 5000); // or req.on('close', ...) in Express
1169
+
1170
+ try {
1171
+ const rows = await db.queryAsync('SELECT /* expensive */ ...', [], { signal: controller.signal });
1172
+ } catch (err) {
1173
+ if (err.gdscode === GDSCode.CANCELLED) {
1174
+ // cancelled server-side; the connection remains usable
1175
+ } else if (err.name === 'AbortError') {
1176
+ // the signal was already aborted — the query was never sent
1177
+ } else {
1178
+ throw err;
1179
+ }
1180
+ }
1181
+ ```
1182
+
1183
+ The option works with the callback API as well (`db.query(sql, params, cb,
1184
+ { signal })`) and on transaction-level queries. A running operation can also
1185
+ be cancelled manually from anywhere with `db.cancel()` / `await
1186
+ db.cancelAsync()`.
1187
+
1188
+ Notes:
1189
+
1190
+ - Cancellation is **per attachment** (that is how the Firebird protocol
1191
+ defines it): it cancels whatever is currently executing on that
1192
+ connection. With a pool this is naturally scoped to the request holding
1193
+ the connection.
1194
+ - A signal that is already aborted rejects immediately with an `AbortError`
1195
+ without contacting the server; cancelling an idle connection is harmless.
1196
+
1197
+ ### Batch Execution (Firebird 4.0+)
1198
+
1199
+ `executeBatch` sends many parameter rows for one statement in a single
1200
+ round-trip using the Firebird 4 wire batch API (`op_batch_create` /
1201
+ `op_batch_msg` / `op_batch_exec`) — typically 5–10× faster than executing
1202
+ row by row. Available on the database, transaction and statement objects, in
1203
+ callback and promise flavours.
1204
+
1205
+ ```js
1206
+ const rows = [
1207
+ [1, 'Alice', 10.50, new Date(), true, 100n],
1208
+ [2, 'Bob', null, new Date(), false, null], // NULLs per column
1209
+ [3, 'Carol', 7.25, new Date(), true, 300n],
1210
+ ];
1211
+
1212
+ // database level: own transaction, all-or-nothing (rollback if any record fails)
1213
+ const res = await db.executeBatchAsync(
1214
+ 'INSERT INTO t (id, name, amount, created, active, big) VALUES (?, ?, ?, ?, ?, ?)',
1215
+ rows);
1216
+ console.log(res.recordCount, res.updateCounts, res.success);
1217
+
1218
+ // transaction level: partial success — failed records are reported,
1219
+ // the rest can still be committed
1220
+ const tr = await db.transactionAsync();
1221
+ const r = await tr.executeBatchAsync('INSERT INTO t (...) VALUES (?, ?, ?, ?, ?, ?)', rows);
1222
+ if (!r.success) {
1223
+ console.log('failed record indexes:', r.errorRecordNumbers); // 0-based
1224
+ console.log('first error:', r.errors[0].error.message);
1225
+ }
1226
+ await tr.commitAsync();
1227
+ ```
1228
+
1229
+ The result object contains `recordCount`, `updateCounts` (one entry per
1230
+ record), `errors` (`{ recordNumber, error }` with full gdscode details),
1231
+ `errorRecordNumbers` and `success`. At the database level a failed batch
1232
+ rejects with the first record error, carrying the full completion state as
1233
+ `err.batchCompletion`.
1234
+
1235
+ Options (last argument): `chunkSize` (rows per `op_batch_msg` packet,
1236
+ default 500), `multiError` (default `true` — collect all record errors
1237
+ instead of stopping at the first), `bufferSize` (server-side batch buffer
1238
+ in bytes).
1239
+
1240
+ Notes:
1241
+
1242
+ - Requires wire protocol 16+ (Firebird 4.0 or newer server).
1243
+ - Values are encoded from the statement's own parameter metadata, so
1244
+ NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
1245
+ `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
1246
+ exactly. BLOB and ARRAY parameters are not supported in batches yet.
1247
+ - Oversized `CHAR`/`VARCHAR` values fail the batch client-side before
1248
+ anything is sent; server-side record errors (constraint violations,
1249
+ truncation…) are reported per record.
1250
+
1154
1251
  ### Statement Timeouts (Firebird 4.0+)
1155
1252
  Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
1156
1253
  ```js
package/lib/types.d.ts CHANGED
@@ -64,9 +64,44 @@ export type TransactionOptions = {
64
64
  wait?: boolean;
65
65
  waitTimeout?: number;
66
66
  };
67
+ /** Result of an executeBatch call (Firebird 4 batch API, protocol 16+). */
68
+ export interface BatchResult {
69
+ /** Total number of records processed by the server. */
70
+ recordCount: number;
71
+ /** Per-record update counts (in record order). */
72
+ updateCounts: number[];
73
+ /** Per-record failures with full status vectors (capped by `detailedErrors`). */
74
+ errors: Array<{
75
+ recordNumber: number;
76
+ error: Error;
77
+ }>;
78
+ /** Record numbers of ALL failed records (detailed + status-less). */
79
+ errorRecordNumbers: number[];
80
+ /** True when every record executed without error. */
81
+ success: boolean;
82
+ }
83
+ export type BatchOptions = {
84
+ /** Continue past per-record errors and report them all (default true). */
85
+ multiError?: boolean;
86
+ /** Server-side batch buffer limit in bytes (BATCH_TAG_BUFFER_BYTES_SIZE). */
87
+ bufferSize?: number;
88
+ /** Max number of detailed per-record status vectors returned (server default 64). */
89
+ detailedErrors?: number;
90
+ /** Rows per op_batch_msg packet (default 500). */
91
+ chunkSize?: number;
92
+ };
67
93
  export type QueryOptions = {
68
94
  timeout?: number;
69
95
  scrollable?: boolean;
96
+ /**
97
+ * Abort the query when the signal fires (Firebird 2.5+ / protocol 12+).
98
+ * If the signal is already aborted the query is not sent at all and the
99
+ * callback/promise fails with an `AbortError`. If it fires mid-flight an
100
+ * out-of-band op_cancel is sent and the query fails with
101
+ * `err.gdscode === GDSCode.CANCELLED`. Cancellation is per-attachment:
102
+ * it cancels whatever is currently executing on the connection.
103
+ */
104
+ signal?: AbortSignal;
70
105
  };
71
106
  export interface Database {
72
107
  detach(callback?: SimpleCallback): Database;
@@ -74,6 +109,8 @@ export interface Database {
74
109
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
75
110
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
76
111
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
112
+ /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
113
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
77
114
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
78
115
  drop(callback: SimpleCallback): void;
79
116
  escape(value: any): string;
@@ -84,6 +121,7 @@ export interface Database {
84
121
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
85
122
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
86
123
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
124
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
87
125
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
88
126
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
89
127
  transactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
@@ -94,11 +132,21 @@ export interface Database {
94
132
  attachEventAsync(): Promise<any>;
95
133
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
96
134
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
135
+ /**
136
+ * Cancel the operation currently executing on this connection
137
+ * (Firebird 2.5+ / protocol 12+). The cancelled operation fails through
138
+ * its own callback/promise with `err.gdscode === GDSCode.CANCELLED`.
139
+ */
140
+ cancel(callback?: SimpleCallback): Database;
141
+ cancel(kind: number, callback?: SimpleCallback): Database;
142
+ cancelAsync(kind?: number): Promise<void>;
97
143
  }
98
144
  export interface Transaction {
99
145
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
100
146
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
101
147
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
148
+ /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
149
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
102
150
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
103
151
  commit(callback?: SimpleCallback): void;
104
152
  commitRetaining(callback?: SimpleCallback): void;
@@ -106,6 +154,7 @@ export interface Transaction {
106
154
  rollbackRetaining(callback?: SimpleCallback): void;
107
155
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
108
156
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
157
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
109
158
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
110
159
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
111
160
  newStatementAsync(query: string): Promise<Statement>;
@@ -122,7 +171,10 @@ export interface Statement {
122
171
  fetch(transaction: Transaction, count: number, callback: QueryCallback): void;
123
172
  fetchScroll(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset: number, count: number, callback: QueryCallback): void;
124
173
  fetchAll(transaction: Transaction, callback: QueryCallback): void;
174
+ /** Execute this prepared statement once per row (Firebird 4.0+ batch API). */
175
+ executeBatch(transaction: Transaction, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
125
176
  executeAsync(transaction: Transaction, params?: any[], options?: QueryOptions): Promise<any>;
177
+ executeBatchAsync(transaction: Transaction, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
126
178
  fetchAsync(transaction: Transaction, count: number | 'all'): Promise<any>;
127
179
  fetchScrollAsync(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset?: number, count?: number): Promise<any>;
128
180
  fetchAllAsync(transaction: Transaction): Promise<any>;
@@ -57,6 +57,15 @@ declare class Connection {
57
57
  sendOpContAuth(authData: any, authDataEnc: any, pluginName: any): void;
58
58
  sendOpCrypt(encryptPlugin: any): void;
59
59
  sendOpCryptKeyCallback(pluginData: any): void;
60
+ /**
61
+ * Send an out-of-band op_cancel packet (protocol 12+ / Firebird 2.5+).
62
+ * The server reads it asynchronously while an operation is executing and
63
+ * makes that operation fail with isc_cancelled (GDSCode.CANCELLED); the
64
+ * op_cancel packet itself has no response, so nothing is queued here.
65
+ */
66
+ cancelOperation(kind: any, callback: any): this;
67
+ /** Write a prebuilt packet and queue its response callback. */
68
+ _queueEventBuffer(buffer: any, callback: any): void;
60
69
  _queueEvent(callback: any, defer?: boolean): void;
61
70
  connect(options: any, callback: any): void;
62
71
  attach(options: any, callback?: any, db?: any): void;
@@ -75,6 +84,19 @@ declare class Connection {
75
84
  allocateAndPrepareStatement(transaction: any, query: any, plan: any, callback: any): void;
76
85
  prepare(transaction: any, query: any, plan: any, callback: any): void;
77
86
  prepareStatement(transaction: any, statement: any, query: any, plan: any, callback: any): this;
87
+ /**
88
+ * Execute a statement once per row using the Firebird 4 batch API
89
+ * (protocol 16+): op_batch_create + op_batch_msg(s) + op_batch_exec +
90
+ * op_batch_rls, all pipelined in a single network flush. Every packet
91
+ * gets an in-order response (op_batch_cs for exec), so the regular
92
+ * response queue keeps everything in sync.
93
+ *
94
+ * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
95
+ * are not supported yet. The callback receives a completion object:
96
+ * { recordCount, updateCounts, errors: [{recordNumber, error}],
97
+ * errorRecordNumbers, success }.
98
+ */
99
+ executeBatch(transaction: any, statement: any, rows: any, callback: any, options: any): this;
78
100
  executeStatement(transaction: any, statement: any, params: any, callback: any, custom: any): this;
79
101
  sendExecute(op: number, statement: any, transaction: any, callback: any, parameters?: any[]): void;
80
102
  fetch(statement: any, transaction: any, count: any, callback: any): void;