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
package/lib/wire/connection.js
CHANGED
|
@@ -44,6 +44,7 @@ const srp = __importStar(require("../srp"));
|
|
|
44
44
|
const crypt = __importStar(require("../unix-crypt"));
|
|
45
45
|
const const_1 = __importDefault(require("./const"));
|
|
46
46
|
const Xsql = __importStar(require("./xsqlvar"));
|
|
47
|
+
const codepages_1 = require("./codepages");
|
|
47
48
|
const service_1 = __importDefault(require("./service"));
|
|
48
49
|
const database_1 = __importDefault(require("./database"));
|
|
49
50
|
const statement_1 = __importDefault(require("./statement"));
|
|
@@ -109,6 +110,20 @@ function statementCacheLimit(options) {
|
|
|
109
110
|
}
|
|
110
111
|
// SQL type-code names live in xsqlvar.ts alongside the descriptors
|
|
111
112
|
const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
|
|
113
|
+
/**
|
|
114
|
+
* Build the wire parameter for a text value in the CONNECTION charset:
|
|
115
|
+
* plain SQLParamString for UTF-8 connections, pre-encoded bytes for
|
|
116
|
+
* everything else — SQLParamString hardwires utf8, which corrupted
|
|
117
|
+
* writes on latin1/codepage connections (issues #319/#301).
|
|
118
|
+
*/
|
|
119
|
+
function textParam(options, value) {
|
|
120
|
+
const codec = Xsql.resolveTextCodec(options);
|
|
121
|
+
const enc = Xsql.resolveTextEncoding(options);
|
|
122
|
+
if (!codec && enc === 'utf8') {
|
|
123
|
+
return new Xsql.SQLParamString(value);
|
|
124
|
+
}
|
|
125
|
+
return new Xsql.SQLParamBuffer(Xsql.encodeConnectionText(options, value));
|
|
126
|
+
}
|
|
112
127
|
/**
|
|
113
128
|
* Run the user's typeCast hook (options.typeCast) for one column value.
|
|
114
129
|
* The hook receives the column metadata and a next() returning the value
|
|
@@ -240,6 +255,24 @@ class Connection {
|
|
|
240
255
|
(0, callback_1.doError)(err, queue[i]);
|
|
241
256
|
}
|
|
242
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Deliver a connection-level error to 'error' listeners — and ONLY to
|
|
260
|
+
* listeners. Emitting an unlistened 'error' makes Node throw the error
|
|
261
|
+
* object as an uncaught exception; for errors that originate in
|
|
262
|
+
* background contexts (the reconnect timer, socket-level failures whose
|
|
263
|
+
* operations are separately rejected via _rejectPending) that crashes
|
|
264
|
+
* the process — or, under a test runner, fails whatever unrelated test
|
|
265
|
+
* happens to be running. The failing operations themselves always
|
|
266
|
+
* still receive their error through their own callbacks.
|
|
267
|
+
*/
|
|
268
|
+
_emitError(err) {
|
|
269
|
+
if (this.db && typeof this.db.listenerCount === 'function' && this.db.listenerCount('error') > 0) {
|
|
270
|
+
this.db.emit('error', err);
|
|
271
|
+
}
|
|
272
|
+
else if (process.env.FIREBIRD_DEBUG) {
|
|
273
|
+
console.warn('[fb-debug] connection error (no error listener):', err && err.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
243
276
|
_bind_events(host, port, callback) {
|
|
244
277
|
var self = this;
|
|
245
278
|
self._socket.on('close', function () {
|
|
@@ -256,6 +289,14 @@ class Connection {
|
|
|
256
289
|
return;
|
|
257
290
|
}
|
|
258
291
|
self._rejectPending(lostError);
|
|
292
|
+
// Pooled connections do not self-reconnect: the pool is the
|
|
293
|
+
// recovery authority (dead-connection check on checkout + the
|
|
294
|
+
// reaper), and a background reconnect here would produce a
|
|
295
|
+
// zombie attachment the pool no longer tracks — whose own
|
|
296
|
+
// failures then surface as uncatchable async errors.
|
|
297
|
+
if (self.options && self.options.isPool) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
259
300
|
self._retry_connection_id = setTimeout(function () {
|
|
260
301
|
self._socket.removeAllListeners();
|
|
261
302
|
// transiently null while the replacement Connection is built
|
|
@@ -264,12 +305,12 @@ class Connection {
|
|
|
264
305
|
var ctx = new Connection(host, port, function (err) {
|
|
265
306
|
ctx.connect(self.options, function (err) {
|
|
266
307
|
if (err) {
|
|
267
|
-
self.
|
|
308
|
+
self._emitError(err);
|
|
268
309
|
return;
|
|
269
310
|
}
|
|
270
311
|
ctx.attach(self.options, function (err) {
|
|
271
312
|
if (err) {
|
|
272
|
-
self.
|
|
313
|
+
self._emitError(err);
|
|
273
314
|
return;
|
|
274
315
|
}
|
|
275
316
|
self.db.emit('reconnect');
|
|
@@ -281,8 +322,10 @@ class Connection {
|
|
|
281
322
|
});
|
|
282
323
|
self._socket.on('error', function (e) {
|
|
283
324
|
self.error = e;
|
|
284
|
-
|
|
285
|
-
|
|
325
|
+
// listeners only (_emitError): the affected operations get their
|
|
326
|
+
// errors via the close handler's _rejectPending — an unlistened
|
|
327
|
+
// socket error must not become an uncaught exception
|
|
328
|
+
self._emitError(e);
|
|
286
329
|
if (callback)
|
|
287
330
|
callback(e);
|
|
288
331
|
});
|
|
@@ -648,7 +691,9 @@ class Connection {
|
|
|
648
691
|
msg.pos = 0;
|
|
649
692
|
blr.pos = 0;
|
|
650
693
|
blr.addByte(const_1.default.isc_dpb_version1);
|
|
651
|
-
|
|
694
|
+
// charset names are sent verbatim — normalize case so encoding:
|
|
695
|
+
// 'utf8'/'win1251' (e.g. from URI query params) is accepted
|
|
696
|
+
blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
652
697
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
653
698
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
|
|
654
699
|
blr.addByte(const_1.default.isc_dpb_utf8_filename);
|
|
@@ -750,8 +795,13 @@ class Connection {
|
|
|
750
795
|
var blr = this._blr;
|
|
751
796
|
blr.pos = 0;
|
|
752
797
|
blr.addByte(const_1.default.isc_dpb_version1);
|
|
753
|
-
|
|
754
|
-
|
|
798
|
+
// honour options.encoding on the CREATE path too — hardcoding UTF8
|
|
799
|
+
// for lc_ctype made attachOrCreate silently ignore the requested
|
|
800
|
+
// connection charset (issue #319: 'Malformed string' for codepage
|
|
801
|
+
// text). The new database's DEFAULT charset follows the connection
|
|
802
|
+
// encoding unless options.defaultCharset overrides it.
|
|
803
|
+
blr.addString(const_1.default.isc_dpb_set_db_charset, String(options.defaultCharset || options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
804
|
+
blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
755
805
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
756
806
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
|
|
757
807
|
blr.addByte(const_1.default.isc_dpb_utf8_filename);
|
|
@@ -835,7 +885,9 @@ class Connection {
|
|
|
835
885
|
}
|
|
836
886
|
throwClosed(callback) {
|
|
837
887
|
var err = new Error('Connection is closed.');
|
|
838
|
-
|
|
888
|
+
// listeners only: the caller receives the error through its own
|
|
889
|
+
// callback below either way
|
|
890
|
+
this._emitError(err);
|
|
839
891
|
if (callback)
|
|
840
892
|
callback(err);
|
|
841
893
|
return this;
|
|
@@ -1027,7 +1079,9 @@ class Connection {
|
|
|
1027
1079
|
msg.addInt(transaction.handle);
|
|
1028
1080
|
msg.addInt(0xFFFF);
|
|
1029
1081
|
msg.addInt(3); // dialect = 3
|
|
1030
|
-
|
|
1082
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1083
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1084
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1031
1085
|
msg.addBlr(blr);
|
|
1032
1086
|
msg.addInt(65535); // buffer_length
|
|
1033
1087
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
|
|
@@ -1075,7 +1129,9 @@ class Connection {
|
|
|
1075
1129
|
msg.addInt(transaction.handle);
|
|
1076
1130
|
msg.addInt(statement.handle);
|
|
1077
1131
|
msg.addInt(3); // dialect = 3
|
|
1078
|
-
|
|
1132
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1133
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1134
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1079
1135
|
msg.addBlr(blr);
|
|
1080
1136
|
msg.addInt(65535); // buffer_length
|
|
1081
1137
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
|
|
@@ -1100,10 +1156,13 @@ class Connection {
|
|
|
1100
1156
|
* gets an in-order response (op_batch_cs for exec), so the regular
|
|
1101
1157
|
* response queue keeps everything in sync.
|
|
1102
1158
|
*
|
|
1103
|
-
* rows: array of parameter arrays, one per record. BLOB
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1159
|
+
* rows: array of parameter arrays, one per record. BLOB columns accept
|
|
1160
|
+
* Buffers, strings, JSON-able objects or pre-created blob quad ids —
|
|
1161
|
+
* values are uploaded as transaction blobs first (all initiated
|
|
1162
|
+
* back-to-back so they pipeline) and the batch messages reference their
|
|
1163
|
+
* ids. ARRAY columns are not supported. The callback receives a
|
|
1164
|
+
* completion object: { recordCount, updateCounts, errors:
|
|
1165
|
+
* [{recordNumber, error}], errorRecordNumbers, success }.
|
|
1107
1166
|
*/
|
|
1108
1167
|
executeBatch(transaction, statement, rows, callback, options) {
|
|
1109
1168
|
options = options || {};
|
|
@@ -1135,6 +1194,82 @@ class Connection {
|
|
|
1135
1194
|
return;
|
|
1136
1195
|
}
|
|
1137
1196
|
}
|
|
1197
|
+
var self = this;
|
|
1198
|
+
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
1199
|
+
// transaction blob and replace it (in a cloned row) with the quad
|
|
1200
|
+
// id the batch message will carry. All uploads are initiated
|
|
1201
|
+
// back-to-back, so the create/segment/close ops pipeline on the
|
|
1202
|
+
// wire instead of paying a round trip per blob.
|
|
1203
|
+
var blobCols = [];
|
|
1204
|
+
for (var bj = 0; bj < input.length; bj++) {
|
|
1205
|
+
var bt = input[bj].type;
|
|
1206
|
+
if (bt === const_1.default.SQL_BLOB || bt === const_1.default.SQL_QUAD) {
|
|
1207
|
+
blobCols.push(bj);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// all-NULL blob columns are common — a large row set must not pay
|
|
1211
|
+
// for a full clone when there is nothing to upload or unwrap
|
|
1212
|
+
var needsBlobPass = false;
|
|
1213
|
+
if (blobCols.length) {
|
|
1214
|
+
outer: for (var ri = 0; ri < rows.length; ri++) {
|
|
1215
|
+
for (var ci = 0; ci < blobCols.length; ci++) {
|
|
1216
|
+
var bv = rows[ri][blobCols[ci]];
|
|
1217
|
+
if (bv !== null && bv !== undefined) {
|
|
1218
|
+
needsBlobPass = true;
|
|
1219
|
+
break outer;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (needsBlobPass) {
|
|
1225
|
+
var cloned = rows.map(function (r) { return r.slice(); });
|
|
1226
|
+
var pendingBlobs = 1; // sentinel so zero uploads still settle
|
|
1227
|
+
var blobFailure = null;
|
|
1228
|
+
var settleBlobs = function (err) {
|
|
1229
|
+
if (err && !blobFailure) {
|
|
1230
|
+
blobFailure = err;
|
|
1231
|
+
}
|
|
1232
|
+
if (--pendingBlobs) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (blobFailure) {
|
|
1236
|
+
(0, callback_1.doError)(blobFailure, callback);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
self._executeBatchEncoded(transaction, statement, cloned, callback, options);
|
|
1240
|
+
};
|
|
1241
|
+
cloned.forEach(function (row) {
|
|
1242
|
+
blobCols.forEach(function (j) {
|
|
1243
|
+
var v = row[j];
|
|
1244
|
+
if (v === null || v === undefined) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// a pre-created blob id (SQLParamQuad wrapper) passes
|
|
1248
|
+
// through; plain {high, low} objects are deliberately NOT
|
|
1249
|
+
// treated as ids — they are legitimate JSON blob content
|
|
1250
|
+
// and would silently misroute to a bogus blob reference
|
|
1251
|
+
if (v instanceof Xsql.SQLParamQuad) {
|
|
1252
|
+
row[j] = v.value;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
pendingBlobs++;
|
|
1256
|
+
self.uploadBlob(transaction, v, function (err, oid) {
|
|
1257
|
+
if (!err) {
|
|
1258
|
+
row[j] = oid;
|
|
1259
|
+
}
|
|
1260
|
+
settleBlobs(err);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
settleBlobs();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
this._executeBatchEncoded(transaction, statement, rows, callback, options);
|
|
1268
|
+
}
|
|
1269
|
+
/** Encode and send the batch packets (rows are fully materialized:
|
|
1270
|
+
* blob values already replaced by quad ids by executeBatch). */
|
|
1271
|
+
_executeBatchEncoded(transaction, statement, rows, callback, options) {
|
|
1272
|
+
var input = statement.input;
|
|
1138
1273
|
var built;
|
|
1139
1274
|
try {
|
|
1140
1275
|
built = buildBatchEncoders(input, Object.assign({}, this.options, options));
|
|
@@ -1302,9 +1437,9 @@ class Connection {
|
|
|
1302
1437
|
if (Buffer.isBuffer(value))
|
|
1303
1438
|
b = value;
|
|
1304
1439
|
else if (typeof (value) === 'string')
|
|
1305
|
-
b =
|
|
1440
|
+
b = Xsql.encodeConnectionText(self.options, value);
|
|
1306
1441
|
else if (!isStream)
|
|
1307
|
-
b =
|
|
1442
|
+
b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
|
|
1308
1443
|
// Use configured transfer size or default to 1024
|
|
1309
1444
|
var chunkSize = self.options.blobChunkSize || 1024;
|
|
1310
1445
|
if (Buffer.isBuffer(b)) {
|
|
@@ -1445,14 +1580,18 @@ class Connection {
|
|
|
1445
1580
|
ret[i] = new Xsql.SQLParamDouble(value);
|
|
1446
1581
|
break;
|
|
1447
1582
|
case 'string':
|
|
1448
|
-
ret[i] =
|
|
1583
|
+
ret[i] = textParam(self.options, value);
|
|
1449
1584
|
break;
|
|
1450
1585
|
case 'boolean':
|
|
1451
|
-
|
|
1586
|
+
// metadata-directed: BOOLEAN targets get a
|
|
1587
|
+
// real blr_bool (issue #122); smallint
|
|
1588
|
+
// targets keep the legacy 0/1 (BOOLEAN
|
|
1589
|
+
// does not convert to numbers)
|
|
1590
|
+
ret[i] = new Xsql.SQLParamBool(value, meta.type === const_1.default.SQL_BOOLEAN);
|
|
1452
1591
|
break;
|
|
1453
1592
|
default:
|
|
1454
1593
|
//throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
|
|
1455
|
-
ret[i] =
|
|
1594
|
+
ret[i] = textParam(self.options, value.toString());
|
|
1456
1595
|
break;
|
|
1457
1596
|
}
|
|
1458
1597
|
}
|
|
@@ -1683,6 +1822,33 @@ class Connection {
|
|
|
1683
1822
|
msg.addInt(65535); // buffer_length
|
|
1684
1823
|
this._queueEvent(callback);
|
|
1685
1824
|
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Resolve the pending blobAsText fetches of a decoded row batch
|
|
1827
|
+
* (ret.arrBlob) and write the text back into ret.data. Reads run
|
|
1828
|
+
* sequentially to respect Firebird's per-connection open-blob-handle
|
|
1829
|
+
* limit (issue #387). Used by fetchAll for cursors and by
|
|
1830
|
+
* transaction.execute for op_execute2 singletons (EXECUTE PROCEDURE /
|
|
1831
|
+
* RETURNING — issue #305, whose blobs never resolved before).
|
|
1832
|
+
*/
|
|
1833
|
+
resolveTextBlobs(transaction, ret, callback) {
|
|
1834
|
+
const self = this;
|
|
1835
|
+
const fns = (ret && ret.arrBlob) || [];
|
|
1836
|
+
if (!fns.length) {
|
|
1837
|
+
return callback();
|
|
1838
|
+
}
|
|
1839
|
+
const read = (index) => {
|
|
1840
|
+
if (index >= fns.length) {
|
|
1841
|
+
return callback();
|
|
1842
|
+
}
|
|
1843
|
+
fns[index](transaction).then((blob) => {
|
|
1844
|
+
// nestTables === true rows: the value lives in the
|
|
1845
|
+
// per-table sub-object, not on the row itself
|
|
1846
|
+
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(self.options, blob.meta || {}, parseValueIfJson(blob.value, self.options));
|
|
1847
|
+
read(index + 1);
|
|
1848
|
+
}, callback);
|
|
1849
|
+
};
|
|
1850
|
+
read(0);
|
|
1851
|
+
}
|
|
1686
1852
|
fetchAll(statement, transaction, callback) {
|
|
1687
1853
|
const self = this;
|
|
1688
1854
|
const custom = statement.options || {};
|
|
@@ -1695,26 +1861,10 @@ class Connection {
|
|
|
1695
1861
|
return;
|
|
1696
1862
|
}
|
|
1697
1863
|
if (ret && ret.data && ret.data.length) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
const arrBlobFns = ret.arrBlob || [];
|
|
1703
|
-
const readBlobsSequentially = (index, results) => {
|
|
1704
|
-
if (index >= arrBlobFns.length) {
|
|
1705
|
-
return Promise.resolve(results);
|
|
1706
|
-
}
|
|
1707
|
-
return arrBlobFns[index](transaction).then((v) => {
|
|
1708
|
-
results.push(v);
|
|
1709
|
-
return readBlobsSequentially(index + 1, results);
|
|
1710
|
-
});
|
|
1711
|
-
};
|
|
1712
|
-
readBlobsSequentially(0, []).then((arrBlob) => {
|
|
1713
|
-
for (let i = 0; i < arrBlob.length; i++) {
|
|
1714
|
-
const blob = arrBlob[i];
|
|
1715
|
-
// nestTables === true rows: the value lives in the
|
|
1716
|
-
// per-table sub-object, not on the row itself
|
|
1717
|
-
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
|
|
1864
|
+
self.resolveTextBlobs(transaction, ret, (blobErr) => {
|
|
1865
|
+
if (blobErr) {
|
|
1866
|
+
callback(blobErr);
|
|
1867
|
+
return;
|
|
1718
1868
|
}
|
|
1719
1869
|
doSynchronousLoop(ret.data, (row, _i, next) => {
|
|
1720
1870
|
const pos = asStream ? streamIndex++ : (data.push(row) - 1);
|
|
@@ -1736,7 +1886,7 @@ class Connection {
|
|
|
1736
1886
|
self.fetch(statement, transaction, const_1.default.DEFAULT_FETCHSIZE, loop);
|
|
1737
1887
|
}
|
|
1738
1888
|
});
|
|
1739
|
-
})
|
|
1889
|
+
});
|
|
1740
1890
|
return;
|
|
1741
1891
|
}
|
|
1742
1892
|
if (ret && ret.fetched) {
|
|
@@ -1794,6 +1944,47 @@ class Connection {
|
|
|
1794
1944
|
msg.addBlr(blr);
|
|
1795
1945
|
this._queueEvent(callback);
|
|
1796
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Create a transaction blob, upload `value` (Buffer, string, or a
|
|
1949
|
+
* JSON-able object) and deliver its quad id. executeBatch's blob
|
|
1950
|
+
* pre-pass uses this: batch messages reference pre-created transaction
|
|
1951
|
+
* blobs (the batch parameter buffer's default BLOB_NONE policy), just
|
|
1952
|
+
* like the classic execute path stores blob params.
|
|
1953
|
+
*/
|
|
1954
|
+
uploadBlob(transaction, value, callback) {
|
|
1955
|
+
var self = this;
|
|
1956
|
+
var b;
|
|
1957
|
+
if (Buffer.isBuffer(value)) {
|
|
1958
|
+
b = value;
|
|
1959
|
+
}
|
|
1960
|
+
else if (typeof value === 'string') {
|
|
1961
|
+
b = Xsql.encodeConnectionText(this.options, value);
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
b = Xsql.encodeConnectionText(this.options, JSON.stringify(value));
|
|
1965
|
+
}
|
|
1966
|
+
self.createBlob2(transaction, function (err, blob) {
|
|
1967
|
+
if (err) {
|
|
1968
|
+
return callback(err);
|
|
1969
|
+
}
|
|
1970
|
+
var chunkSize = self.options.blobChunkSize || 1024;
|
|
1971
|
+
// bufferReader's next() drops errors — capture the first segment
|
|
1972
|
+
// failure so a truncated upload is never reported as success
|
|
1973
|
+
var segmentError = null;
|
|
1974
|
+
bufferReader(b, chunkSize, function (part, next) {
|
|
1975
|
+
self.batchSegments(blob, part, function (segErr) {
|
|
1976
|
+
if (segErr && !segmentError) {
|
|
1977
|
+
segmentError = segErr;
|
|
1978
|
+
}
|
|
1979
|
+
next();
|
|
1980
|
+
});
|
|
1981
|
+
}, function () {
|
|
1982
|
+
self.closeBlob(blob, function (closeErr) {
|
|
1983
|
+
callback(segmentError || closeErr, blob.oid);
|
|
1984
|
+
}, false);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1797
1988
|
svcattach(options, callback, svc) {
|
|
1798
1989
|
this._lowercase_keys = options.lowercase_keys || const_1.default.DEFAULT_LOWERCASE_KEYS;
|
|
1799
1990
|
var database = options.database || options.filename;
|
|
@@ -2832,6 +3023,43 @@ function describe(buff, statement) {
|
|
|
2832
3023
|
}
|
|
2833
3024
|
unpackCharSetCollation(statement.input);
|
|
2834
3025
|
unpackCharSetCollation(statement.output);
|
|
3026
|
+
scaleOutputLengths(statement.output, statement.connection && statement.connection.options);
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Widen declared OUTPUT byte lengths for text columns whose charset the
|
|
3030
|
+
* server did NOT transliterate in the describe. Under a UTF8 connection
|
|
3031
|
+
* the server rewrites known charsets (a WIN1251 VARCHAR(15) is described
|
|
3032
|
+
* as UTF8 with length 60) — but NONE (and any untransliterated charset)
|
|
3033
|
+
* keeps its native length, and the engine's fetch-time capacity check
|
|
3034
|
+
* then treats those bytes as connection-charset characters: VARCHAR(15)
|
|
3035
|
+
* NONE declared as 15 bytes only fits floor(15/4) = 3 characters and
|
|
3036
|
+
* longer values fail with "string right truncation" (issue #422).
|
|
3037
|
+
* Declaring charCapacity × connectionWidth bytes makes the check pass;
|
|
3038
|
+
* the width-based trim in the text decoders restores the column's true
|
|
3039
|
+
* character capacity. The native length is preserved for metadata.
|
|
3040
|
+
*/
|
|
3041
|
+
function scaleOutputLengths(output, options) {
|
|
3042
|
+
if (!output || !output.length)
|
|
3043
|
+
return;
|
|
3044
|
+
const connWidth = Xsql.getFirebirdCharsetWidth(options && options.encoding);
|
|
3045
|
+
if (connWidth <= 1)
|
|
3046
|
+
return;
|
|
3047
|
+
for (let i = 0; i < output.length; i++) {
|
|
3048
|
+
const p = output[i];
|
|
3049
|
+
if (!p || (p.type !== const_1.default.SQL_TEXT && p.type !== const_1.default.SQL_VARYING))
|
|
3050
|
+
continue;
|
|
3051
|
+
// OCTETS (id 1) is binary and never transliterated
|
|
3052
|
+
if (p.charSetId === undefined || p.charSetId === 1)
|
|
3053
|
+
continue;
|
|
3054
|
+
const colWidth = (0, codepages_1.charsetWidthById)(p.charSetId);
|
|
3055
|
+
if (colWidth >= connWidth)
|
|
3056
|
+
continue;
|
|
3057
|
+
p.nativeLength = p.length;
|
|
3058
|
+
// BLR encodes the length as a 16-bit word — cap the widened value
|
|
3059
|
+
// (a >16KB single-byte column keeps its full byte capacity, it just
|
|
3060
|
+
// can't be over-declared four-fold)
|
|
3061
|
+
p.length = Math.min(Math.floor(p.length / colWidth) * connWidth, Math.floor(0xFFFF / connWidth) * connWidth);
|
|
3062
|
+
}
|
|
2835
3063
|
}
|
|
2836
3064
|
/**
|
|
2837
3065
|
* Batch support: the engine requires every batch message to use EXACTLY the
|
|
@@ -2858,7 +3086,7 @@ function buildBatchEncoders(input, options) {
|
|
|
2858
3086
|
return String(v);
|
|
2859
3087
|
};
|
|
2860
3088
|
var toBytes = function (v, meta, column) {
|
|
2861
|
-
var b = Buffer.isBuffer(v) ? v :
|
|
3089
|
+
var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
|
|
2862
3090
|
if (b.length > meta.length) {
|
|
2863
3091
|
throw new Error('Batch value for column ' + column + ' is ' + b.length +
|
|
2864
3092
|
' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
|
|
@@ -2994,6 +3222,24 @@ function buildBatchEncoders(input, options) {
|
|
|
2994
3222
|
align(4);
|
|
2995
3223
|
offset += 4;
|
|
2996
3224
|
break;
|
|
3225
|
+
case const_1.default.SQL_BLOB:
|
|
3226
|
+
case const_1.default.SQL_QUAD:
|
|
3227
|
+
// the value is a transaction blob quad id — placed by
|
|
3228
|
+
// executeBatch's uploadBlob pre-pass, or passed by the
|
|
3229
|
+
// caller directly. ISC_QUAD: two longs, align 4.
|
|
3230
|
+
encoders.push((function (col) {
|
|
3231
|
+
return function (msg, v) {
|
|
3232
|
+
if (!v || typeof v.high !== 'number' || typeof v.low !== 'number') {
|
|
3233
|
+
throw new Error('Batch value for BLOB column ' + col +
|
|
3234
|
+
' must be a Buffer, string, object, or blob quad id');
|
|
3235
|
+
}
|
|
3236
|
+
msg.addInt(v.high);
|
|
3237
|
+
msg.addInt(v.low);
|
|
3238
|
+
};
|
|
3239
|
+
})(column));
|
|
3240
|
+
align(4);
|
|
3241
|
+
offset += 8;
|
|
3242
|
+
break;
|
|
2997
3243
|
default:
|
|
2998
3244
|
throw new Error('executeBatch does not support the type of parameter ' + column +
|
|
2999
3245
|
' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
|
|
@@ -3042,11 +3288,15 @@ function CalcBlr(blr, xsqlda) {
|
|
|
3042
3288
|
}
|
|
3043
3289
|
function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
|
|
3044
3290
|
const infoValue = { row, column, value: '', meta, table };
|
|
3291
|
+
// decode ONCE from the concatenated bytes: per-segment toString() both
|
|
3292
|
+
// ignored the connection charset (#301 — cyrillic mojibake) and could
|
|
3293
|
+
// split a multi-byte UTF-8 character across segment boundaries
|
|
3294
|
+
const chunks = [];
|
|
3045
3295
|
return (transactionArg) => {
|
|
3046
3296
|
const cacheKey = `${id.high}:${id.low}`;
|
|
3047
3297
|
if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
|
|
3048
3298
|
const data = statement.connection._inlineBlobs.get(cacheKey);
|
|
3049
|
-
infoValue.value = data ?
|
|
3299
|
+
infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
|
|
3050
3300
|
return Promise.resolve(infoValue);
|
|
3051
3301
|
}
|
|
3052
3302
|
const singleTransaction = transactionArg === undefined;
|
|
@@ -3085,13 +3335,13 @@ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
|
|
|
3085
3335
|
}
|
|
3086
3336
|
if (ret.buffer) {
|
|
3087
3337
|
const blr = new serialize_1.BlrReader(ret.buffer);
|
|
3088
|
-
|
|
3089
|
-
infoValue.value += data.toString(const_1.default.DEFAULT_ENCODING);
|
|
3338
|
+
chunks.push(blr.readSegment());
|
|
3090
3339
|
}
|
|
3091
3340
|
if (ret.handle !== 2) {
|
|
3092
3341
|
read();
|
|
3093
3342
|
return;
|
|
3094
3343
|
}
|
|
3344
|
+
infoValue.value = Xsql.decodeConnectionText(statement.connection.options, Buffer.concat(chunks));
|
|
3095
3345
|
statement.connection.closeBlob(blob);
|
|
3096
3346
|
if (singleTransaction) {
|
|
3097
3347
|
transaction.commit((err) => {
|
package/lib/wire/database.d.ts
CHANGED
|
@@ -52,6 +52,15 @@ declare class Database extends Events.EventEmitter {
|
|
|
52
52
|
* blobAsText and jsonAsObject all apply.
|
|
53
53
|
*/
|
|
54
54
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): import("node:stream").Readable;
|
|
55
|
+
/**
|
|
56
|
+
* Bulk-insert Writable (the COPY FROM analogue, Firebird 4.0+): write
|
|
57
|
+
* parameter-array rows, they are flushed in chunks through the batch
|
|
58
|
+
* API on one prepared statement. Runs its own transaction — committed
|
|
59
|
+
* on finish, rolled back on error/destroy (all-or-nothing for the
|
|
60
|
+
* whole stream). BLOB columns accept Buffers/strings. After 'finish',
|
|
61
|
+
* stream.recordCount / stream.affectedRows carry the totals.
|
|
62
|
+
*/
|
|
63
|
+
batchStream(query: string, options?: any): import("node:stream").Writable;
|
|
55
64
|
query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): this;
|
|
56
65
|
drop(callback?: SimpleCallback): void;
|
|
57
66
|
/**
|
package/lib/wire/database.js
CHANGED
|
@@ -11,6 +11,7 @@ const xsqlvar_1 = require("./xsqlvar");
|
|
|
11
11
|
const eventConnection_1 = __importDefault(require("./eventConnection"));
|
|
12
12
|
const fbEventManager_1 = __importDefault(require("./fbEventManager"));
|
|
13
13
|
const query_stream_1 = __importDefault(require("./query-stream"));
|
|
14
|
+
const batch_stream_1 = __importDefault(require("./batch-stream"));
|
|
14
15
|
/***************************************
|
|
15
16
|
*
|
|
16
17
|
* Database
|
|
@@ -241,12 +242,7 @@ class Database extends events_1.default.EventEmitter {
|
|
|
241
242
|
}
|
|
242
243
|
if (!result.success) {
|
|
243
244
|
transaction.rollback(function () {
|
|
244
|
-
|
|
245
|
-
var batchError = first
|
|
246
|
-
? first.error
|
|
247
|
-
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
248
|
-
batchError.batchCompletion = result;
|
|
249
|
-
(0, callback_1.doError)(batchError, callback);
|
|
245
|
+
(0, callback_1.doError)((0, utils_1.batchResultToError)(result), callback);
|
|
250
246
|
});
|
|
251
247
|
return;
|
|
252
248
|
}
|
|
@@ -347,6 +343,17 @@ class Database extends events_1.default.EventEmitter {
|
|
|
347
343
|
queryStream(query, params, options) {
|
|
348
344
|
return (0, query_stream_1.default)(this, query, params, options);
|
|
349
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Bulk-insert Writable (the COPY FROM analogue, Firebird 4.0+): write
|
|
348
|
+
* parameter-array rows, they are flushed in chunks through the batch
|
|
349
|
+
* API on one prepared statement. Runs its own transaction — committed
|
|
350
|
+
* on finish, rolled back on error/destroy (all-or-nothing for the
|
|
351
|
+
* whole stream). BLOB columns accept Buffers/strings. After 'finish',
|
|
352
|
+
* stream.recordCount / stream.affectedRows carry the totals.
|
|
353
|
+
*/
|
|
354
|
+
batchStream(query, options) {
|
|
355
|
+
return (0, batch_stream_1.default)(this, query, options, true);
|
|
356
|
+
}
|
|
350
357
|
query(query, params, callback, options = {}) {
|
|
351
358
|
if (params instanceof Function) {
|
|
352
359
|
options = callback || {};
|
package/lib/wire/serialize.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export declare class XdrWriter {
|
|
|
48
48
|
addDecFloat34(value: number | string | bigint): void;
|
|
49
49
|
addUInt(value: number): void;
|
|
50
50
|
addString(s: string, encoding: BufferEncoding): void;
|
|
51
|
+
/** addString for pre-encoded bytes (codepage connection charsets). */
|
|
52
|
+
addStringBuffer(b: Buffer): void;
|
|
51
53
|
addText(s: string, encoding: BufferEncoding): void;
|
|
52
54
|
addParamBuffer(b: Buffer): void;
|
|
53
55
|
addBlr(blr: BlrWriter): void;
|
package/lib/wire/serialize.js
CHANGED
|
@@ -277,6 +277,16 @@ class XdrWriter {
|
|
|
277
277
|
this.buffer.fill(0, this.pos + len, this.pos + alen);
|
|
278
278
|
this.pos += alen;
|
|
279
279
|
}
|
|
280
|
+
/** addString for pre-encoded bytes (codepage connection charsets). */
|
|
281
|
+
addStringBuffer(b) {
|
|
282
|
+
var alen = align(b.length);
|
|
283
|
+
this.ensure(alen + 4);
|
|
284
|
+
this.buffer.writeInt32BE(b.length, this.pos);
|
|
285
|
+
this.pos += 4;
|
|
286
|
+
b.copy(this.buffer, this.pos);
|
|
287
|
+
this.buffer.fill(0, this.pos + b.length, this.pos + alen);
|
|
288
|
+
this.pos += alen;
|
|
289
|
+
}
|
|
280
290
|
addText(s, encoding) {
|
|
281
291
|
var len = Buffer.byteLength(s, encoding);
|
|
282
292
|
var alen = align(len);
|
package/lib/wire/socket.js
CHANGED
|
@@ -132,8 +132,14 @@ class Socket {
|
|
|
132
132
|
* Compress and/or encrypt data before sending to socket.
|
|
133
133
|
*/
|
|
134
134
|
write(data, defer = false) {
|
|
135
|
+
// Callers pass views of the connection's shared _msg buffer, and both
|
|
136
|
+
// net.Socket (when it cannot flush immediately) and zlib keep a
|
|
137
|
+
// REFERENCE to the chunk — a later sender rebuilding _msg would then
|
|
138
|
+
// corrupt this queued packet (intermittent, load-dependent). Own the
|
|
139
|
+
// bytes at the send boundary, once.
|
|
140
|
+
data = Buffer.from(data);
|
|
135
141
|
if (process.env.FIREBIRD_DEBUG) {
|
|
136
|
-
console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length,
|
|
142
|
+
console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length, data.toString('hex'), this.encrypt, defer);
|
|
137
143
|
}
|
|
138
144
|
if (defer) {
|
|
139
145
|
// Accumulate deferred packets instead of overwriting. Multiple
|
|
@@ -142,8 +148,8 @@ class Socket {
|
|
|
142
148
|
// overwriting the buffer silently drops packets and desynchronises
|
|
143
149
|
// the request/response queue, causing the connection to hang.
|
|
144
150
|
this.buffer = this.buffer
|
|
145
|
-
? Buffer.concat([this.buffer,
|
|
146
|
-
:
|
|
151
|
+
? Buffer.concat([this.buffer, data])
|
|
152
|
+
: data;
|
|
147
153
|
return;
|
|
148
154
|
}
|
|
149
155
|
if (!defer && this.buffer) {
|
|
@@ -48,6 +48,12 @@ declare class Transaction {
|
|
|
48
48
|
* stream ends — commit or roll back yourself.
|
|
49
49
|
*/
|
|
50
50
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): import("node:stream").Readable;
|
|
51
|
+
/**
|
|
52
|
+
* Bulk-insert Writable running inside this transaction (see
|
|
53
|
+
* Database.batchStream). The transaction is NOT committed or rolled
|
|
54
|
+
* back by the stream — settle it yourself after 'finish'/'error'.
|
|
55
|
+
*/
|
|
56
|
+
batchStream(query: string, options?: any): import("node:stream").Writable;
|
|
51
57
|
query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): void;
|
|
52
58
|
/**
|
|
53
59
|
* Execute `query` once per row in `rows` using the Firebird 4 batch API
|