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.
- package/README.md +60 -4
- package/lib/types.d.ts +35 -0
- package/lib/wire/connection.d.ts +15 -0
- package/lib/wire/connection.js +461 -0
- package/lib/wire/const.d.ts +15 -0
- package/lib/wire/const.js +21 -0
- package/lib/wire/database.d.ts +11 -0
- package/lib/wire/database.js +46 -0
- package/lib/wire/serialize.d.ts +2 -1
- package/lib/wire/serialize.js +9 -4
- package/lib/wire/statement.d.ts +6 -0
- package/lib/wire/statement.js +11 -0
- package/lib/wire/transaction.d.ts +11 -0
- package/lib/wire/transaction.js +27 -0
- package/lib/wire/xsqlvar.d.ts +9 -0
- package/lib/wire/xsqlvar.js +22 -11
- package/package.json +1 -1
- package/src/types.ts +35 -0
- package/src/wire/connection.ts +498 -0
- package/src/wire/const.ts +22 -0
- package/src/wire/database.ts +53 -0
- package/src/wire/serialize.ts +11 -5
- package/src/wire/statement.ts +13 -0
- package/src/wire/transaction.ts +30 -0
- package/src/wire/xsqlvar.ts +25 -14
package/src/wire/connection.ts
CHANGED
|
@@ -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,
|
package/src/wire/database.ts
CHANGED
|
@@ -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;
|
package/src/wire/serialize.ts
CHANGED
|
@@ -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
|
|
288
|
-
//
|
|
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;
|