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 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`; INT128 uses a mixed `number`/`string` path. Unsafe coefficients may lose precision. |
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`. INT128 uses a
756
- mixed number/string decoding path. For coefficients outside JavaScript's safe
757
- integer range, the result type can depend on the Firebird wire type and value,
758
- and numeric precision is not guaranteed. `LOSSY` remains the default so that
759
- adding `numericMode` does not silently change result types for applications
760
- upgrading from earlier node-firebird releases.
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, so
1897
- NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
1898
- `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
1899
- exactly.
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:// URI, or a
49
- * traditional [host[/port]:]database string when there is no scheme.
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:// URI
54
- * or traditional host[/port]:database) everywhere options are taken.
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
- * Parse a firebird:// connection URI into an options object.
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
- if (url.protocol !== 'firebird:') {
68
- throw new Error('Unsupported connection URI scheme "' + url.protocol.replace(/:$/, '') +
69
- '" (expected firebird://...)');
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:// URI, or a
176
- * traditional [host[/port]:]database string when there is no scheme.
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:// URI
185
- * or traditional host[/port]:database) everywhere options are taken.
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) {
@@ -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(scaled(v, m.scale)); };
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(scaled(v, m.scale)); };
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(typeof v === 'bigint' ? scaledBig(v, m.scale) : scaled(v, m.scale));
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(scaledBig(v, m.scale)); };
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;
@@ -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.writeBigUInt64BE(high, this.pos);
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;
@@ -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.
@@ -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
- this._socket = net_1.default.createConnection(port, host);
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.
@@ -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);
@@ -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
- if (value > MAX_SAFE_BIGINT) {
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) / ScaleDivisor[Math.abs(scale)];
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
- if (this.scale) {
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
- if (this.scale)
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.readInt128(), this.scale)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.15.1",
3
+ "version": "2.16.1",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
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
- * Parse a firebird:// connection URI into an options object.
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
- if (url.protocol !== 'firebird:') {
68
- throw new Error('Unsupported connection URI scheme "' + url.protocol.replace(/:$/, '') +
69
- '" (expected firebird://...)');
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:// URI, or a
186
- * traditional [host[/port]:]database string when there is no scheme.
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:// URI
196
- * or traditional host[/port]:database) everywhere options are taken.
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 {
@@ -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(scaled(v, m.scale)); };
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(scaled(v, m.scale)); };
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(typeof v === 'bigint' ? (scaledBig(v, m.scale) as any) : scaled(v, m.scale));
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(scaledBig(v, m.scale)); };
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
 
@@ -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.writeBigUInt64BE(high, this.pos);
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;
@@ -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
- this._socket = net.createConnection(port, host);
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.
@@ -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
- if (value > MAX_SAFE_BIGINT) {
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) / ScaleDivisor[Math.abs(scale)];
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
- if (this.scale) {
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
- if (this.scale) ret = ret / ScaleDivisor[Math.abs(this.scale)];
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.readInt128(), this.scale)
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