node-firebird 2.15.1 → 2.16.1
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 +50 -12
- package/lib/types.d.ts +6 -0
- package/lib/uri.d.ts +9 -5
- package/lib/uri.js +40 -9
- package/lib/wire/connection.js +64 -23
- package/lib/wire/serialize.js +1 -1
- package/lib/wire/socket.d.ts +1 -1
- package/lib/wire/socket.js +3 -2
- package/lib/wire/xsqlvar.d.ts +8 -0
- package/lib/wire/xsqlvar.js +103 -9
- package/package.json +1 -1
- package/src/types.ts +6 -0
- package/src/uri.ts +43 -9
- package/src/wire/connection.ts +58 -24
- package/src/wire/serialize.ts +1 -1
- package/src/wire/socket.ts +3 -2
- package/src/wire/xsqlvar.ts +107 -9
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- [Installation](#installation)
|
|
12
12
|
- [Usage](#usage) — including [developing the driver](#developing-the-driver)
|
|
13
13
|
- [Promises and async/await](#promises-and-asyncawait) — the `*Async` API plus `withConnection` / `withTransaction` helpers
|
|
14
|
-
- [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
|
|
14
|
+
- [Connection types](#connection-types) — connection options, `firebird://` / `inet://` URIs and traditional connection strings, classic connections, pooling
|
|
15
15
|
- [Database object (db)](#database-object-db) — database, transaction and statement methods/options
|
|
16
16
|
- [Examples](#examples) — parametrized queries, tagged-template queries (sql), named placeholders, nested result tables (nestTables), row-key transforms (transformKeys), result metadata / affected rows (withMeta), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
|
|
17
17
|
- [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts incl. BLOBs), bulk-insert stream (batchStream), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
|
|
@@ -200,6 +200,7 @@ options.dbCryptConfig = undefined; // optional; database encryption key for encr
|
|
|
200
200
|
options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
|
|
201
201
|
options.enableKeepAlive = true; // TCP keepalive probing to detect dead/stale connections (same option names as mysql2); set to false to disable
|
|
202
202
|
options.keepAliveInitialDelay = 60000; // ms a socket must be idle before the first keepalive probe (ignored when enableKeepAlive is false)
|
|
203
|
+
options.ipFamily = 4; // optional; force IPv4 (4) or IPv6 (6) when resolving host — also set by inet4:// / inet6:// URIs
|
|
203
204
|
options.parallelWorkers = undefined; // optional; request multiple thread workers for maintenance/index tasks (FB >= 5)
|
|
204
205
|
options.maxInlineBlobSize = undefined; // optional; threshold size in bytes for inline blob transmission (default 65535, FB >= 5.0.3)
|
|
205
206
|
options.maxNegotiatedProtocols = undefined; // optional; cap how many protocol versions are offered, oldest first (default: all, up to Protocol 20; set to 10 to stop at Protocol 19)
|
|
@@ -248,6 +249,20 @@ be passed as query parameters. IPv6 hosts use brackets:
|
|
|
248
249
|
`firebird://[::1]:3050/employee`. The parser is exported as
|
|
249
250
|
`Firebird.parseConnectionUri(uri)` if you need the resulting options object.
|
|
250
251
|
|
|
252
|
+
Firebird's own URL-style connection strings (Firebird 3+) are accepted with
|
|
253
|
+
the same rules, so a string that works with isql works here unchanged:
|
|
254
|
+
|
|
255
|
+
| URI | meaning |
|
|
256
|
+
| :--- | :--- |
|
|
257
|
+
| `inet://host/employee` | same as `firebird://host/employee` |
|
|
258
|
+
| `inet://host:3051//var/fb/prod.fdb` | host, port and an absolute path |
|
|
259
|
+
| `inet4://host/employee` | resolve `host` as IPv4 only (sets `ipFamily: 4`) |
|
|
260
|
+
| `inet6://[::1]/employee` | IPv6 only (sets `ipFamily: 6`) |
|
|
261
|
+
|
|
262
|
+
`inet://` strings may carry credentials and query options exactly like
|
|
263
|
+
`firebird://` ones. The local-only transports `xnet://` and `wnet://` are
|
|
264
|
+
rejected — this driver speaks TCP only.
|
|
265
|
+
|
|
251
266
|
### Traditional connection strings (old style)
|
|
252
267
|
|
|
253
268
|
The classic Firebird connection string format — the same
|
|
@@ -748,16 +763,29 @@ option `numericMode` controls how those result values are exposed:
|
|
|
748
763
|
|
|
749
764
|
| Mode | Result policy |
|
|
750
765
|
| :--- | :--- |
|
|
751
|
-
| `Firebird.NUMERIC_MODE_LOSSY` | INT64-backed values are returned as `number
|
|
766
|
+
| `Firebird.NUMERIC_MODE_LOSSY` | INT64-backed values are returned as `number`, losing precision beyond the safe integer range; INT128 returns a `number` for a safe coefficient and an exact scaled `string` for an unsafe one. |
|
|
752
767
|
| `Firebird.NUMERIC_MODE_SAFE` | Safe coefficients are returned as `number`; unsafe coefficients as exact scaled `string`. |
|
|
753
768
|
| `Firebird.NUMERIC_MODE_STRING` | All values are returned as exact scaled `string`. |
|
|
754
769
|
|
|
755
|
-
`LOSSY` decodes INT64-backed values through JavaScript `Number
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
770
|
+
`LOSSY` decodes INT64-backed values through JavaScript `Number`, so a
|
|
771
|
+
coefficient beyond JavaScript's safe integer range loses digits silently.
|
|
772
|
+
INT128 instead takes a mixed path: a coefficient inside the inclusive safe
|
|
773
|
+
range is returned as a scaled `number`, and one outside it — in either
|
|
774
|
+
direction — is returned as an exact scaled `string`, formatted just as
|
|
775
|
+
`STRING` would format it. The result type of an INT128 column therefore
|
|
776
|
+
depends on the value:
|
|
777
|
+
|
|
778
|
+
```js
|
|
779
|
+
// numericMode: LOSSY, INT128 column
|
|
780
|
+
// coefficient 12345, scale -2 -> 123.45 (number)
|
|
781
|
+
// coefficient -12345, scale -2 -> -123.45 (number)
|
|
782
|
+
// coefficient 2^127-1, scale 0 -> '170141183460469231731687303715884105727'
|
|
783
|
+
// coefficient -2^127, scale 0 -> '-170141183460469231731687303715884105728'
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Use `SAFE` or `STRING` when a stable result type matters. `LOSSY` remains the
|
|
787
|
+
default so that adding `numericMode` does not silently change result types for
|
|
788
|
+
applications upgrading from earlier node-firebird releases.
|
|
761
789
|
|
|
762
790
|
`SAFE` tests the raw integer coefficient against JavaScript's inclusive safe
|
|
763
791
|
range (`Number.MIN_SAFE_INTEGER` through `Number.MAX_SAFE_INTEGER`) before
|
|
@@ -774,6 +802,12 @@ const db = await Firebird.attachAsync({
|
|
|
774
802
|
// DECIMAL coefficient 420000,-4 -> '42.0000'
|
|
775
803
|
```
|
|
776
804
|
|
|
805
|
+
Every declared scale is supported in all three modes, including the 18
|
|
806
|
+
fractional digits an INT64-backed `NUMERIC(18,18)` allows and the 38 an INT128
|
|
807
|
+
column can declare. A Firebird coefficient represents `value * 10^scale`, so
|
|
808
|
+
the positive scales that dialect 1 and some legacy metadata still produce
|
|
809
|
+
scale the value up rather than down.
|
|
810
|
+
|
|
777
811
|
The string literals `'lossy'`, `'safe'`, and `'string'` are accepted too,
|
|
778
812
|
including in connection URIs (`?numericMode=safe`). `NULL` remains `null` in
|
|
779
813
|
every mode. The option does not change `FLOAT`, `DOUBLE`, `DECFLOAT`, or input
|
|
@@ -1893,10 +1927,14 @@ in bytes).
|
|
|
1893
1927
|
Notes:
|
|
1894
1928
|
|
|
1895
1929
|
- Requires wire protocol 16+ (Firebird 4.0 or newer server).
|
|
1896
|
-
- Values are encoded from the statement's own parameter metadata
|
|
1897
|
-
NUMERIC
|
|
1898
|
-
|
|
1899
|
-
|
|
1930
|
+
- Values are encoded from the statement's own parameter metadata. Fixed-point
|
|
1931
|
+
`NUMERIC`/`DECIMAL`, `BIGINT`, and `INT128` parameters accept numbers, decimal
|
|
1932
|
+
strings, and `BigInt`. Decimal strings retain their exact digits; finite
|
|
1933
|
+
numbers are interpreted through their canonical decimal representation and
|
|
1934
|
+
rounded to the declared scale with ties away from zero. Use a string (or
|
|
1935
|
+
`BigInt` for whole values) when the input is outside JavaScript's safe integer
|
|
1936
|
+
range. `BOOLEAN`, `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE`, and `DECFLOAT`
|
|
1937
|
+
use their corresponding wire types.
|
|
1900
1938
|
- `BLOB` columns accept Buffers, strings, JSON-able objects, or
|
|
1901
1939
|
pre-created blob quad ids: values are uploaded as transaction blobs
|
|
1902
1940
|
first — all initiated back-to-back so the blob ops pipeline on the
|
package/lib/types.d.ts
CHANGED
|
@@ -432,6 +432,12 @@ export interface Options {
|
|
|
432
432
|
* probe is sent (default 60000). Ignored when enableKeepAlive is false.
|
|
433
433
|
*/
|
|
434
434
|
keepAliveInitialDelay?: number;
|
|
435
|
+
/**
|
|
436
|
+
* Force the TCP socket to IPv4 (4) or IPv6 (6) when resolving `host`.
|
|
437
|
+
* Unset lets the resolver choose. Set automatically by `inet4://` /
|
|
438
|
+
* `inet6://` connection URIs.
|
|
439
|
+
*/
|
|
440
|
+
ipFamily?: 4 | 6;
|
|
435
441
|
pluginName?: string;
|
|
436
442
|
parallelWorkers?: number;
|
|
437
443
|
maxInlineBlobSize?: number;
|
package/lib/uri.d.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
***************************************/
|
|
6
6
|
import type { Options } from './types';
|
|
7
7
|
/**
|
|
8
|
-
* Parse a firebird:// connection URI into an options object.
|
|
8
|
+
* Parse a firebird:// or inet:// connection URI into an options object.
|
|
9
9
|
*
|
|
10
10
|
* firebird://user:password@host:port/database?option=value&...
|
|
11
|
+
* inet://host:port/database (Firebird's own URL style;
|
|
12
|
+
* inet4://... / inet6://... also force IPv4 / IPv6)
|
|
11
13
|
*
|
|
12
14
|
* The database part:
|
|
13
15
|
* firebird://host/employee → alias "employee"
|
|
@@ -45,13 +47,15 @@ export declare function parseConnectionUri(uri: string): Options;
|
|
|
45
47
|
*/
|
|
46
48
|
export declare function parseOldStyleConnectionString(str: string): Options;
|
|
47
49
|
/**
|
|
48
|
-
* Parse any connection string the driver accepts: a firebird://
|
|
49
|
-
* traditional [host[/port]:]database string when
|
|
50
|
+
* Parse any connection string the driver accepts: a firebird:// or
|
|
51
|
+
* inet[4|6]:// URI, or a traditional [host[/port]:]database string when
|
|
52
|
+
* there is no scheme.
|
|
50
53
|
*/
|
|
51
54
|
export declare function parseConnectionString(str: string): Options;
|
|
52
55
|
/**
|
|
53
|
-
* Accept either an options object or a connection string (firebird://
|
|
54
|
-
* or traditional host[/port]:database) everywhere options are
|
|
56
|
+
* Accept either an options object or a connection string (firebird:// or
|
|
57
|
+
* inet:// URI, or traditional host[/port]:database) everywhere options are
|
|
58
|
+
* taken.
|
|
55
59
|
* Strings are parsed; objects pass through unchanged.
|
|
56
60
|
*/
|
|
57
61
|
export declare function normalizeOptions<T>(options: T | string): T;
|
package/lib/uri.js
CHANGED
|
@@ -22,7 +22,7 @@ const NUMBER_KEYS = new Set([
|
|
|
22
22
|
'port', 'pageSize', 'timeout', 'retryConnectionInterval',
|
|
23
23
|
'blobChunkSize', 'blobReadChunkSize', 'wireCrypt', 'parallelWorkers',
|
|
24
24
|
'maxInlineBlobSize', 'maxNegotiatedProtocols', 'connectTimeout',
|
|
25
|
-
'min', 'idleTimeoutMillis', 'keepAliveInitialDelay',
|
|
25
|
+
'min', 'idleTimeoutMillis', 'keepAliveInitialDelay', 'ipFamily',
|
|
26
26
|
]);
|
|
27
27
|
function coerce(key, value) {
|
|
28
28
|
if (BOOLEAN_KEYS.has(key)) {
|
|
@@ -38,9 +38,29 @@ function coerce(key, value) {
|
|
|
38
38
|
return value;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* URI schemes accepted by parseConnectionUri. `firebird://` is this driver's
|
|
42
|
+
* own scheme; `inet://`, `inet4://` and `inet6://` are Firebird's native
|
|
43
|
+
* URL-style connection strings (Firebird 3+), so a string that works with
|
|
44
|
+
* isql works here too. inet4/inet6 pin the socket's IP family (the
|
|
45
|
+
* `ipFamily` option); inet and firebird let the resolver pick.
|
|
46
|
+
*/
|
|
47
|
+
const URI_SCHEMES = {
|
|
48
|
+
'firebird:': undefined,
|
|
49
|
+
'inet:': undefined,
|
|
50
|
+
'inet4:': 4,
|
|
51
|
+
'inet6:': 6,
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Firebird protocols this driver cannot speak: they are local-machine IPC
|
|
55
|
+
* transports (shared memory / named pipes), not TCP.
|
|
56
|
+
*/
|
|
57
|
+
const LOCAL_SCHEMES = /^(xnet|wnet):$/;
|
|
58
|
+
/**
|
|
59
|
+
* Parse a firebird:// or inet:// connection URI into an options object.
|
|
42
60
|
*
|
|
43
61
|
* firebird://user:password@host:port/database?option=value&...
|
|
62
|
+
* inet://host:port/database (Firebird's own URL style;
|
|
63
|
+
* inet4://... / inet6://... also force IPv4 / IPv6)
|
|
44
64
|
*
|
|
45
65
|
* The database part:
|
|
46
66
|
* firebird://host/employee → alias "employee"
|
|
@@ -64,11 +84,20 @@ function parseConnectionUri(uri) {
|
|
|
64
84
|
catch (e) {
|
|
65
85
|
throw new Error('Invalid connection URI: ' + uri);
|
|
66
86
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
var scheme = url.protocol.toLowerCase();
|
|
88
|
+
if (LOCAL_SCHEMES.test(scheme)) {
|
|
89
|
+
throw new Error('Unsupported connection URI scheme "' + scheme.replace(/:$/, '') +
|
|
90
|
+
'" (local IPC transports are not available over the wire — use inet:// or firebird://)');
|
|
91
|
+
}
|
|
92
|
+
if (!Object.prototype.hasOwnProperty.call(URI_SCHEMES, scheme)) {
|
|
93
|
+
throw new Error('Unsupported connection URI scheme "' + scheme.replace(/:$/, '') +
|
|
94
|
+
'" (expected firebird://, inet://, inet4:// or inet6://)');
|
|
70
95
|
}
|
|
71
96
|
var options = {};
|
|
97
|
+
var ipFamily = URI_SCHEMES[scheme];
|
|
98
|
+
if (ipFamily !== undefined) {
|
|
99
|
+
options.ipFamily = ipFamily;
|
|
100
|
+
}
|
|
72
101
|
if (url.hostname) {
|
|
73
102
|
// URL keeps IPv6 hostnames bracketed ([::1]); net.connect wants them bare
|
|
74
103
|
options.host = url.hostname.replace(/^\[(.*)\]$/, '$1');
|
|
@@ -172,8 +201,9 @@ function parseOldStyleConnectionString(str) {
|
|
|
172
201
|
}
|
|
173
202
|
const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
174
203
|
/**
|
|
175
|
-
* Parse any connection string the driver accepts: a firebird://
|
|
176
|
-
* traditional [host[/port]:]database string when
|
|
204
|
+
* Parse any connection string the driver accepts: a firebird:// or
|
|
205
|
+
* inet[4|6]:// URI, or a traditional [host[/port]:]database string when
|
|
206
|
+
* there is no scheme.
|
|
177
207
|
*/
|
|
178
208
|
function parseConnectionString(str) {
|
|
179
209
|
return URI_SCHEME.test(str)
|
|
@@ -181,8 +211,9 @@ function parseConnectionString(str) {
|
|
|
181
211
|
: parseOldStyleConnectionString(str);
|
|
182
212
|
}
|
|
183
213
|
/**
|
|
184
|
-
* Accept either an options object or a connection string (firebird://
|
|
185
|
-
* or traditional host[/port]:database) everywhere options are
|
|
214
|
+
* Accept either an options object or a connection string (firebird:// or
|
|
215
|
+
* inet:// URI, or traditional host[/port]:database) everywhere options are
|
|
216
|
+
* taken.
|
|
186
217
|
* Strings are parsed; objects pass through unchanged.
|
|
187
218
|
*/
|
|
188
219
|
function normalizeOptions(options) {
|
package/lib/wire/connection.js
CHANGED
|
@@ -168,7 +168,7 @@ class Connection {
|
|
|
168
168
|
this._detachTimeout;
|
|
169
169
|
this._detachCallback;
|
|
170
170
|
this._detachAuto;
|
|
171
|
-
this._socket = new socket_1.default(port, host, options.enableKeepAlive !== false, options.keepAliveInitialDelay);
|
|
171
|
+
this._socket = new socket_1.default(port, host, options.enableKeepAlive !== false, options.keepAliveInitialDelay, options.ipFamily);
|
|
172
172
|
this._pending = [];
|
|
173
173
|
this._isOpened = false;
|
|
174
174
|
this._isClosed = false;
|
|
@@ -1194,6 +1194,17 @@ class Connection {
|
|
|
1194
1194
|
return;
|
|
1195
1195
|
}
|
|
1196
1196
|
}
|
|
1197
|
+
// Validate fixed-point values before the BLOB pre-pass. Blob uploads
|
|
1198
|
+
// write immediately, so deferring numeric validation until message
|
|
1199
|
+
// encoding could leave transaction blob state behind for a batch that
|
|
1200
|
+
// can never be sent.
|
|
1201
|
+
try {
|
|
1202
|
+
validateBatchFixedPointRows(input, rows);
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
(0, callback_1.doError)(err, callback);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1197
1208
|
var self = this;
|
|
1198
1209
|
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
1199
1210
|
// transaction blob and replace it (in a cloned row) with the quad
|
|
@@ -3065,6 +3076,46 @@ function scaleOutputLengths(output, options) {
|
|
|
3065
3076
|
p.length = Math.min(Math.floor(p.length / colWidth) * connWidth, Math.floor(0xFFFF / connWidth) * connWidth);
|
|
3066
3077
|
}
|
|
3067
3078
|
}
|
|
3079
|
+
function scaleBatchFixedPoint(value, meta, bits, column) {
|
|
3080
|
+
try {
|
|
3081
|
+
return Xsql.toScaledInteger(value, meta.scale, bits);
|
|
3082
|
+
}
|
|
3083
|
+
catch (err) {
|
|
3084
|
+
var message = err instanceof Error ? err.message : String(err);
|
|
3085
|
+
throw new Error('Invalid fixed-point batch value for column ' + column +
|
|
3086
|
+
' (' + (meta.field || '?') + '): ' + message);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
/** Validate values whose batch wire representation is metadata-directed.
|
|
3090
|
+
* This runs before BLOB uploads, which may write to the transaction as soon
|
|
3091
|
+
* as executeBatch starts its asynchronous pre-pass. */
|
|
3092
|
+
function validateBatchFixedPointRows(input, rows) {
|
|
3093
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3094
|
+
for (var j = 0; j < input.length; j++) {
|
|
3095
|
+
var value = rows[i][j];
|
|
3096
|
+
if (value === null || value === undefined)
|
|
3097
|
+
continue;
|
|
3098
|
+
var bits = undefined;
|
|
3099
|
+
switch (input[j].type) {
|
|
3100
|
+
case const_1.default.SQL_SHORT:
|
|
3101
|
+
bits = 16;
|
|
3102
|
+
break;
|
|
3103
|
+
case const_1.default.SQL_LONG:
|
|
3104
|
+
bits = 32;
|
|
3105
|
+
break;
|
|
3106
|
+
case const_1.default.SQL_INT64:
|
|
3107
|
+
bits = 64;
|
|
3108
|
+
break;
|
|
3109
|
+
case const_1.default.SQL_INT128:
|
|
3110
|
+
bits = 128;
|
|
3111
|
+
break;
|
|
3112
|
+
}
|
|
3113
|
+
if (bits !== undefined) {
|
|
3114
|
+
scaleBatchFixedPoint(value, input[j], bits, j + 1);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3068
3119
|
/**
|
|
3069
3120
|
* Batch support: the engine requires every batch message to use EXACTLY the
|
|
3070
3121
|
* statement's described input format (unlike op_execute, where the client
|
|
@@ -3104,16 +3155,6 @@ function buildBatchEncoders(input, options) {
|
|
|
3104
3155
|
return (0, utils_1.parseDate)(v);
|
|
3105
3156
|
return new Date(v);
|
|
3106
3157
|
};
|
|
3107
|
-
var scaled = function (v, scale) {
|
|
3108
|
-
var n = typeof v === 'string' ? parseFloat(v) : Number(v);
|
|
3109
|
-
return scale ? Math.round(n * Math.pow(10, -scale)) : n;
|
|
3110
|
-
};
|
|
3111
|
-
var scaledBig = function (v, scale) {
|
|
3112
|
-
if (typeof v === 'bigint') {
|
|
3113
|
-
return scale ? v * (10n ** BigInt(-scale)) : v;
|
|
3114
|
-
}
|
|
3115
|
-
return BigInt(scaled(v, scale));
|
|
3116
|
-
};
|
|
3117
3158
|
for (var j = 0; j < input.length; j++) {
|
|
3118
3159
|
var meta = input[j];
|
|
3119
3160
|
var column = j + 1;
|
|
@@ -3144,32 +3185,32 @@ function buildBatchEncoders(input, options) {
|
|
|
3144
3185
|
break;
|
|
3145
3186
|
case const_1.default.SQL_SHORT:
|
|
3146
3187
|
// 2 bytes in the message struct (msglen), 4 on the XDR wire
|
|
3147
|
-
encoders.push((function (m) {
|
|
3148
|
-
return function (msg, v) { msg.addInt(
|
|
3149
|
-
})(meta));
|
|
3188
|
+
encoders.push((function (m, col) {
|
|
3189
|
+
return function (msg, v) { msg.addInt(Number(scaleBatchFixedPoint(v, m, 16, col))); };
|
|
3190
|
+
})(meta, column));
|
|
3150
3191
|
align(2);
|
|
3151
3192
|
offset += 2;
|
|
3152
3193
|
break;
|
|
3153
3194
|
case const_1.default.SQL_LONG:
|
|
3154
|
-
encoders.push((function (m) {
|
|
3155
|
-
return function (msg, v) { msg.addInt(
|
|
3156
|
-
})(meta));
|
|
3195
|
+
encoders.push((function (m, col) {
|
|
3196
|
+
return function (msg, v) { msg.addInt(Number(scaleBatchFixedPoint(v, m, 32, col))); };
|
|
3197
|
+
})(meta, column));
|
|
3157
3198
|
align(4);
|
|
3158
3199
|
offset += 4;
|
|
3159
3200
|
break;
|
|
3160
3201
|
case const_1.default.SQL_INT64:
|
|
3161
|
-
encoders.push((function (m) {
|
|
3202
|
+
encoders.push((function (m, col) {
|
|
3162
3203
|
return function (msg, v) {
|
|
3163
|
-
msg.addInt64(
|
|
3204
|
+
msg.addInt64(scaleBatchFixedPoint(v, m, 64, col));
|
|
3164
3205
|
};
|
|
3165
|
-
})(meta));
|
|
3206
|
+
})(meta, column));
|
|
3166
3207
|
align(8);
|
|
3167
3208
|
offset += 8;
|
|
3168
3209
|
break;
|
|
3169
3210
|
case const_1.default.SQL_INT128:
|
|
3170
|
-
encoders.push((function (m) {
|
|
3171
|
-
return function (msg, v) { msg.addInt128(
|
|
3172
|
-
})(meta));
|
|
3211
|
+
encoders.push((function (m, col) {
|
|
3212
|
+
return function (msg, v) { msg.addInt128(scaleBatchFixedPoint(v, m, 128, col)); };
|
|
3213
|
+
})(meta, column));
|
|
3173
3214
|
align(8);
|
|
3174
3215
|
offset += 16;
|
|
3175
3216
|
break;
|
package/lib/wire/serialize.js
CHANGED
|
@@ -241,7 +241,7 @@ class XdrWriter {
|
|
|
241
241
|
const bigValue = BigInt(value);
|
|
242
242
|
const high = bigValue >> BigInt(64);
|
|
243
243
|
const low = bigValue & BigInt("0xFFFFFFFFFFFFFFFF");
|
|
244
|
-
this.buffer.
|
|
244
|
+
this.buffer.writeBigInt64BE(high, this.pos);
|
|
245
245
|
this.pos += 8;
|
|
246
246
|
this.buffer.writeBigUInt64BE(low, this.pos);
|
|
247
247
|
this.pos += 8;
|
package/lib/wire/socket.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ declare class Socket {
|
|
|
32
32
|
encrypt: boolean;
|
|
33
33
|
encryptCipher: any;
|
|
34
34
|
decryptCipher: any;
|
|
35
|
-
constructor(port: number, host: string, enableKeepAlive?: boolean, keepAliveInitialDelay?: number);
|
|
35
|
+
constructor(port: number, host: string, enableKeepAlive?: boolean, keepAliveInitialDelay?: number, ipFamily?: 4 | 6);
|
|
36
36
|
/**
|
|
37
37
|
* Decompress and/or decrypt data when received.
|
|
38
38
|
* Override on data event.
|
package/lib/wire/socket.js
CHANGED
|
@@ -82,9 +82,10 @@ class ChaChaCipher {
|
|
|
82
82
|
*/
|
|
83
83
|
class Socket {
|
|
84
84
|
static { this.Arc4 = Arc4; }
|
|
85
|
-
constructor(port, host, enableKeepAlive = true, keepAliveInitialDelay = 60000) {
|
|
85
|
+
constructor(port, host, enableKeepAlive = true, keepAliveInitialDelay = 60000, ipFamily) {
|
|
86
86
|
this.compress = false;
|
|
87
|
-
|
|
87
|
+
// family: 0 is "either" (Node's default); 4/6 pin the resolver
|
|
88
|
+
this._socket = net_1.default.createConnection({ port: port, host: host, family: ipFamily || 0 });
|
|
88
89
|
this._socket.setNoDelay(true);
|
|
89
90
|
// TCP keepalive probing detects dead/stale connections; the delay is
|
|
90
91
|
// how long the socket must be idle before the first probe.
|
package/lib/wire/xsqlvar.d.ts
CHANGED
|
@@ -273,6 +273,14 @@ export declare class SQLParamInt128 {
|
|
|
273
273
|
calcBlr(blr: BlrWriter): void;
|
|
274
274
|
encode(data: XdrWriter): void;
|
|
275
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Convert a decimal input to the signed integer coefficient used by a
|
|
278
|
+
* Firebird fixed-point wire type. Numbers are interpreted through their
|
|
279
|
+
* canonical decimal string; strings and bigints never pass through Number.
|
|
280
|
+
* Digits discarded by the target scale are rounded to nearest, ties away
|
|
281
|
+
* from zero, matching Firebird's conversion of decimal parameter text.
|
|
282
|
+
*/
|
|
283
|
+
export declare function toScaledInteger(value: number | string | bigint, scale: number, bits: 16 | 32 | 64 | 128): bigint;
|
|
276
284
|
export declare class SQLParamDecFloat16 {
|
|
277
285
|
value: any;
|
|
278
286
|
constructor(value: any);
|
package/lib/wire/xsqlvar.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.nestCell = nestCell;
|
|
|
18
18
|
exports.describeField = describeField;
|
|
19
19
|
exports.describeFields = describeFields;
|
|
20
20
|
exports.parseRecordCounts = parseRecordCounts;
|
|
21
|
+
exports.toScaledInteger = toScaledInteger;
|
|
21
22
|
exports.encodeDateTimeParts = encodeDateTimeParts;
|
|
22
23
|
const const_1 = __importDefault(require("./const"));
|
|
23
24
|
const serialize_1 = require("./serialize");
|
|
@@ -27,11 +28,29 @@ const codepages_1 = require("./codepages");
|
|
|
27
28
|
* SQLVar
|
|
28
29
|
*
|
|
29
30
|
***************************************/
|
|
30
|
-
const ScaleDivisor = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, 1000000000000000];
|
|
31
31
|
const DateOffset = 40587, TimeCoeff = 86400000, MsPerMinute = 60000;
|
|
32
32
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
33
33
|
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
34
34
|
const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
35
|
+
/**
|
|
36
|
+
* Apply a Firebird numeric scale to a value already narrowed to a JS number.
|
|
37
|
+
*
|
|
38
|
+
* This replaces the former lookup table of divisors, which only held
|
|
39
|
+
* 10^0..10^15: any larger scale indexed past its end and yielded NaN. That is
|
|
40
|
+
* reachable for INT64 (NUMERIC(18,18)) and routine for INT128, where the scale
|
|
41
|
+
* runs to 38. Math.pow(10, n) returns the identical double for every exponent
|
|
42
|
+
* the table did cover, so in-range results are unchanged.
|
|
43
|
+
*
|
|
44
|
+
* Positive scales multiply, matching decodeExactNumeric and formatScaledBigInt;
|
|
45
|
+
* the table path divided by them, which was the wrong direction.
|
|
46
|
+
*/
|
|
47
|
+
function applyScale(value, scale) {
|
|
48
|
+
if (!scale)
|
|
49
|
+
return value;
|
|
50
|
+
return scale < 0
|
|
51
|
+
? value / Math.pow(10, -scale)
|
|
52
|
+
: value * Math.pow(10, scale);
|
|
53
|
+
}
|
|
35
54
|
/** Format a signed Firebird integer coefficient without passing through Number. */
|
|
36
55
|
function formatScaledBigInt(value, scale) {
|
|
37
56
|
const negative = value < 0n;
|
|
@@ -56,10 +75,16 @@ function decodeExactNumeric(value, scale, mode) {
|
|
|
56
75
|
}
|
|
57
76
|
/** Decode INT128 using the mixed number/string policy of lossy mode. */
|
|
58
77
|
function decodeLossyInt128(value, scale) {
|
|
59
|
-
|
|
78
|
+
// Both bounds matter, as in decodeExactNumeric. While this path read the
|
|
79
|
+
// coefficient unsigned, every negative arrived as a huge positive and so
|
|
80
|
+
// always took the exact-string branch, which masked the missing lower
|
|
81
|
+
// bound. Now that the reader is signed, a large negative would otherwise
|
|
82
|
+
// fall through to Number() and lose precision while its positive twin
|
|
83
|
+
// stayed exact.
|
|
84
|
+
if (value > MAX_SAFE_BIGINT || value < MIN_SAFE_BIGINT) {
|
|
60
85
|
return formatScaledBigInt(value, scale);
|
|
61
86
|
}
|
|
62
|
-
return Number(value)
|
|
87
|
+
return applyScale(Number(value), scale);
|
|
63
88
|
}
|
|
64
89
|
/**
|
|
65
90
|
* Maps Firebird character-set names (upper-case) to the Node.js Buffer
|
|
@@ -487,9 +512,7 @@ exports.SQLVarArray = SQLVarArray;
|
|
|
487
512
|
class SQLVarInt extends SQLVarBase {
|
|
488
513
|
decode(data, lowerV13) {
|
|
489
514
|
var ret = data.readInt();
|
|
490
|
-
|
|
491
|
-
ret = ret / ScaleDivisor[Math.abs(this.scale)];
|
|
492
|
-
}
|
|
515
|
+
ret = applyScale(ret, this.scale);
|
|
493
516
|
if (!lowerV13 || !data.readInt()) {
|
|
494
517
|
return ret;
|
|
495
518
|
}
|
|
@@ -516,8 +539,7 @@ class SQLVarInt64 extends SQLVarBase {
|
|
|
516
539
|
let ret;
|
|
517
540
|
if (mode === const_1.default.NUMERIC_MODE_LOSSY) {
|
|
518
541
|
ret = data.readInt64();
|
|
519
|
-
|
|
520
|
-
ret = ret / ScaleDivisor[Math.abs(this.scale)];
|
|
542
|
+
ret = applyScale(ret, this.scale);
|
|
521
543
|
}
|
|
522
544
|
else {
|
|
523
545
|
ret = decodeExactNumeric(data.readInt64BigInt(), this.scale, mode);
|
|
@@ -538,7 +560,7 @@ class SQLVarInt128 extends SQLVarBase {
|
|
|
538
560
|
decode(data, lowerV13, options) {
|
|
539
561
|
const mode = options?.numericMode || const_1.default.NUMERIC_MODE_LOSSY;
|
|
540
562
|
const ret = mode === const_1.default.NUMERIC_MODE_LOSSY
|
|
541
|
-
? decodeLossyInt128(data.
|
|
563
|
+
? decodeLossyInt128(data.readInt128Signed(), this.scale)
|
|
542
564
|
: decodeExactNumeric(data.readInt128Signed(), this.scale, mode);
|
|
543
565
|
if (!lowerV13 || !data.readInt()) {
|
|
544
566
|
return ret;
|
|
@@ -813,6 +835,78 @@ class SQLParamInt128 {
|
|
|
813
835
|
}
|
|
814
836
|
exports.SQLParamInt128 = SQLParamInt128;
|
|
815
837
|
//------------------------------------------------------
|
|
838
|
+
const FIXED_POINT_RE = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/;
|
|
839
|
+
/**
|
|
840
|
+
* Convert a decimal input to the signed integer coefficient used by a
|
|
841
|
+
* Firebird fixed-point wire type. Numbers are interpreted through their
|
|
842
|
+
* canonical decimal string; strings and bigints never pass through Number.
|
|
843
|
+
* Digits discarded by the target scale are rounded to nearest, ties away
|
|
844
|
+
* from zero, matching Firebird's conversion of decimal parameter text.
|
|
845
|
+
*/
|
|
846
|
+
function toScaledInteger(value, scale, bits) {
|
|
847
|
+
if (!Number.isSafeInteger(scale)) {
|
|
848
|
+
throw new TypeError('Fixed-point scale must be an integer');
|
|
849
|
+
}
|
|
850
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
851
|
+
throw new TypeError('Fixed-point value must be finite');
|
|
852
|
+
}
|
|
853
|
+
if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'bigint') {
|
|
854
|
+
throw new TypeError('Fixed-point value must be a number, string, or bigint');
|
|
855
|
+
}
|
|
856
|
+
const text = String(value).trim();
|
|
857
|
+
const match = FIXED_POINT_RE.exec(text);
|
|
858
|
+
if (!match) {
|
|
859
|
+
throw new TypeError('Invalid fixed-point value: ' + text);
|
|
860
|
+
}
|
|
861
|
+
const negative = match[1] === '-';
|
|
862
|
+
const integer = match[2] || '0';
|
|
863
|
+
const fraction = match[3] !== undefined ? match[3] : (match[4] || '');
|
|
864
|
+
const exponent = match[5] === undefined ? 0 : Number(match[5]);
|
|
865
|
+
if (!Number.isSafeInteger(exponent)) {
|
|
866
|
+
throw new RangeError('Fixed-point exponent is outside the supported range: ' + match[5]);
|
|
867
|
+
}
|
|
868
|
+
let digits = (integer + fraction).replace(/^0+/, '') || '0';
|
|
869
|
+
if (digits === '0')
|
|
870
|
+
return 0n;
|
|
871
|
+
const shift = exponent - fraction.length - scale;
|
|
872
|
+
let coefficientDigits;
|
|
873
|
+
let roundUp = false;
|
|
874
|
+
if (shift >= 0) {
|
|
875
|
+
// Every supported destination is at most 39 decimal digits. Avoid
|
|
876
|
+
// constructing an arbitrarily large BigInt for inputs such as 1e999999.
|
|
877
|
+
if (digits.length + shift > 40) {
|
|
878
|
+
throw new RangeError('Fixed-point value is outside the signed ' + bits + '-bit range: ' + text);
|
|
879
|
+
}
|
|
880
|
+
coefficientDigits = digits + '0'.repeat(shift);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
const discarded = -shift;
|
|
884
|
+
if (discarded < digits.length) {
|
|
885
|
+
const split = digits.length - discarded;
|
|
886
|
+
coefficientDigits = digits.slice(0, split);
|
|
887
|
+
roundUp = digits.charCodeAt(split) >= 0x35;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
coefficientDigits = '0';
|
|
891
|
+
// If discarded exceeds the number of significant digits, the
|
|
892
|
+
// magnitude is below 0.1 coefficient and cannot round to one.
|
|
893
|
+
roundUp = discarded === digits.length && digits.charCodeAt(0) >= 0x35;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
let coefficient = BigInt(coefficientDigits);
|
|
897
|
+
if (roundUp)
|
|
898
|
+
coefficient += 1n;
|
|
899
|
+
if (negative)
|
|
900
|
+
coefficient = -coefficient;
|
|
901
|
+
const width = BigInt(bits);
|
|
902
|
+
const min = -(1n << (width - 1n));
|
|
903
|
+
const max = (1n << (width - 1n)) - 1n;
|
|
904
|
+
if (coefficient < min || coefficient > max) {
|
|
905
|
+
throw new RangeError('Fixed-point value is outside the signed ' + bits + '-bit range: ' + text);
|
|
906
|
+
}
|
|
907
|
+
return coefficient;
|
|
908
|
+
}
|
|
909
|
+
//------------------------------------------------------
|
|
816
910
|
class SQLParamDecFloat16 {
|
|
817
911
|
constructor(value) {
|
|
818
912
|
this.value = value;
|
package/package.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -493,6 +493,12 @@ export interface Options {
|
|
|
493
493
|
* probe is sent (default 60000). Ignored when enableKeepAlive is false.
|
|
494
494
|
*/
|
|
495
495
|
keepAliveInitialDelay?: number;
|
|
496
|
+
/**
|
|
497
|
+
* Force the TCP socket to IPv4 (4) or IPv6 (6) when resolving `host`.
|
|
498
|
+
* Unset lets the resolver choose. Set automatically by `inet4://` /
|
|
499
|
+
* `inet6://` connection URIs.
|
|
500
|
+
*/
|
|
501
|
+
ipFamily?: 4 | 6;
|
|
496
502
|
pluginName?: string;
|
|
497
503
|
parallelWorkers?: number;
|
|
498
504
|
maxInlineBlobSize?: number;
|
package/src/uri.ts
CHANGED
|
@@ -20,7 +20,7 @@ const NUMBER_KEYS = new Set([
|
|
|
20
20
|
'port', 'pageSize', 'timeout', 'retryConnectionInterval',
|
|
21
21
|
'blobChunkSize', 'blobReadChunkSize', 'wireCrypt', 'parallelWorkers',
|
|
22
22
|
'maxInlineBlobSize', 'maxNegotiatedProtocols', 'connectTimeout',
|
|
23
|
-
'min', 'idleTimeoutMillis', 'keepAliveInitialDelay',
|
|
23
|
+
'min', 'idleTimeoutMillis', 'keepAliveInitialDelay', 'ipFamily',
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
26
|
function coerce(key: string, value: string): any {
|
|
@@ -38,9 +38,31 @@ function coerce(key: string, value: string): any {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* URI schemes accepted by parseConnectionUri. `firebird://` is this driver's
|
|
42
|
+
* own scheme; `inet://`, `inet4://` and `inet6://` are Firebird's native
|
|
43
|
+
* URL-style connection strings (Firebird 3+), so a string that works with
|
|
44
|
+
* isql works here too. inet4/inet6 pin the socket's IP family (the
|
|
45
|
+
* `ipFamily` option); inet and firebird let the resolver pick.
|
|
46
|
+
*/
|
|
47
|
+
const URI_SCHEMES: Record<string, 4 | 6 | undefined> = {
|
|
48
|
+
'firebird:': undefined,
|
|
49
|
+
'inet:': undefined,
|
|
50
|
+
'inet4:': 4,
|
|
51
|
+
'inet6:': 6,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Firebird protocols this driver cannot speak: they are local-machine IPC
|
|
56
|
+
* transports (shared memory / named pipes), not TCP.
|
|
57
|
+
*/
|
|
58
|
+
const LOCAL_SCHEMES = /^(xnet|wnet):$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a firebird:// or inet:// connection URI into an options object.
|
|
42
62
|
*
|
|
43
63
|
* firebird://user:password@host:port/database?option=value&...
|
|
64
|
+
* inet://host:port/database (Firebird's own URL style;
|
|
65
|
+
* inet4://... / inet6://... also force IPv4 / IPv6)
|
|
44
66
|
*
|
|
45
67
|
* The database part:
|
|
46
68
|
* firebird://host/employee → alias "employee"
|
|
@@ -64,13 +86,23 @@ export function parseConnectionUri(uri: string): Options {
|
|
|
64
86
|
throw new Error('Invalid connection URI: ' + uri);
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
var scheme = url.protocol.toLowerCase();
|
|
90
|
+
if (LOCAL_SCHEMES.test(scheme)) {
|
|
91
|
+
throw new Error('Unsupported connection URI scheme "' + scheme.replace(/:$/, '') +
|
|
92
|
+
'" (local IPC transports are not available over the wire — use inet:// or firebird://)');
|
|
93
|
+
}
|
|
94
|
+
if (!Object.prototype.hasOwnProperty.call(URI_SCHEMES, scheme)) {
|
|
95
|
+
throw new Error('Unsupported connection URI scheme "' + scheme.replace(/:$/, '') +
|
|
96
|
+
'" (expected firebird://, inet://, inet4:// or inet6://)');
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
var options: any = {};
|
|
73
100
|
|
|
101
|
+
var ipFamily = URI_SCHEMES[scheme];
|
|
102
|
+
if (ipFamily !== undefined) {
|
|
103
|
+
options.ipFamily = ipFamily;
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
if (url.hostname) {
|
|
75
107
|
// URL keeps IPv6 hostnames bracketed ([::1]); net.connect wants them bare
|
|
76
108
|
options.host = url.hostname.replace(/^\[(.*)\]$/, '$1');
|
|
@@ -182,8 +214,9 @@ export function parseOldStyleConnectionString(str: string): Options {
|
|
|
182
214
|
const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
183
215
|
|
|
184
216
|
/**
|
|
185
|
-
* Parse any connection string the driver accepts: a firebird://
|
|
186
|
-
* traditional [host[/port]:]database string when
|
|
217
|
+
* Parse any connection string the driver accepts: a firebird:// or
|
|
218
|
+
* inet[4|6]:// URI, or a traditional [host[/port]:]database string when
|
|
219
|
+
* there is no scheme.
|
|
187
220
|
*/
|
|
188
221
|
export function parseConnectionString(str: string): Options {
|
|
189
222
|
return URI_SCHEME.test(str)
|
|
@@ -192,8 +225,9 @@ export function parseConnectionString(str: string): Options {
|
|
|
192
225
|
}
|
|
193
226
|
|
|
194
227
|
/**
|
|
195
|
-
* Accept either an options object or a connection string (firebird://
|
|
196
|
-
* or traditional host[/port]:database) everywhere options are
|
|
228
|
+
* Accept either an options object or a connection string (firebird:// or
|
|
229
|
+
* inet:// URI, or traditional host[/port]:database) everywhere options are
|
|
230
|
+
* taken.
|
|
197
231
|
* Strings are parsed; objects pass through unchanged.
|
|
198
232
|
*/
|
|
199
233
|
export function normalizeOptions<T>(options: T | string): T {
|
package/src/wire/connection.ts
CHANGED
|
@@ -186,7 +186,8 @@ class Connection {
|
|
|
186
186
|
this._detachAuto;
|
|
187
187
|
this._socket = new Socket(port, host,
|
|
188
188
|
options.enableKeepAlive !== false,
|
|
189
|
-
options.keepAliveInitialDelay
|
|
189
|
+
options.keepAliveInitialDelay,
|
|
190
|
+
options.ipFamily);
|
|
190
191
|
this._pending = [];
|
|
191
192
|
this._isOpened = false;
|
|
192
193
|
this._isClosed = false;
|
|
@@ -1453,6 +1454,17 @@ class Connection {
|
|
|
1453
1454
|
}
|
|
1454
1455
|
}
|
|
1455
1456
|
|
|
1457
|
+
// Validate fixed-point values before the BLOB pre-pass. Blob uploads
|
|
1458
|
+
// write immediately, so deferring numeric validation until message
|
|
1459
|
+
// encoding could leave transaction blob state behind for a batch that
|
|
1460
|
+
// can never be sent.
|
|
1461
|
+
try {
|
|
1462
|
+
validateBatchFixedPointRows(input, rows as any[][]);
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
doError(err, callback);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1456
1468
|
var self = this;
|
|
1457
1469
|
|
|
1458
1470
|
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
@@ -3537,6 +3549,39 @@ function scaleOutputLengths(output: any[], options: any) {
|
|
|
3537
3549
|
}
|
|
3538
3550
|
}
|
|
3539
3551
|
|
|
3552
|
+
function scaleBatchFixedPoint(value: any, meta: any, bits: 16 | 32 | 64 | 128, column: number): bigint {
|
|
3553
|
+
try {
|
|
3554
|
+
return Xsql.toScaledInteger(value, meta.scale, bits);
|
|
3555
|
+
} catch (err) {
|
|
3556
|
+
var message = err instanceof Error ? err.message : String(err);
|
|
3557
|
+
throw new Error('Invalid fixed-point batch value for column ' + column +
|
|
3558
|
+
' (' + (meta.field || '?') + '): ' + message);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
/** Validate values whose batch wire representation is metadata-directed.
|
|
3563
|
+
* This runs before BLOB uploads, which may write to the transaction as soon
|
|
3564
|
+
* as executeBatch starts its asynchronous pre-pass. */
|
|
3565
|
+
function validateBatchFixedPointRows(input: any[], rows: any[][]): void {
|
|
3566
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3567
|
+
for (var j = 0; j < input.length; j++) {
|
|
3568
|
+
var value = rows[i][j];
|
|
3569
|
+
if (value === null || value === undefined) continue;
|
|
3570
|
+
|
|
3571
|
+
var bits: 16 | 32 | 64 | 128 | undefined = undefined;
|
|
3572
|
+
switch (input[j].type) {
|
|
3573
|
+
case Const.SQL_SHORT: bits = 16; break;
|
|
3574
|
+
case Const.SQL_LONG: bits = 32; break;
|
|
3575
|
+
case Const.SQL_INT64: bits = 64; break;
|
|
3576
|
+
case Const.SQL_INT128: bits = 128; break;
|
|
3577
|
+
}
|
|
3578
|
+
if (bits !== undefined) {
|
|
3579
|
+
scaleBatchFixedPoint(value, input[j], bits, j + 1);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3540
3585
|
/**
|
|
3541
3586
|
* Batch support: the engine requires every batch message to use EXACTLY the
|
|
3542
3587
|
* statement's described input format (unlike op_execute, where the client
|
|
@@ -3572,17 +3617,6 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3572
3617
|
if (typeof v === 'string') return parseDate(v);
|
|
3573
3618
|
return new Date(v);
|
|
3574
3619
|
};
|
|
3575
|
-
var scaled = function(v: any, scale: number): number {
|
|
3576
|
-
var n = typeof v === 'string' ? parseFloat(v) : Number(v);
|
|
3577
|
-
return scale ? Math.round(n * Math.pow(10, -scale)) : n;
|
|
3578
|
-
};
|
|
3579
|
-
var scaledBig = function(v: any, scale: number): bigint {
|
|
3580
|
-
if (typeof v === 'bigint') {
|
|
3581
|
-
return scale ? v * (10n ** BigInt(-scale)) : v;
|
|
3582
|
-
}
|
|
3583
|
-
return BigInt(scaled(v, scale));
|
|
3584
|
-
};
|
|
3585
|
-
|
|
3586
3620
|
for (var j = 0; j < input.length; j++) {
|
|
3587
3621
|
var meta = input[j];
|
|
3588
3622
|
var column = j + 1;
|
|
@@ -3615,32 +3649,32 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3615
3649
|
|
|
3616
3650
|
case Const.SQL_SHORT:
|
|
3617
3651
|
// 2 bytes in the message struct (msglen), 4 on the XDR wire
|
|
3618
|
-
encoders.push((function(m) {
|
|
3619
|
-
return function(msg: any, v: any) { msg.addInt(
|
|
3620
|
-
})(meta));
|
|
3652
|
+
encoders.push((function(m, col) {
|
|
3653
|
+
return function(msg: any, v: any) { msg.addInt(Number(scaleBatchFixedPoint(v, m, 16, col))); };
|
|
3654
|
+
})(meta, column));
|
|
3621
3655
|
align(2); offset += 2;
|
|
3622
3656
|
break;
|
|
3623
3657
|
|
|
3624
3658
|
case Const.SQL_LONG:
|
|
3625
|
-
encoders.push((function(m) {
|
|
3626
|
-
return function(msg: any, v: any) { msg.addInt(
|
|
3627
|
-
})(meta));
|
|
3659
|
+
encoders.push((function(m, col) {
|
|
3660
|
+
return function(msg: any, v: any) { msg.addInt(Number(scaleBatchFixedPoint(v, m, 32, col))); };
|
|
3661
|
+
})(meta, column));
|
|
3628
3662
|
align(4); offset += 4;
|
|
3629
3663
|
break;
|
|
3630
3664
|
|
|
3631
3665
|
case Const.SQL_INT64:
|
|
3632
|
-
encoders.push((function(m) {
|
|
3666
|
+
encoders.push((function(m, col) {
|
|
3633
3667
|
return function(msg: any, v: any) {
|
|
3634
|
-
msg.addInt64(
|
|
3668
|
+
msg.addInt64(scaleBatchFixedPoint(v, m, 64, col));
|
|
3635
3669
|
};
|
|
3636
|
-
})(meta));
|
|
3670
|
+
})(meta, column));
|
|
3637
3671
|
align(8); offset += 8;
|
|
3638
3672
|
break;
|
|
3639
3673
|
|
|
3640
3674
|
case Const.SQL_INT128:
|
|
3641
|
-
encoders.push((function(m) {
|
|
3642
|
-
return function(msg: any, v: any) { msg.addInt128(
|
|
3643
|
-
})(meta));
|
|
3675
|
+
encoders.push((function(m, col) {
|
|
3676
|
+
return function(msg: any, v: any) { msg.addInt128(scaleBatchFixedPoint(v, m, 128, col)); };
|
|
3677
|
+
})(meta, column));
|
|
3644
3678
|
align(8); offset += 16;
|
|
3645
3679
|
break;
|
|
3646
3680
|
|
package/src/wire/serialize.ts
CHANGED
|
@@ -303,7 +303,7 @@ export class XdrWriter {
|
|
|
303
303
|
const high = bigValue >> BigInt(64);
|
|
304
304
|
const low = bigValue & BigInt("0xFFFFFFFFFFFFFFFF");
|
|
305
305
|
|
|
306
|
-
this.buffer.
|
|
306
|
+
this.buffer.writeBigInt64BE(high, this.pos);
|
|
307
307
|
this.pos += 8;
|
|
308
308
|
this.buffer.writeBigUInt64BE(low, this.pos);
|
|
309
309
|
this.pos += 8;
|
package/src/wire/socket.ts
CHANGED
|
@@ -109,8 +109,9 @@ class Socket {
|
|
|
109
109
|
encryptCipher: any;
|
|
110
110
|
decryptCipher: any;
|
|
111
111
|
|
|
112
|
-
constructor(port: number, host: string, enableKeepAlive = true, keepAliveInitialDelay = 60000) {
|
|
113
|
-
|
|
112
|
+
constructor(port: number, host: string, enableKeepAlive = true, keepAliveInitialDelay = 60000, ipFamily?: 4 | 6) {
|
|
113
|
+
// family: 0 is "either" (Node's default); 4/6 pin the resolver
|
|
114
|
+
this._socket = net.createConnection({ port: port, host: host, family: ipFamily || 0 });
|
|
114
115
|
this._socket.setNoDelay(true);
|
|
115
116
|
// TCP keepalive probing detects dead/stale connections; the delay is
|
|
116
117
|
// how long the socket must be idle before the first probe.
|
package/src/wire/xsqlvar.ts
CHANGED
|
@@ -11,8 +11,6 @@ import type { NumericMode, RecordCounts } from '../types';
|
|
|
11
11
|
*
|
|
12
12
|
***************************************/
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
ScaleDivisor = [1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000,10000000000, 100000000000,1000000000000,10000000000000,100000000000000,1000000000000000];
|
|
16
14
|
const
|
|
17
15
|
DateOffset = 40587,
|
|
18
16
|
TimeCoeff = 86400000,
|
|
@@ -26,6 +24,25 @@ type NumericDecodeOptions = {
|
|
|
26
24
|
numericMode?: NumericMode;
|
|
27
25
|
};
|
|
28
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Apply a Firebird numeric scale to a value already narrowed to a JS number.
|
|
29
|
+
*
|
|
30
|
+
* This replaces the former lookup table of divisors, which only held
|
|
31
|
+
* 10^0..10^15: any larger scale indexed past its end and yielded NaN. That is
|
|
32
|
+
* reachable for INT64 (NUMERIC(18,18)) and routine for INT128, where the scale
|
|
33
|
+
* runs to 38. Math.pow(10, n) returns the identical double for every exponent
|
|
34
|
+
* the table did cover, so in-range results are unchanged.
|
|
35
|
+
*
|
|
36
|
+
* Positive scales multiply, matching decodeExactNumeric and formatScaledBigInt;
|
|
37
|
+
* the table path divided by them, which was the wrong direction.
|
|
38
|
+
*/
|
|
39
|
+
function applyScale(value: number, scale: number): number {
|
|
40
|
+
if (!scale) return value;
|
|
41
|
+
return scale < 0
|
|
42
|
+
? value / Math.pow(10, -scale)
|
|
43
|
+
: value * Math.pow(10, scale);
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/** Format a signed Firebird integer coefficient without passing through Number. */
|
|
30
47
|
function formatScaledBigInt(value: bigint, scale: number): string {
|
|
31
48
|
const negative = value < 0n;
|
|
@@ -51,11 +68,17 @@ function decodeExactNumeric(value: bigint, scale: number, mode: 'safe' | 'string
|
|
|
51
68
|
|
|
52
69
|
/** Decode INT128 using the mixed number/string policy of lossy mode. */
|
|
53
70
|
function decodeLossyInt128(value: bigint, scale: number): number | string {
|
|
54
|
-
|
|
71
|
+
// Both bounds matter, as in decodeExactNumeric. While this path read the
|
|
72
|
+
// coefficient unsigned, every negative arrived as a huge positive and so
|
|
73
|
+
// always took the exact-string branch, which masked the missing lower
|
|
74
|
+
// bound. Now that the reader is signed, a large negative would otherwise
|
|
75
|
+
// fall through to Number() and lose precision while its positive twin
|
|
76
|
+
// stayed exact.
|
|
77
|
+
if (value > MAX_SAFE_BIGINT || value < MIN_SAFE_BIGINT) {
|
|
55
78
|
return formatScaledBigInt(value, scale);
|
|
56
79
|
}
|
|
57
80
|
|
|
58
|
-
return Number(value)
|
|
81
|
+
return applyScale(Number(value), scale);
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
/**
|
|
@@ -564,9 +587,7 @@ export class SQLVarInt extends SQLVarBase {
|
|
|
564
587
|
decode(data: XdrReader, lowerV13: boolean) {
|
|
565
588
|
var ret = data.readInt();
|
|
566
589
|
|
|
567
|
-
|
|
568
|
-
ret = ret / ScaleDivisor[Math.abs(this.scale)];
|
|
569
|
-
}
|
|
590
|
+
ret = applyScale(ret, this.scale);
|
|
570
591
|
|
|
571
592
|
if (!lowerV13 || !data.readInt()) {
|
|
572
593
|
return ret;
|
|
@@ -599,7 +620,7 @@ export class SQLVarInt64 extends SQLVarBase {
|
|
|
599
620
|
|
|
600
621
|
if (mode === Const.NUMERIC_MODE_LOSSY) {
|
|
601
622
|
ret = data.readInt64();
|
|
602
|
-
|
|
623
|
+
ret = applyScale(ret, this.scale);
|
|
603
624
|
} else {
|
|
604
625
|
ret = decodeExactNumeric(data.readInt64BigInt(), this.scale, mode);
|
|
605
626
|
}
|
|
@@ -622,7 +643,7 @@ export class SQLVarInt128 extends SQLVarBase {
|
|
|
622
643
|
decode(data: XdrReader, lowerV13: boolean, options?: NumericDecodeOptions) {
|
|
623
644
|
const mode = options?.numericMode || Const.NUMERIC_MODE_LOSSY;
|
|
624
645
|
const ret = mode === Const.NUMERIC_MODE_LOSSY
|
|
625
|
-
? decodeLossyInt128(data.
|
|
646
|
+
? decodeLossyInt128(data.readInt128Signed(), this.scale)
|
|
626
647
|
: decodeExactNumeric(data.readInt128Signed(), this.scale, mode);
|
|
627
648
|
|
|
628
649
|
if (!lowerV13 || !data.readInt()) {
|
|
@@ -957,6 +978,83 @@ export class SQLParamInt128 {
|
|
|
957
978
|
|
|
958
979
|
//------------------------------------------------------
|
|
959
980
|
|
|
981
|
+
const FIXED_POINT_RE = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/;
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Convert a decimal input to the signed integer coefficient used by a
|
|
985
|
+
* Firebird fixed-point wire type. Numbers are interpreted through their
|
|
986
|
+
* canonical decimal string; strings and bigints never pass through Number.
|
|
987
|
+
* Digits discarded by the target scale are rounded to nearest, ties away
|
|
988
|
+
* from zero, matching Firebird's conversion of decimal parameter text.
|
|
989
|
+
*/
|
|
990
|
+
export function toScaledInteger(value: number | string | bigint, scale: number, bits: 16 | 32 | 64 | 128): bigint {
|
|
991
|
+
if (!Number.isSafeInteger(scale)) {
|
|
992
|
+
throw new TypeError('Fixed-point scale must be an integer');
|
|
993
|
+
}
|
|
994
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
995
|
+
throw new TypeError('Fixed-point value must be finite');
|
|
996
|
+
}
|
|
997
|
+
if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'bigint') {
|
|
998
|
+
throw new TypeError('Fixed-point value must be a number, string, or bigint');
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const text = String(value).trim();
|
|
1002
|
+
const match = FIXED_POINT_RE.exec(text);
|
|
1003
|
+
if (!match) {
|
|
1004
|
+
throw new TypeError('Invalid fixed-point value: ' + text);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const negative = match[1] === '-';
|
|
1008
|
+
const integer = match[2] || '0';
|
|
1009
|
+
const fraction = match[3] !== undefined ? match[3] : (match[4] || '');
|
|
1010
|
+
const exponent = match[5] === undefined ? 0 : Number(match[5]);
|
|
1011
|
+
if (!Number.isSafeInteger(exponent)) {
|
|
1012
|
+
throw new RangeError('Fixed-point exponent is outside the supported range: ' + match[5]);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
let digits = (integer + fraction).replace(/^0+/, '') || '0';
|
|
1016
|
+
if (digits === '0') return 0n;
|
|
1017
|
+
|
|
1018
|
+
const shift = exponent - fraction.length - scale;
|
|
1019
|
+
let coefficientDigits: string;
|
|
1020
|
+
let roundUp = false;
|
|
1021
|
+
|
|
1022
|
+
if (shift >= 0) {
|
|
1023
|
+
// Every supported destination is at most 39 decimal digits. Avoid
|
|
1024
|
+
// constructing an arbitrarily large BigInt for inputs such as 1e999999.
|
|
1025
|
+
if (digits.length + shift > 40) {
|
|
1026
|
+
throw new RangeError('Fixed-point value is outside the signed ' + bits + '-bit range: ' + text);
|
|
1027
|
+
}
|
|
1028
|
+
coefficientDigits = digits + '0'.repeat(shift);
|
|
1029
|
+
} else {
|
|
1030
|
+
const discarded = -shift;
|
|
1031
|
+
if (discarded < digits.length) {
|
|
1032
|
+
const split = digits.length - discarded;
|
|
1033
|
+
coefficientDigits = digits.slice(0, split);
|
|
1034
|
+
roundUp = digits.charCodeAt(split) >= 0x35;
|
|
1035
|
+
} else {
|
|
1036
|
+
coefficientDigits = '0';
|
|
1037
|
+
// If discarded exceeds the number of significant digits, the
|
|
1038
|
+
// magnitude is below 0.1 coefficient and cannot round to one.
|
|
1039
|
+
roundUp = discarded === digits.length && digits.charCodeAt(0) >= 0x35;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
let coefficient = BigInt(coefficientDigits);
|
|
1044
|
+
if (roundUp) coefficient += 1n;
|
|
1045
|
+
if (negative) coefficient = -coefficient;
|
|
1046
|
+
|
|
1047
|
+
const width = BigInt(bits);
|
|
1048
|
+
const min = -(1n << (width - 1n));
|
|
1049
|
+
const max = (1n << (width - 1n)) - 1n;
|
|
1050
|
+
if (coefficient < min || coefficient > max) {
|
|
1051
|
+
throw new RangeError('Fixed-point value is outside the signed ' + bits + '-bit range: ' + text);
|
|
1052
|
+
}
|
|
1053
|
+
return coefficient;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
//------------------------------------------------------
|
|
1057
|
+
|
|
960
1058
|
export class SQLParamDecFloat16 {
|
|
961
1059
|
value: any;
|
|
962
1060
|
|