node-firebird 2.16.0 → 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 +34 -11
- package/lib/wire/connection.js +63 -22
- package/lib/wire/serialize.js +1 -1
- package/lib/wire/xsqlvar.d.ts +8 -0
- package/lib/wire/xsqlvar.js +103 -9
- package/package.json +1 -1
- package/src/wire/connection.ts +56 -23
- package/src/wire/serialize.ts +1 -1
- package/src/wire/xsqlvar.ts +107 -9
package/README.md
CHANGED
|
@@ -763,16 +763,29 @@ option `numericMode` controls how those result values are exposed:
|
|
|
763
763
|
|
|
764
764
|
| Mode | Result policy |
|
|
765
765
|
| :--- | :--- |
|
|
766
|
-
| `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. |
|
|
767
767
|
| `Firebird.NUMERIC_MODE_SAFE` | Safe coefficients are returned as `number`; unsafe coefficients as exact scaled `string`. |
|
|
768
768
|
| `Firebird.NUMERIC_MODE_STRING` | All values are returned as exact scaled `string`. |
|
|
769
769
|
|
|
770
|
-
`LOSSY` decodes INT64-backed values through JavaScript `Number
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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.
|
|
776
789
|
|
|
777
790
|
`SAFE` tests the raw integer coefficient against JavaScript's inclusive safe
|
|
778
791
|
range (`Number.MIN_SAFE_INTEGER` through `Number.MAX_SAFE_INTEGER`) before
|
|
@@ -789,6 +802,12 @@ const db = await Firebird.attachAsync({
|
|
|
789
802
|
// DECIMAL coefficient 420000,-4 -> '42.0000'
|
|
790
803
|
```
|
|
791
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
|
+
|
|
792
811
|
The string literals `'lossy'`, `'safe'`, and `'string'` are accepted too,
|
|
793
812
|
including in connection URIs (`?numericMode=safe`). `NULL` remains `null` in
|
|
794
813
|
every mode. The option does not change `FLOAT`, `DOUBLE`, `DECFLOAT`, or input
|
|
@@ -1908,10 +1927,14 @@ in bytes).
|
|
|
1908
1927
|
Notes:
|
|
1909
1928
|
|
|
1910
1929
|
- Requires wire protocol 16+ (Firebird 4.0 or newer server).
|
|
1911
|
-
- Values are encoded from the statement's own parameter metadata
|
|
1912
|
-
NUMERIC
|
|
1913
|
-
|
|
1914
|
-
|
|
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.
|
|
1915
1938
|
- `BLOB` columns accept Buffers, strings, JSON-able objects, or
|
|
1916
1939
|
pre-created blob quad ids: values are uploaded as transaction blobs
|
|
1917
1940
|
first — all initiated back-to-back so the blob ops pipeline on the
|
package/lib/wire/connection.js
CHANGED
|
@@ -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/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/wire/connection.ts
CHANGED
|
@@ -1454,6 +1454,17 @@ class Connection {
|
|
|
1454
1454
|
}
|
|
1455
1455
|
}
|
|
1456
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
|
+
|
|
1457
1468
|
var self = this;
|
|
1458
1469
|
|
|
1459
1470
|
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
@@ -3538,6 +3549,39 @@ function scaleOutputLengths(output: any[], options: any) {
|
|
|
3538
3549
|
}
|
|
3539
3550
|
}
|
|
3540
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
|
+
|
|
3541
3585
|
/**
|
|
3542
3586
|
* Batch support: the engine requires every batch message to use EXACTLY the
|
|
3543
3587
|
* statement's described input format (unlike op_execute, where the client
|
|
@@ -3573,17 +3617,6 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3573
3617
|
if (typeof v === 'string') return parseDate(v);
|
|
3574
3618
|
return new Date(v);
|
|
3575
3619
|
};
|
|
3576
|
-
var scaled = function(v: any, scale: number): number {
|
|
3577
|
-
var n = typeof v === 'string' ? parseFloat(v) : Number(v);
|
|
3578
|
-
return scale ? Math.round(n * Math.pow(10, -scale)) : n;
|
|
3579
|
-
};
|
|
3580
|
-
var scaledBig = function(v: any, scale: number): bigint {
|
|
3581
|
-
if (typeof v === 'bigint') {
|
|
3582
|
-
return scale ? v * (10n ** BigInt(-scale)) : v;
|
|
3583
|
-
}
|
|
3584
|
-
return BigInt(scaled(v, scale));
|
|
3585
|
-
};
|
|
3586
|
-
|
|
3587
3620
|
for (var j = 0; j < input.length; j++) {
|
|
3588
3621
|
var meta = input[j];
|
|
3589
3622
|
var column = j + 1;
|
|
@@ -3616,32 +3649,32 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3616
3649
|
|
|
3617
3650
|
case Const.SQL_SHORT:
|
|
3618
3651
|
// 2 bytes in the message struct (msglen), 4 on the XDR wire
|
|
3619
|
-
encoders.push((function(m) {
|
|
3620
|
-
return function(msg: any, v: any) { msg.addInt(
|
|
3621
|
-
})(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));
|
|
3622
3655
|
align(2); offset += 2;
|
|
3623
3656
|
break;
|
|
3624
3657
|
|
|
3625
3658
|
case Const.SQL_LONG:
|
|
3626
|
-
encoders.push((function(m) {
|
|
3627
|
-
return function(msg: any, v: any) { msg.addInt(
|
|
3628
|
-
})(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));
|
|
3629
3662
|
align(4); offset += 4;
|
|
3630
3663
|
break;
|
|
3631
3664
|
|
|
3632
3665
|
case Const.SQL_INT64:
|
|
3633
|
-
encoders.push((function(m) {
|
|
3666
|
+
encoders.push((function(m, col) {
|
|
3634
3667
|
return function(msg: any, v: any) {
|
|
3635
|
-
msg.addInt64(
|
|
3668
|
+
msg.addInt64(scaleBatchFixedPoint(v, m, 64, col));
|
|
3636
3669
|
};
|
|
3637
|
-
})(meta));
|
|
3670
|
+
})(meta, column));
|
|
3638
3671
|
align(8); offset += 8;
|
|
3639
3672
|
break;
|
|
3640
3673
|
|
|
3641
3674
|
case Const.SQL_INT128:
|
|
3642
|
-
encoders.push((function(m) {
|
|
3643
|
-
return function(msg: any, v: any) { msg.addInt128(
|
|
3644
|
-
})(meta));
|
|
3675
|
+
encoders.push((function(m, col) {
|
|
3676
|
+
return function(msg: any, v: any) { msg.addInt128(scaleBatchFixedPoint(v, m, 128, col)); };
|
|
3677
|
+
})(meta, column));
|
|
3645
3678
|
align(8); offset += 16;
|
|
3646
3679
|
break;
|
|
3647
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/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
|
|