node-firebird 2.6.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.
@@ -401,6 +401,19 @@ class Connection {
401
401
  }
402
402
 
403
403
 
404
+ /** Write a prebuilt packet and queue its response callback. */
405
+ _queueEventBuffer(buffer, callback) {
406
+ if (this._isClosed) {
407
+ if (callback)
408
+ callback(new Error('Connection is closed.'));
409
+ return;
410
+ }
411
+
412
+ this._socket.write(buffer);
413
+ this._queue.push(callback);
414
+ }
415
+
416
+
404
417
  _queueEvent(callback, defer = false) {
405
418
  var self = this;
406
419
 
@@ -1158,6 +1171,208 @@ class Connection {
1158
1171
 
1159
1172
 
1160
1173
 
1174
+ /**
1175
+ * Execute a statement once per row using the Firebird 4 batch API
1176
+ * (protocol 16+): op_batch_create + op_batch_msg(s) + op_batch_exec +
1177
+ * op_batch_rls, all pipelined in a single network flush. Every packet
1178
+ * gets an in-order response (op_batch_cs for exec), so the regular
1179
+ * response queue keeps everything in sync.
1180
+ *
1181
+ * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
1182
+ * are not supported yet. The callback receives a completion object:
1183
+ * { recordCount, updateCounts, errors: [{recordNumber, error}],
1184
+ * errorRecordNumbers, success }.
1185
+ */
1186
+ executeBatch(transaction, statement, rows, callback, options) {
1187
+ options = options || {};
1188
+
1189
+ if (this._isClosed)
1190
+ return this.throwClosed(callback);
1191
+
1192
+ if (!this.accept || this.accept.protocolVersion < Const.PROTOCOL_VERSION16) {
1193
+ doError(new Error('executeBatch requires protocol 16+ (Firebird 4.0 or newer)'), callback);
1194
+ return;
1195
+ }
1196
+
1197
+ var input = statement.input;
1198
+ if (!input || !input.length) {
1199
+ doError(new Error('executeBatch requires a statement with input parameters'), callback);
1200
+ return;
1201
+ }
1202
+
1203
+ if (!Array.isArray(rows)) {
1204
+ doError(new Error('executeBatch expects an array of parameter rows'), callback);
1205
+ return;
1206
+ }
1207
+
1208
+ if (rows.length === 0) {
1209
+ if (callback) callback(undefined, {
1210
+ recordCount: 0, updateCounts: [], errors: [], errorRecordNumbers: [], success: true,
1211
+ });
1212
+ return;
1213
+ }
1214
+
1215
+ for (var i = 0; i < rows.length; i++) {
1216
+ if (!Array.isArray(rows[i]) || rows[i].length !== input.length) {
1217
+ doError(new Error('executeBatch row ' + i + ' must be an array of ' + input.length + ' values'), callback);
1218
+ return;
1219
+ }
1220
+ }
1221
+
1222
+ var built;
1223
+ try {
1224
+ built = buildBatchEncoders(input, Object.assign({}, this.options, options));
1225
+ } catch (err) {
1226
+ doError(err, callback);
1227
+ return;
1228
+ }
1229
+
1230
+ var self = this;
1231
+ var encoders = built.encoders;
1232
+ var chunkSize = options.chunkSize > 0 ? options.chunkSize : 500;
1233
+ var chunkCount = Math.ceil(rows.length / chunkSize);
1234
+
1235
+ var failure: any = null;
1236
+ var completion: any = null;
1237
+ var remaining = 2 + chunkCount; // create + msg chunks + exec
1238
+
1239
+ function settle(err?: any) {
1240
+ if (err && !failure)
1241
+ failure = err;
1242
+ remaining--;
1243
+ if (remaining > 0)
1244
+ return;
1245
+
1246
+ if (failure) {
1247
+ doError(failure, callback);
1248
+ return;
1249
+ }
1250
+
1251
+ var detailed = completion ? completion.detailedErrors : [];
1252
+ var errorRecordNumbers = detailed.map(function(e) { return e.recordNumber; })
1253
+ .concat(completion ? completion.errorRecordNumbers : []);
1254
+
1255
+ if (callback) callback(undefined, {
1256
+ recordCount: completion ? completion.recordCount : 0,
1257
+ updateCounts: completion ? completion.updateCounts : [],
1258
+ errors: detailed,
1259
+ errorRecordNumbers: errorRecordNumbers,
1260
+ success: errorRecordNumbers.length === 0,
1261
+ });
1262
+ }
1263
+
1264
+ var msg = this._msg;
1265
+ var blr = this._blr;
1266
+
1267
+ // Build every packet before writing anything: an encoding failure
1268
+ // (e.g. an oversized CHAR/VARCHAR value) must abort the batch before
1269
+ // a single byte hits the wire, or the response queue desyncs.
1270
+ var packets: Buffer[] = [];
1271
+ try {
1272
+ // 1. op_batch_create — the BLR is the statement's own described
1273
+ // input format: the engine requires batch messages to match it
1274
+ // exactly (a value-derived format is rejected with SQLDA errors).
1275
+ msg.pos = 0;
1276
+ blr.pos = 0;
1277
+ CalcBlr(blr, input);
1278
+
1279
+ msg.addInt(Const.op_batch_create);
1280
+ msg.addInt(statement.handle);
1281
+ msg.addBlr(blr);
1282
+ msg.addInt(built.msglen);
1283
+ var pb = buildBatchPb(options);
1284
+ msg.addInt(pb.length);
1285
+ msg.addParamBuffer(pb);
1286
+ packets.push(Buffer.from(msg.getData()));
1287
+
1288
+ // 2. op_batch_msg — packed messages (null bitmap + non-null
1289
+ // values), exactly the op_execute protocol-13 message encoding.
1290
+ for (var c = 0; c < chunkCount; c++) {
1291
+ var start = c * chunkSize;
1292
+ var end = Math.min(start + chunkSize, rows.length);
1293
+
1294
+ msg.pos = 0;
1295
+ msg.addInt(Const.op_batch_msg);
1296
+ msg.addInt(statement.handle);
1297
+ msg.addInt(end - start);
1298
+
1299
+ for (var i = start; i < end; i++) {
1300
+ var row = rows[i];
1301
+
1302
+ var nullBits = new BitSet();
1303
+ for (var j = 0; j < input.length; j++) {
1304
+ nullBits.set(j, row[j] === null || row[j] === undefined ? 1 : 0);
1305
+ }
1306
+ var nullBuffer = nullBits.toBuffer();
1307
+ var requireBytes = Math.floor((input.length + 7) / 8);
1308
+ var remainingBytes = requireBytes - nullBuffer.length;
1309
+
1310
+ if (nullBuffer.length) {
1311
+ msg.addBuffer(nullBuffer);
1312
+ }
1313
+ if (remainingBytes > 0) {
1314
+ msg.addBuffer(Buffer.alloc(remainingBytes));
1315
+ }
1316
+ msg.addAlignment(requireBytes);
1317
+
1318
+ for (var j = 0; j < input.length; j++) {
1319
+ if (row[j] !== null && row[j] !== undefined) {
1320
+ encoders[j](msg, row[j]);
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ packets.push(Buffer.from(msg.getData()));
1326
+ }
1327
+
1328
+ // 3. op_batch_exec — answered with op_batch_cs.
1329
+ msg.pos = 0;
1330
+ msg.addInt(Const.op_batch_exec);
1331
+ msg.addInt(statement.handle);
1332
+ msg.addInt(transaction.handle);
1333
+ packets.push(Buffer.from(msg.getData()));
1334
+
1335
+ // 4. op_batch_rls — free the server-side batch.
1336
+ msg.pos = 0;
1337
+ msg.addInt(Const.op_batch_rls);
1338
+ msg.addInt(statement.handle);
1339
+ packets.push(Buffer.from(msg.getData()));
1340
+ } catch (err) {
1341
+ doError(err, callback);
1342
+ return;
1343
+ }
1344
+
1345
+ // Pipeline everything: the server answers each packet in order
1346
+ // (op_response for create/msg, op_batch_cs for exec), so one queued
1347
+ // callback per packet keeps the response stream in sync. The server
1348
+ // DEFERS the responses to create/msg/rls (PORT_lazy send_partial) —
1349
+ // they only hit the wire when a flushing send occurs, which the
1350
+ // op_batch_cs answer to exec provides. The rls response however is
1351
+ // deferred past our last packet and arrives with the NEXT flushed
1352
+ // response (commit, query, detach…), so completion must not wait
1353
+ // for it: its queued callback is a no-op that just keeps the
1354
+ // response stream aligned.
1355
+ var execIndex = packets.length - 2;
1356
+ for (var p = 0; p < packets.length; p++) {
1357
+ this._pending.push('executeBatch');
1358
+ if (p === execIndex) {
1359
+ this._queueEventBuffer(packets[p], function(err, ret) {
1360
+ if (!err && ret && ret.batchCompletion) {
1361
+ completion = ret.batchCompletion;
1362
+ settle();
1363
+ } else {
1364
+ settle(err || new Error('op_batch_exec did not return a completion state'));
1365
+ }
1366
+ });
1367
+ } else if (p === packets.length - 1) {
1368
+ this._queueEventBuffer(packets[p], function() {});
1369
+ } else {
1370
+ this._queueEventBuffer(packets[p], function(err) { settle(err); });
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+
1161
1376
  executeStatement(transaction, statement, params, callback, custom) {
1162
1377
 
1163
1378
  if (this._isClosed)
@@ -2010,6 +2225,46 @@ function decodeResponse(data: any, callback: any, cnx: any, lowercase_keys: any,
2010
2225
  };
2011
2226
  // Parse normal and lazy response
2012
2227
  return parseOpResponse(data, response, loop);
2228
+ case Const.op_batch_cs: {
2229
+ // Batch completion state (response to op_batch_exec):
2230
+ // statement, reccount, updates count, vectors count, errors
2231
+ // count, then the update-count array, the (recnum + status
2232
+ // vector) pairs and finally the status-less error recnums.
2233
+ var completion: any = {
2234
+ statementHandle: data.readInt(),
2235
+ recordCount: data.readInt(),
2236
+ };
2237
+ var updatesCount = data.readInt();
2238
+ var vectorsCount = data.readInt();
2239
+ var simpleErrorsCount = data.readInt();
2240
+
2241
+ completion.updateCounts = new Array(updatesCount);
2242
+ for (var bi = 0; bi < updatesCount; bi++) {
2243
+ completion.updateCounts[bi] = data.readInt();
2244
+ }
2245
+
2246
+ completion.detailedErrors = [];
2247
+ for (var bi = 0; bi < vectorsCount; bi++) {
2248
+ var recordNumber = data.readInt();
2249
+ var vector = readStatusVector(data);
2250
+ var recordError: any = new Error(lookupMessages(vector.status) || 'Batch record failed');
2251
+ if (vector.status.length) {
2252
+ recordError.gdscode = vector.status[0].gdscode;
2253
+ recordError.gdsparams = vector.status[0].params;
2254
+ }
2255
+ if (vector.sqlcode !== undefined) {
2256
+ recordError.sqlcode = vector.sqlcode;
2257
+ }
2258
+ completion.detailedErrors.push({ recordNumber: recordNumber, error: recordError });
2259
+ }
2260
+
2261
+ completion.errorRecordNumbers = [];
2262
+ for (var bi = 0; bi < simpleErrorsCount; bi++) {
2263
+ completion.errorRecordNumbers.push(data.readInt());
2264
+ }
2265
+
2266
+ return cb(null, { batchCompletion: completion });
2267
+ }
2013
2268
  case Const.op_fetch_response:
2014
2269
  case Const.op_sql_response:
2015
2270
  var statement = callback.statement;
@@ -2515,6 +2770,47 @@ function decodeResponse(data: any, callback: any, cnx: any, lowercase_keys: any,
2515
2770
  }
2516
2771
  }
2517
2772
 
2773
+ /**
2774
+ * Read one XDR status vector (as in op_response / op_batch_cs error
2775
+ * vectors): a stream of isc_arg_* items terminated by isc_arg_end.
2776
+ */
2777
+ function readStatusVector(data: any): { status: any[]; sqlcode?: number } {
2778
+ var result: { status: any[]; sqlcode?: number } = { status: [] };
2779
+ var item: any = {};
2780
+
2781
+ while (true) {
2782
+ var op = data.readInt();
2783
+
2784
+ switch (op) {
2785
+ case Const.isc_arg_end:
2786
+ return result;
2787
+ case Const.isc_arg_gds:
2788
+ var num = data.readInt();
2789
+ if (!num) {
2790
+ break;
2791
+ }
2792
+ item = { gdscode: num };
2793
+ result.status.push(item);
2794
+ break;
2795
+ case Const.isc_arg_string:
2796
+ case Const.isc_arg_interpreted:
2797
+ case Const.isc_arg_sql_state:
2798
+ var str = data.readString(Const.DEFAULT_ENCODING);
2799
+ (item.params = item.params || []).push(str);
2800
+ break;
2801
+ case Const.isc_arg_number:
2802
+ var n = data.readInt();
2803
+ (item.params = item.params || []).push(n);
2804
+ if (item.gdscode === Const.isc_sqlerr) {
2805
+ result.sqlcode = n;
2806
+ }
2807
+ break;
2808
+ default:
2809
+ throw new Error('Unexpected status vector item: ' + op);
2810
+ }
2811
+ }
2812
+ }
2813
+
2518
2814
  function parseOpResponse(data: any, response: any, cb?: (err?: any, response?: any) => void) {
2519
2815
  var handle = data.readInt();
2520
2816
 
@@ -2716,6 +3012,208 @@ function describe(buff: Buffer, statement: any) {
2716
3012
  unpackCharSetCollation(statement.output);
2717
3013
  }
2718
3014
 
3015
+ /**
3016
+ * Batch support: the engine requires every batch message to use EXACTLY the
3017
+ * statement's described input format (unlike op_execute, where the client
3018
+ * may declare its own format and the server converts). The BLR is therefore
3019
+ * CalcBlr(statement.input), and this helper returns one value encoder per
3020
+ * column that writes the wire representation mirroring the corresponding
3021
+ * SQLVar* decode, including scale. Returns { encoders, msglen } where
3022
+ * encoders[j](msg, value) writes a non-null value and msglen is the
3023
+ * unpacked message length sent with op_batch_create.
3024
+ */
3025
+ function buildBatchEncoders(input: any[], options: any) {
3026
+ var encoders: Array<(msg: any, v: any) => void> = [];
3027
+ var offset = 0;
3028
+ var align = function(a: number) { offset = (offset + a - 1) & ~(a - 1); };
3029
+
3030
+ var jsonAsObject = options && options.jsonAsObject;
3031
+ var toText = function(v: any): string {
3032
+ if (typeof v === 'string') return v;
3033
+ if (v instanceof Date) return v.toString();
3034
+ if (typeof v === 'object' && !Buffer.isBuffer(v)) return jsonAsObject ? JSON.stringify(v) : v.toString();
3035
+ return String(v);
3036
+ };
3037
+ var toBytes = function(v: any, meta: any, column: number): Buffer {
3038
+ var b = Buffer.isBuffer(v) ? v : Buffer.from(toText(v), Const.DEFAULT_ENCODING);
3039
+ if (b.length > meta.length) {
3040
+ throw new Error('Batch value for column ' + column + ' is ' + b.length +
3041
+ ' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
3042
+ }
3043
+ return b;
3044
+ };
3045
+ var toDate = function(v: any): Date {
3046
+ if (v instanceof Date) return v;
3047
+ if (typeof v === 'string') return parseDate(v);
3048
+ return new Date(v);
3049
+ };
3050
+ var scaled = function(v: any, scale: number): number {
3051
+ var n = typeof v === 'string' ? parseFloat(v) : Number(v);
3052
+ return scale ? Math.round(n * Math.pow(10, -scale)) : n;
3053
+ };
3054
+ var scaledBig = function(v: any, scale: number): bigint {
3055
+ if (typeof v === 'bigint') {
3056
+ return scale ? v * (10n ** BigInt(-scale)) : v;
3057
+ }
3058
+ return BigInt(scaled(v, scale));
3059
+ };
3060
+
3061
+ for (var j = 0; j < input.length; j++) {
3062
+ var meta = input[j];
3063
+ var column = j + 1;
3064
+
3065
+ switch (meta.type) {
3066
+ case Const.SQL_TEXT: // CHAR: fixed meta.length bytes, space-padded
3067
+ encoders.push((function(m, col) {
3068
+ return function(msg: any, v: any) {
3069
+ var b = toBytes(v, m, col);
3070
+ if (b.length < m.length) {
3071
+ b = Buffer.concat([b, Buffer.alloc(m.length - b.length, 0x20)]);
3072
+ }
3073
+ msg.addParamBuffer(b);
3074
+ };
3075
+ })(meta, column));
3076
+ offset += meta.length;
3077
+ break;
3078
+
3079
+ case Const.SQL_VARYING:
3080
+ case Const.SQL_NULL:
3081
+ encoders.push((function(m, col) {
3082
+ return function(msg: any, v: any) {
3083
+ var b = toBytes(v, m, col);
3084
+ msg.addInt(b.length);
3085
+ msg.addParamBuffer(b);
3086
+ };
3087
+ })(meta, column));
3088
+ align(2); offset += 2 + meta.length;
3089
+ break;
3090
+
3091
+ case Const.SQL_SHORT:
3092
+ // 2 bytes in the message struct (msglen), 4 on the XDR wire
3093
+ encoders.push((function(m) {
3094
+ return function(msg: any, v: any) { msg.addInt(scaled(v, m.scale)); };
3095
+ })(meta));
3096
+ align(2); offset += 2;
3097
+ break;
3098
+
3099
+ case Const.SQL_LONG:
3100
+ encoders.push((function(m) {
3101
+ return function(msg: any, v: any) { msg.addInt(scaled(v, m.scale)); };
3102
+ })(meta));
3103
+ align(4); offset += 4;
3104
+ break;
3105
+
3106
+ case Const.SQL_INT64:
3107
+ encoders.push((function(m) {
3108
+ return function(msg: any, v: any) {
3109
+ msg.addInt64(typeof v === 'bigint' ? (scaledBig(v, m.scale) as any) : scaled(v, m.scale));
3110
+ };
3111
+ })(meta));
3112
+ align(8); offset += 8;
3113
+ break;
3114
+
3115
+ case Const.SQL_INT128:
3116
+ encoders.push((function(m) {
3117
+ return function(msg: any, v: any) { msg.addInt128(scaledBig(v, m.scale)); };
3118
+ })(meta));
3119
+ align(8); offset += 16;
3120
+ break;
3121
+
3122
+ case Const.SQL_FLOAT:
3123
+ case Const.SQL_D_FLOAT:
3124
+ encoders.push(function(msg: any, v: any) { msg.addFloat(Number(v)); });
3125
+ align(4); offset += 4;
3126
+ break;
3127
+
3128
+ case Const.SQL_DOUBLE:
3129
+ encoders.push(function(msg: any, v: any) { msg.addDouble(Number(v)); });
3130
+ align(8); offset += 8;
3131
+ break;
3132
+
3133
+ case Const.SQL_DEC16:
3134
+ encoders.push(function(msg: any, v: any) { msg.addDecFloat16(v); });
3135
+ align(8); offset += 8;
3136
+ break;
3137
+
3138
+ case Const.SQL_DEC34:
3139
+ encoders.push(function(msg: any, v: any) { msg.addDecFloat34(v); });
3140
+ align(8); offset += 16;
3141
+ break;
3142
+
3143
+ case Const.SQL_BOOLEAN:
3144
+ // xdr_datum sends booleans as a 1-byte opaque (value byte
3145
+ // first, then 3 pad bytes), not as a big-endian int.
3146
+ encoders.push(function(msg: any, v: any) {
3147
+ msg.addBuffer(Buffer.from([v ? 1 : 0]));
3148
+ msg.addAlignment(1);
3149
+ });
3150
+ offset += 1;
3151
+ break;
3152
+
3153
+ case Const.SQL_TIMESTAMP:
3154
+ encoders.push(function(msg: any, v: any) {
3155
+ var parts = Xsql.encodeDateTimeParts(toDate(v));
3156
+ msg.addInt(parts.date);
3157
+ msg.addUInt(parts.time);
3158
+ });
3159
+ align(4); offset += 8;
3160
+ break;
3161
+
3162
+ case Const.SQL_TYPE_DATE:
3163
+ encoders.push(function(msg: any, v: any) {
3164
+ msg.addInt(Xsql.encodeDateTimeParts(toDate(v)).date);
3165
+ });
3166
+ align(4); offset += 4;
3167
+ break;
3168
+
3169
+ case Const.SQL_TYPE_TIME:
3170
+ encoders.push(function(msg: any, v: any) {
3171
+ msg.addUInt(Xsql.encodeDateTimeParts(toDate(v)).time);
3172
+ });
3173
+ align(4); offset += 4;
3174
+ break;
3175
+
3176
+ default:
3177
+ throw new Error('executeBatch does not support the type of parameter ' + column +
3178
+ ' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
3179
+ }
3180
+
3181
+ // null indicator short that CalcBlr appends per parameter
3182
+ align(2); offset += 2;
3183
+ }
3184
+
3185
+ // msglen must EXACTLY match the length the server computes from the BLR
3186
+ // (MsgMetadata::makeOffsets — no trailing alignment), or op_batch_create
3187
+ // fails with -804 SQLDA errors.
3188
+ return { encoders: encoders, msglen: offset };
3189
+ }
3190
+
3191
+ /**
3192
+ * Build the batch parameter buffer (p_batch_pb): a wide-tagged clumplet
3193
+ * buffer — version byte, then per clumplet a tag byte, int32 LE length and
3194
+ * the value (ints are 4-byte LE).
3195
+ */
3196
+ function buildBatchPb(options: any): Buffer {
3197
+ var parts: number[] = [Const.BATCH_VERSION1];
3198
+
3199
+ var addIntClumplet = function(tag: number, value: number) {
3200
+ parts.push(tag, 4, 0, 0, 0, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF);
3201
+ };
3202
+
3203
+ addIntClumplet(Const.BATCH_TAG_RECORD_COUNTS, 1);
3204
+ if (!options || options.multiError !== false) {
3205
+ addIntClumplet(Const.BATCH_TAG_MULTIERROR, 1);
3206
+ }
3207
+ if (options && options.bufferSize) {
3208
+ addIntClumplet(Const.BATCH_TAG_BUFFER_BYTES_SIZE, options.bufferSize);
3209
+ }
3210
+ if (options && options.detailedErrors !== undefined) {
3211
+ addIntClumplet(Const.BATCH_TAG_DETAILED_ERRORS, options.detailedErrors);
3212
+ }
3213
+
3214
+ return Buffer.from(parts);
3215
+ }
3216
+
2719
3217
  function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
2720
3218
  blr.addBytes([Const.blr_version5, Const.blr_begin, Const.blr_message, 0]); // + message number
2721
3219
  blr.addWord(xsqlda.length * 2);
package/src/wire/const.ts CHANGED
@@ -121,6 +121,14 @@ const op = {
121
121
  op_crypt_key_callback : 97,
122
122
  op_cond_accept : 98, // Server accepts connection, returns some data to client
123
123
  // and asks client to continue authentication before attach call
124
+ op_batch_create : 99, // Create a statement batch (protocol 16+)
125
+ op_batch_msg : 100, // Add a bunch of messages to the batch
126
+ op_batch_exec : 101, // Execute the batch; server answers with op_batch_cs
127
+ op_batch_rls : 102, // Release the batch
128
+ op_batch_cs : 103, // Batch completion state (response to op_batch_exec)
129
+ op_batch_cancel : 109, // Cancel the batch
130
+ op_batch_sync : 110, // Wait for batch processing
131
+ op_info_batch : 111,
124
132
  op_fetch_scroll : 112,
125
133
  op_info_cursor : 113,
126
134
  op_inline_blob : 114,
@@ -132,6 +140,19 @@ const dsql = {
132
140
  DSQL_unprepare : 4, // >: 2.5
133
141
  };
134
142
 
143
+ // Batch parameter buffer (op_batch_create p_batch_pb) — a wide-tagged
144
+ // clumplet buffer: version byte, then per clumplet a tag byte, an int32 LE
145
+ // length and the value bytes. Tags from Firebird's IBatch interface.
146
+ const batch = {
147
+ BATCH_VERSION1 : 1,
148
+ BATCH_TAG_MULTIERROR : 1, // continue after per-record errors
149
+ BATCH_TAG_RECORD_COUNTS : 2, // return per-record update counts
150
+ BATCH_TAG_BUFFER_BYTES_SIZE: 3, // server-side batch buffer limit
151
+ BATCH_TAG_BLOB_POLICY : 4,
152
+ BATCH_TAG_DETAILED_ERRORS : 5, // max detailed status vectors returned
153
+ BATCH_BLOB_NONE : 0,
154
+ };
155
+
135
156
  // fb_cancel_operation kinds sent with op_cancel (protocol 12+ / Firebird 2.5+)
136
157
  const cancelKind = {
137
158
  fb_cancel_disable : 1, // disable execution of fb_cancel_raise requests
@@ -866,6 +887,7 @@ const Const = Object.freeze({
866
887
  ...authOptions,
867
888
  ...blr,
868
889
  ...blobType,
890
+ ...batch,
869
891
  ...buffer,
870
892
  ...cancelKind,
871
893
  ...cnct,
@@ -229,6 +229,54 @@ class Database extends Events.EventEmitter {
229
229
  return self;
230
230
  }
231
231
 
232
+ /**
233
+ * Bulk-execute `query` once per row via the Firebird 4 batch API
234
+ * (protocol 16+) with all-or-nothing semantics: the batch runs in its
235
+ * own transaction, committed only when every record succeeded and
236
+ * rolled back otherwise. On failure the error of the first failed
237
+ * record is reported, with the full completion attached as
238
+ * err.batchCompletion. Use transaction.executeBatch for partial-success
239
+ * handling.
240
+ */
241
+ executeBatch(query: string, rows: any[][], callback?: any, options?: any): this {
242
+ var self = this;
243
+
244
+ self.connection.startTransaction(function(err: any, transaction: any) {
245
+ if (err) {
246
+ doError(err, callback);
247
+ return;
248
+ }
249
+
250
+ transaction.executeBatch(query, rows, function(err: any, result: any) {
251
+ if (err) {
252
+ transaction.rollback(function() {
253
+ doError(err, callback);
254
+ });
255
+ return;
256
+ }
257
+
258
+ if (!result.success) {
259
+ transaction.rollback(function() {
260
+ var first = result.errors.length ? result.errors[0] : null;
261
+ var batchError: any = first
262
+ ? first.error
263
+ : new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
264
+ batchError.batchCompletion = result;
265
+ doError(batchError, callback);
266
+ });
267
+ return;
268
+ }
269
+
270
+ transaction.commit(function(err: any) {
271
+ if (callback)
272
+ callback(err, result);
273
+ });
274
+ }, options);
275
+ });
276
+
277
+ return self;
278
+ }
279
+
232
280
  sequentially(query: string, params?: any, on?: any, callback?: any, options: any = {}): this {
233
281
  if (params instanceof Function) {
234
282
  options = callback;
@@ -473,6 +521,11 @@ class Database extends Events.EventEmitter {
473
521
  return fromCallback(function(cb) { self.execute(query, params, cb, options); });
474
522
  }
475
523
 
524
+ executeBatchAsync(query: string, rows: any[][], options?: any): Promise<any> {
525
+ var self = this;
526
+ return fromCallback(function(cb) { self.executeBatch(query, rows, cb, options); });
527
+ }
528
+
476
529
  sequentiallyAsync(query: string, params?: any, on?: any, options?: any): Promise<void> {
477
530
  if (params instanceof Function) {
478
531
  options = on;
@@ -282,12 +282,12 @@ export class XdrWriter {
282
282
  this.pos += 4;
283
283
  }
284
284
 
285
- addInt64(value: number): void {
285
+ addInt64(value: number | bigint): void {
286
286
  this.ensure(8);
287
- // Note: precision is limited to Number.MAX_SAFE_INTEGER (±2^53-1).
288
- // Values outside this range lose precision, which matches the previous
289
- // Long.fromNumber() behaviour.
290
- this.buffer.writeBigInt64BE(BigInt(Math.trunc(value)), this.pos);
287
+ // Note: for numbers, precision is limited to Number.MAX_SAFE_INTEGER
288
+ // (±2^53-1); values outside this range lose precision, which matches
289
+ // the previous Long.fromNumber() behaviour. BigInts keep full precision.
290
+ this.buffer.writeBigInt64BE(typeof value === 'bigint' ? value : BigInt(Math.trunc(value)), this.pos);
291
291
  this.pos += 8;
292
292
  }
293
293
 
@@ -381,6 +381,12 @@ export class XdrWriter {
381
381
  this.pos += 8;
382
382
  }
383
383
 
384
+ addFloat(value: number): void {
385
+ this.ensure(4);
386
+ this.buffer.writeFloatBE(value, this.pos);
387
+ this.pos += 4;
388
+ }
389
+
384
390
  addQuad(quad: { low: number; high: number }): void {
385
391
  this.ensure(8);
386
392
  var b = this.buffer;