node-firebird 2.14.4 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -210,6 +210,7 @@ options.jsonAsObject = false; // optional; automatically stringify parameters an
210
210
  options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
211
211
  options.nestTables = false; // true nests object rows by source table (row[table][column]); a string separator flattens to 'table<sep>column' keys — see Nested result tables (nestTables). Overridable per query
212
212
  options.transformKeys = undefined; // 'camel' (FIRST_NAME → firstName) or a (key) => key mapper for object-row keys — see Transforming row keys (transformKeys). Overridable per query
213
+ options.numericMode = Firebird.NUMERIC_MODE_LOSSY; // INT64/INT128 result policy: LOSSY (default), SAFE, or STRING
213
214
  options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
214
215
  options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)
215
216
  ```
@@ -738,6 +739,48 @@ on databases and transactions alike. It is ignored by the streaming APIs
738
739
  PROCEDURE`, `affectedRows` reflects DML the procedure performed — a
739
740
  procedure that only returns values reports 0 alongside its row.
740
741
 
742
+ ### Fixed-point numeric results (numericMode)
743
+
744
+ Firebird sends `BIGINT`, `INT128`, and the `NUMERIC`/`DECIMAL` types backed by
745
+ them as signed integer coefficients plus a decimal scale. JavaScript numbers
746
+ cannot represent every INT64 or INT128 coefficient exactly. The connection
747
+ option `numericMode` controls how those result values are exposed:
748
+
749
+ | Mode | Result policy |
750
+ | :--- | :--- |
751
+ | `Firebird.NUMERIC_MODE_LOSSY` | INT64-backed values are returned as `number`; INT128 uses a mixed `number`/`string` path. Unsafe coefficients may lose precision. |
752
+ | `Firebird.NUMERIC_MODE_SAFE` | Safe coefficients are returned as `number`; unsafe coefficients as exact scaled `string`. |
753
+ | `Firebird.NUMERIC_MODE_STRING` | All values are returned as exact scaled `string`. |
754
+
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.
761
+
762
+ `SAFE` tests the raw integer coefficient against JavaScript's inclusive safe
763
+ range (`Number.MIN_SAFE_INTEGER` through `Number.MAX_SAFE_INTEGER`) before
764
+ applying its scale. `STRING` provides a stable result type and retains zeroes
765
+ implied by the declared scale:
766
+
767
+ ```js
768
+ const db = await Firebird.attachAsync({
769
+ ...options,
770
+ numericMode: Firebird.NUMERIC_MODE_STRING,
771
+ });
772
+
773
+ // BIGINT 42 -> '42'
774
+ // DECIMAL coefficient 420000,-4 -> '42.0000'
775
+ ```
776
+
777
+ The string literals `'lossy'`, `'safe'`, and `'string'` are accepted too,
778
+ including in connection URIs (`?numericMode=safe`). `NULL` remains `null` in
779
+ every mode. The option does not change `FLOAT`, `DOUBLE`, `DECFLOAT`, or input
780
+ parameter encoding. A `SAFE` result returned as a number still has the normal
781
+ IEEE-754 behavior of JavaScript fractional numbers; use `STRING` when the
782
+ decimal representation itself must remain exact.
783
+
741
784
  ### Custom type parsers (typeCast)
742
785
 
743
786
  The `typeCast` connection option lets you override how column values are
@@ -758,10 +801,6 @@ Firebird.attach({
758
801
  const v = next();
759
802
  return v === null ? null : v.toISOString().slice(0, 10);
760
803
  }
761
- // BIGINT columns as strings
762
- if (column.type === Firebird.SQL_TYPES.SQL_INT64 && !column.scale) {
763
- return String(next());
764
- }
765
804
  return next(); // everything else: default decoding
766
805
  },
767
806
  }, (err, db) => { /* ... */ });
@@ -782,6 +821,9 @@ Firebird.attach({
782
821
 
783
822
  Notes:
784
823
 
824
+ - The hook runs after [`numericMode`](#fixed-point-numeric-results-numericmode).
825
+ Calling `String(next())` cannot recover digits already lost by lossy
826
+ numeric decoding; select `SAFE` or `STRING` when exact coefficients matter.
785
827
  - Non-text BLOB columns reach the hook as the usual asynchronous fetch
786
828
  function; text BLOBs with `blobAsText: true` reach it as the resolved
787
829
  string.
package/lib/index.d.ts CHANGED
@@ -13,6 +13,12 @@ export declare const AUTH_PLUGIN_SRP384: string;
13
13
  export declare const AUTH_PLUGIN_SRP512: string;
14
14
  export declare const WIRE_CRYPT_DISABLE: number;
15
15
  export declare const WIRE_CRYPT_ENABLE: number;
16
+ /** Decode through JavaScript Number where applicable; unsafe coefficients may lose precision. */
17
+ export declare const NUMERIC_MODE_LOSSY: "lossy";
18
+ /** Return safe INT64/INT128 coefficients as numbers and unsafe ones as exact strings. */
19
+ export declare const NUMERIC_MODE_SAFE: "safe";
20
+ /** Return every INT64/INT128-backed fixed-point value as an exact string. */
21
+ export declare const NUMERIC_MODE_STRING: "string";
16
22
  /** A transaction sees changes done by uncommitted transactions. */
17
23
  export declare const ISOLATION_READ_UNCOMMITTED: number[];
18
24
  /** A transaction sees only data committed before the statement has been executed. */
package/lib/index.js CHANGED
@@ -17,9 +17,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.isc_dpb_old_num_files = exports.isc_dpb_old_file_size = exports.isc_dpb_online_dump = exports.isc_dpb_interp = exports.isc_dpb_sys_user_name_enc = exports.isc_dpb_password_enc = exports.isc_dpb_password = exports.isc_dpb_user_name = exports.isc_dpb_no_reserve = exports.isc_dpb_quit_log = exports.isc_dpb_begin_log = exports.isc_dpb_force_write = exports.isc_dpb_delete_shadow = exports.isc_dpb_sweep_interval = exports.isc_dpb_activate_shadow = exports.isc_dpb_encrypt_key = exports.isc_dpb_sys_user_name = exports.isc_dpb_license = exports.isc_dpb_damaged = exports.isc_dpb_no_garbage_collect = exports.isc_dpb_trace = exports.isc_dpb_number_of_users = exports.isc_dpb_dbkey_scope = exports.isc_dpb_disable_journal = exports.isc_dpb_enable_journal = exports.isc_dpb_sweep = exports.isc_dpb_verify = exports.isc_dpb_garbage_collect = exports.isc_dpb_debug = exports.isc_dpb_buffer_length = exports.isc_dpb_num_buffers = exports.isc_dpb_page_size = exports.isc_dpb_journal = exports.isc_dpb_allocation = exports.isc_dpb_cdd_pathname = exports.isc_dpb_version2 = exports.isc_dpb_version1 = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
21
- exports.isc_dpb_set_db_replica = exports.isc_dpb_session_time_zone = exports.isc_dpb_map_attach = exports.isc_dpb_reset_icu = exports.isc_dpb_nolinger = exports.isc_dpb_config = exports.isc_dpb_auth_plugin_name = exports.isc_dpb_auth_plugin_list = exports.isc_dpb_specific_auth_data = exports.isc_dpb_os_user = exports.isc_dpb_host_name = exports.isc_dpb_remote_protocol = exports.isc_dpb_client_version = exports.isc_dpb_auth_block = exports.isc_dpb_ext_call_depth = exports.isc_dpb_utf8_filename = exports.isc_dpb_org_filename = exports.isc_dpb_trusted_role = exports.isc_dpb_process_name = exports.isc_dpb_trusted_auth = exports.isc_dpb_no_db_triggers = exports.isc_dpb_process_id = exports.isc_dpb_address_path = exports.isc_dpb_gsec_attach = exports.isc_dpb_set_db_charset = exports.isc_dpb_gstat_attach = exports.isc_dpb_gfix_attach = exports.isc_dpb_set_db_sql_dialect = exports.isc_dpb_set_db_readonly = exports.isc_dpb_sql_dialect = exports.isc_dpb_working_directory = exports.isc_dpb_set_page_buffers = exports.isc_dpb_sql_role_name = exports.isc_dpb_gbak_attach = exports.isc_dpb_dummy_packet_interval = exports.isc_dpb_connect_timeout = exports.isc_dpb_sec_attach = exports.isc_dpb_overwrite = exports.isc_dpb_reserved = exports.isc_dpb_shutdown_delay = exports.isc_dpb_online = exports.isc_dpb_shutdown = exports.isc_dpb_cache_manager = exports.isc_dpb_lc_ctype = exports.isc_dpb_lc_messages = exports.isc_dpb_old_dump_id = exports.isc_dpb_old_start_file = exports.isc_dpb_old_start_seqno = exports.isc_dpb_old_start_page = exports.isc_dpb_old_file = void 0;
22
- exports.parseNamedPlaceholders = exports.parseConnectionString = exports.parseConnectionUri = exports.connection = exports.SQL_TYPES = exports.escape = exports.isc_dpb_search_path = exports.isc_dpb_max_inline_blob_size = exports.isc_dpb_max_blob_cache_size = exports.isc_dpb_owner = exports.isc_dpb_worker_attach = exports.isc_dpb_parallel_workers = exports.isc_dpb_upgrade_db = exports.isc_dpb_clear_map = exports.isc_dpb_decfloat_traps = exports.isc_dpb_decfloat_round = exports.isc_dpb_set_bind = void 0;
20
+ exports.isc_dpb_interp = exports.isc_dpb_sys_user_name_enc = exports.isc_dpb_password_enc = exports.isc_dpb_password = exports.isc_dpb_user_name = exports.isc_dpb_no_reserve = exports.isc_dpb_quit_log = exports.isc_dpb_begin_log = exports.isc_dpb_force_write = exports.isc_dpb_delete_shadow = exports.isc_dpb_sweep_interval = exports.isc_dpb_activate_shadow = exports.isc_dpb_encrypt_key = exports.isc_dpb_sys_user_name = exports.isc_dpb_license = exports.isc_dpb_damaged = exports.isc_dpb_no_garbage_collect = exports.isc_dpb_trace = exports.isc_dpb_number_of_users = exports.isc_dpb_dbkey_scope = exports.isc_dpb_disable_journal = exports.isc_dpb_enable_journal = exports.isc_dpb_sweep = exports.isc_dpb_verify = exports.isc_dpb_garbage_collect = exports.isc_dpb_debug = exports.isc_dpb_buffer_length = exports.isc_dpb_num_buffers = exports.isc_dpb_page_size = exports.isc_dpb_journal = exports.isc_dpb_allocation = exports.isc_dpb_cdd_pathname = exports.isc_dpb_version2 = exports.isc_dpb_version1 = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.NUMERIC_MODE_STRING = exports.NUMERIC_MODE_SAFE = exports.NUMERIC_MODE_LOSSY = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
21
+ exports.isc_dpb_reset_icu = exports.isc_dpb_nolinger = exports.isc_dpb_config = exports.isc_dpb_auth_plugin_name = exports.isc_dpb_auth_plugin_list = exports.isc_dpb_specific_auth_data = exports.isc_dpb_os_user = exports.isc_dpb_host_name = exports.isc_dpb_remote_protocol = exports.isc_dpb_client_version = exports.isc_dpb_auth_block = exports.isc_dpb_ext_call_depth = exports.isc_dpb_utf8_filename = exports.isc_dpb_org_filename = exports.isc_dpb_trusted_role = exports.isc_dpb_process_name = exports.isc_dpb_trusted_auth = exports.isc_dpb_no_db_triggers = exports.isc_dpb_process_id = exports.isc_dpb_address_path = exports.isc_dpb_gsec_attach = exports.isc_dpb_set_db_charset = exports.isc_dpb_gstat_attach = exports.isc_dpb_gfix_attach = exports.isc_dpb_set_db_sql_dialect = exports.isc_dpb_set_db_readonly = exports.isc_dpb_sql_dialect = exports.isc_dpb_working_directory = exports.isc_dpb_set_page_buffers = exports.isc_dpb_sql_role_name = exports.isc_dpb_gbak_attach = exports.isc_dpb_dummy_packet_interval = exports.isc_dpb_connect_timeout = exports.isc_dpb_sec_attach = exports.isc_dpb_overwrite = exports.isc_dpb_reserved = exports.isc_dpb_shutdown_delay = exports.isc_dpb_online = exports.isc_dpb_shutdown = exports.isc_dpb_cache_manager = exports.isc_dpb_lc_ctype = exports.isc_dpb_lc_messages = exports.isc_dpb_old_dump_id = exports.isc_dpb_old_start_file = exports.isc_dpb_old_start_seqno = exports.isc_dpb_old_start_page = exports.isc_dpb_old_file = exports.isc_dpb_old_num_files = exports.isc_dpb_old_file_size = exports.isc_dpb_online_dump = void 0;
22
+ exports.parseNamedPlaceholders = exports.parseConnectionString = exports.parseConnectionUri = exports.connection = exports.SQL_TYPES = exports.escape = exports.isc_dpb_search_path = exports.isc_dpb_max_inline_blob_size = exports.isc_dpb_max_blob_cache_size = exports.isc_dpb_owner = exports.isc_dpb_worker_attach = exports.isc_dpb_parallel_workers = exports.isc_dpb_upgrade_db = exports.isc_dpb_clear_map = exports.isc_dpb_decfloat_traps = exports.isc_dpb_decfloat_round = exports.isc_dpb_set_bind = exports.isc_dpb_set_db_replica = exports.isc_dpb_session_time_zone = exports.isc_dpb_map_attach = void 0;
23
23
  exports.attach = attach;
24
24
  exports.drop = drop;
25
25
  exports.create = create;
@@ -54,6 +54,12 @@ exports.AUTH_PLUGIN_SRP384 = const_1.default.AUTH_PLUGIN_SRP384;
54
54
  exports.AUTH_PLUGIN_SRP512 = const_1.default.AUTH_PLUGIN_SRP512;
55
55
  exports.WIRE_CRYPT_DISABLE = const_1.default.WIRE_CRYPT_DISABLE;
56
56
  exports.WIRE_CRYPT_ENABLE = const_1.default.WIRE_CRYPT_ENABLE;
57
+ /** Decode through JavaScript Number where applicable; unsafe coefficients may lose precision. */
58
+ exports.NUMERIC_MODE_LOSSY = const_1.default.NUMERIC_MODE_LOSSY;
59
+ /** Return safe INT64/INT128 coefficients as numbers and unsafe ones as exact strings. */
60
+ exports.NUMERIC_MODE_SAFE = const_1.default.NUMERIC_MODE_SAFE;
61
+ /** Return every INT64/INT128-backed fixed-point value as an exact string. */
62
+ exports.NUMERIC_MODE_STRING = const_1.default.NUMERIC_MODE_STRING;
57
63
  /** A transaction sees changes done by uncommitted transactions. */
58
64
  exports.ISOLATION_READ_UNCOMMITTED = const_1.default.ISOLATION_READ_UNCOMMITTED;
59
65
  /** A transaction sees only data committed before the statement has been executed. */
package/lib/types.d.ts CHANGED
@@ -58,6 +58,8 @@ export interface ColumnMetadata {
58
58
  collationId?: number;
59
59
  }
60
60
  export type Isolation = number[];
61
+ /** Result conversion policy for INT64/INT128-backed fixed-point values. */
62
+ export type NumericMode = 'lossy' | 'safe' | 'string';
61
63
  export type TransactionOptions = {
62
64
  autoCommit?: boolean;
63
65
  autoUndo?: boolean;
@@ -373,6 +375,17 @@ export interface Options {
373
375
  */
374
376
  blobReadChunkSize?: number;
375
377
  wireCrypt?: number;
378
+ /**
379
+ * Result conversion policy for BIGINT/INT128 and fixed-point
380
+ * NUMERIC/DECIMAL values backed by them.
381
+ *
382
+ * - `lossy` (default) decodes through JavaScript `Number` where
383
+ * applicable; unsafe coefficients may lose precision.
384
+ * - `safe` returns a number when the raw integer coefficient is within
385
+ * JavaScript's safe range, otherwise an exact scaled string.
386
+ * - `string` always returns an exact scaled string.
387
+ */
388
+ numericMode?: NumericMode;
376
389
  wireCompression?: boolean;
377
390
  /**
378
391
  * Enable named placeholders: SQL may use `:name` markers and params may
package/lib/uri.js CHANGED
@@ -189,7 +189,14 @@ function normalizeOptions(options) {
189
189
  if (typeof options === 'string') {
190
190
  options = parseConnectionString(options);
191
191
  }
192
- return applyEnvDefaults(options);
192
+ const normalized = applyEnvDefaults(options);
193
+ const numericMode = normalized && normalized.numericMode;
194
+ if (numericMode !== undefined && numericMode !== 'lossy' &&
195
+ numericMode !== 'safe' && numericMode !== 'string') {
196
+ throw new Error('Invalid numericMode option: ' + numericMode +
197
+ ' (expected "lossy", "safe", or "string")');
198
+ }
199
+ return normalized;
193
200
  }
194
201
  /**
195
202
  * Fall back to environment variables for connection settings the caller
@@ -16,6 +16,9 @@ declare const Const: Readonly<{
16
16
  MAX_BUFFER_SIZE: number;
17
17
  MAX_INT: number;
18
18
  MIN_INT: number;
19
+ NUMERIC_MODE_LOSSY: 'lossy';
20
+ NUMERIC_MODE_SAFE: 'safe';
21
+ NUMERIC_MODE_STRING: 'string';
19
22
  op_void: number;
20
23
  op_connect: number;
21
24
  op_exit: number;
package/lib/wire/const.js CHANGED
@@ -22,6 +22,11 @@ const int = {
22
22
  MAX_INT: Math.pow(2, 31) - 1,
23
23
  MIN_INT: -Math.pow(2, 31),
24
24
  };
25
+ const numericMode = {
26
+ NUMERIC_MODE_LOSSY: 'lossy',
27
+ NUMERIC_MODE_SAFE: 'safe',
28
+ NUMERIC_MODE_STRING: 'string',
29
+ };
25
30
  const op = {
26
31
  op_void: 0, // Packet has been voided
27
32
  op_connect: 1, // Connect to remote server
@@ -861,6 +866,7 @@ const Const = Object.freeze({
861
866
  ...int,
862
867
  ...iscAction,
863
868
  ...iscError,
869
+ ...numericMode,
864
870
  ...op,
865
871
  ...protocol,
866
872
  ...service,
@@ -93,7 +93,9 @@ export declare class XdrReader {
93
93
  readInt(): number;
94
94
  readUInt(): number;
95
95
  readInt64(): number;
96
+ readInt64BigInt(): bigint;
96
97
  readInt128(): bigint;
98
+ readInt128Signed(): bigint;
97
99
  readDecFloat16(): string | number;
98
100
  readDecFloat34(): string | number;
99
101
  readShort(): number;
@@ -370,7 +370,10 @@ class XdrReader {
370
370
  // Note: precision is limited to Number.MAX_SAFE_INTEGER (±2^53-1).
371
371
  // Values outside this range lose precision, which matches the previous
372
372
  // Long(low, high).toNumber() behaviour.
373
- const result = Number(this.buffer.readBigInt64BE(this.pos));
373
+ return Number(this.readInt64BigInt());
374
+ }
375
+ readInt64BigInt() {
376
+ const result = this.buffer.readBigInt64BE(this.pos);
374
377
  this.pos += 8;
375
378
  return result;
376
379
  }
@@ -381,6 +384,13 @@ class XdrReader {
381
384
  this.pos += 8;
382
385
  return (BigInt(high) << BigInt(64)) + BigInt(low);
383
386
  }
387
+ readInt128Signed() {
388
+ var high = this.buffer.readBigInt64BE(this.pos);
389
+ this.pos += 8;
390
+ var low = this.buffer.readBigUInt64BE(this.pos);
391
+ this.pos += 8;
392
+ return (BigInt(high) << BigInt(64)) + BigInt(low);
393
+ }
384
394
  readDecFloat16() {
385
395
  // DECFLOAT(16) - IEEE 754 Decimal64 - 8 bytes
386
396
  // Full IEEE 754-2008 Decimal64 implementation
@@ -1,6 +1,9 @@
1
1
  import type { TextCodec } from './codepages';
2
2
  import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
3
- import type { RecordCounts } from '../types';
3
+ import type { NumericMode, RecordCounts } from '../types';
4
+ type NumericDecodeOptions = {
5
+ numericMode?: NumericMode;
6
+ };
4
7
  export declare function getFirebirdCharsetWidth(charset?: string): number;
5
8
  /**
6
9
  * Resolve the Node.js Buffer encoding to use when decoding text from a
@@ -197,11 +200,11 @@ export declare class SQLVarShort extends SQLVarInt {
197
200
  calcBlr(blr: BlrWriter): void;
198
201
  }
199
202
  export declare class SQLVarInt64 extends SQLVarBase {
200
- decode(data: XdrReader, lowerV13: boolean): number | null;
203
+ decode(data: XdrReader, lowerV13: boolean, options?: NumericDecodeOptions): string | number | null;
201
204
  calcBlr(blr: BlrWriter): void;
202
205
  }
203
206
  export declare class SQLVarInt128 extends SQLVarBase {
204
- decode(data: XdrReader, lowerV13: boolean): string | number | null;
207
+ decode(data: XdrReader, lowerV13: boolean, options?: NumericDecodeOptions): string | number | null;
205
208
  calcBlr(blr: BlrWriter): void;
206
209
  }
207
210
  export declare class SQLVarDecFloat16 extends SQLVarBase {
@@ -30,6 +30,37 @@ const codepages_1 = require("./codepages");
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
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
34
+ const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
35
+ /** Format a signed Firebird integer coefficient without passing through Number. */
36
+ function formatScaledBigInt(value, scale) {
37
+ const negative = value < 0n;
38
+ let digits = (negative ? -value : value).toString();
39
+ const sign = negative ? '-' : '';
40
+ if (scale === 0)
41
+ return sign + digits;
42
+ if (scale > 0)
43
+ return sign + digits + '0'.repeat(scale);
44
+ const places = -scale;
45
+ if (digits.length <= places)
46
+ digits = digits.padStart(places + 1, '0');
47
+ return sign + digits.slice(0, -places) + '.' + digits.slice(-places);
48
+ }
49
+ function decodeExactNumeric(value, scale, mode) {
50
+ if (mode === 'string' || value > MAX_SAFE_BIGINT || value < MIN_SAFE_BIGINT) {
51
+ return formatScaledBigInt(value, scale);
52
+ }
53
+ return scale < 0
54
+ ? Number(value) / Math.pow(10, -scale)
55
+ : Number(value) * Math.pow(10, scale);
56
+ }
57
+ /** Decode INT128 using the mixed number/string policy of lossy mode. */
58
+ function decodeLossyInt128(value, scale) {
59
+ if (value > MAX_SAFE_BIGINT) {
60
+ return formatScaledBigInt(value, scale);
61
+ }
62
+ return Number(value) / ScaleDivisor[Math.abs(scale)];
63
+ }
33
64
  /**
34
65
  * Maps Firebird character-set names (upper-case) to the Node.js Buffer
35
66
  * encoding string used by Buffer.toString() / Buffer.from().
@@ -480,10 +511,16 @@ class SQLVarShort extends SQLVarInt {
480
511
  exports.SQLVarShort = SQLVarShort;
481
512
  //------------------------------------------------------
482
513
  class SQLVarInt64 extends SQLVarBase {
483
- decode(data, lowerV13) {
484
- var ret = data.readInt64();
485
- if (this.scale) {
486
- ret = ret / ScaleDivisor[Math.abs(this.scale)];
514
+ decode(data, lowerV13, options) {
515
+ const mode = options?.numericMode || const_1.default.NUMERIC_MODE_LOSSY;
516
+ let ret;
517
+ if (mode === const_1.default.NUMERIC_MODE_LOSSY) {
518
+ ret = data.readInt64();
519
+ if (this.scale)
520
+ ret = ret / ScaleDivisor[Math.abs(this.scale)];
521
+ }
522
+ else {
523
+ ret = decodeExactNumeric(data.readInt64BigInt(), this.scale, mode);
487
524
  }
488
525
  if (!lowerV13 || !data.readInt()) {
489
526
  return ret;
@@ -498,21 +535,11 @@ class SQLVarInt64 extends SQLVarBase {
498
535
  exports.SQLVarInt64 = SQLVarInt64;
499
536
  //------------------------------------------------------
500
537
  class SQLVarInt128 extends SQLVarBase {
501
- decode(data, lowerV13) {
502
- var retBigInt = BigInt(data.readInt128());
503
- let ret;
504
- if (retBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
505
- ret = retBigInt.toString();
506
- var integerPart = ret.slice(0, Math.abs(this.scale) * -1);
507
- var decimalPart = ret.slice(Math.abs(this.scale) * -1);
508
- if (integerPart === '')
509
- integerPart = '0';
510
- ret = `${integerPart}.${decimalPart}`;
511
- }
512
- else {
513
- ret = Number(retBigInt);
514
- ret = ret / ScaleDivisor[Math.abs(this.scale)];
515
- }
538
+ decode(data, lowerV13, options) {
539
+ const mode = options?.numericMode || const_1.default.NUMERIC_MODE_LOSSY;
540
+ const ret = mode === const_1.default.NUMERIC_MODE_LOSSY
541
+ ? decodeLossyInt128(data.readInt128(), this.scale)
542
+ : decodeExactNumeric(data.readInt128Signed(), this.scale, mode);
516
543
  if (!lowerV13 || !data.readInt()) {
517
544
  return ret;
518
545
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.14.4",
3
+ "version": "2.15.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/index.ts CHANGED
@@ -35,6 +35,13 @@ export const AUTH_PLUGIN_SRP512: string = Const.AUTH_PLUGIN_SRP512;
35
35
  export const WIRE_CRYPT_DISABLE: number = Const.WIRE_CRYPT_DISABLE;
36
36
  export const WIRE_CRYPT_ENABLE: number = Const.WIRE_CRYPT_ENABLE;
37
37
 
38
+ /** Decode through JavaScript Number where applicable; unsafe coefficients may lose precision. */
39
+ export const NUMERIC_MODE_LOSSY = Const.NUMERIC_MODE_LOSSY;
40
+ /** Return safe INT64/INT128 coefficients as numbers and unsafe ones as exact strings. */
41
+ export const NUMERIC_MODE_SAFE = Const.NUMERIC_MODE_SAFE;
42
+ /** Return every INT64/INT128-backed fixed-point value as an exact string. */
43
+ export const NUMERIC_MODE_STRING = Const.NUMERIC_MODE_STRING;
44
+
38
45
  /** A transaction sees changes done by uncommitted transactions. */
39
46
  export const ISOLATION_READ_UNCOMMITTED: number[] = Const.ISOLATION_READ_UNCOMMITTED;
40
47
  /** A transaction sees only data committed before the statement has been executed. */
package/src/types.ts CHANGED
@@ -70,6 +70,9 @@ export interface ColumnMetadata {
70
70
 
71
71
  export type Isolation = number[];
72
72
 
73
+ /** Result conversion policy for INT64/INT128-backed fixed-point values. */
74
+ export type NumericMode = 'lossy' | 'safe' | 'string';
75
+
73
76
  export type TransactionOptions = {
74
77
  autoCommit?: boolean;
75
78
  autoUndo?: boolean;
@@ -433,6 +436,17 @@ export interface Options {
433
436
  */
434
437
  blobReadChunkSize?: number;
435
438
  wireCrypt?: number; // WIRE_CRYPT_DISABLE or WIRE_CRYPT_ENABLE
439
+ /**
440
+ * Result conversion policy for BIGINT/INT128 and fixed-point
441
+ * NUMERIC/DECIMAL values backed by them.
442
+ *
443
+ * - `lossy` (default) decodes through JavaScript `Number` where
444
+ * applicable; unsafe coefficients may lose precision.
445
+ * - `safe` returns a number when the raw integer coefficient is within
446
+ * JavaScript's safe range, otherwise an exact scaled string.
447
+ * - `string` always returns an exact scaled string.
448
+ */
449
+ numericMode?: NumericMode;
436
450
  wireCompression?: boolean;
437
451
  /**
438
452
  * Enable named placeholders: SQL may use `:name` markers and params may
package/src/uri.ts CHANGED
@@ -200,7 +200,14 @@ export function normalizeOptions<T>(options: T | string): T {
200
200
  if (typeof options === 'string') {
201
201
  options = parseConnectionString(options) as T;
202
202
  }
203
- return applyEnvDefaults(options as any);
203
+ const normalized = applyEnvDefaults(options as any);
204
+ const numericMode = normalized && normalized.numericMode;
205
+ if (numericMode !== undefined && numericMode !== 'lossy' &&
206
+ numericMode !== 'safe' && numericMode !== 'string') {
207
+ throw new Error('Invalid numericMode option: ' + numericMode +
208
+ ' (expected "lossy", "safe", or "string")');
209
+ }
210
+ return normalized;
204
211
  }
205
212
 
206
213
  /**
package/src/wire/const.ts CHANGED
@@ -25,6 +25,12 @@ const int = {
25
25
  MIN_INT : -Math.pow(2, 31),
26
26
  };
27
27
 
28
+ const numericMode = {
29
+ NUMERIC_MODE_LOSSY : 'lossy',
30
+ NUMERIC_MODE_SAFE : 'safe',
31
+ NUMERIC_MODE_STRING : 'string',
32
+ } as const;
33
+
28
34
  const op = {
29
35
  op_void : 0, // Packet has been voided
30
36
  op_connect : 1, // Connect to remote server
@@ -929,6 +935,7 @@ const Const = Object.freeze({
929
935
  ...int,
930
936
  ...iscAction,
931
937
  ...iscError,
938
+ ...numericMode,
932
939
  ...op,
933
940
  ...protocol,
934
941
  ...service,
@@ -481,12 +481,16 @@ export class XdrReader {
481
481
  // Note: precision is limited to Number.MAX_SAFE_INTEGER (±2^53-1).
482
482
  // Values outside this range lose precision, which matches the previous
483
483
  // Long(low, high).toNumber() behaviour.
484
- const result = Number(this.buffer.readBigInt64BE(this.pos));
484
+ return Number(this.readInt64BigInt());
485
+ }
486
+
487
+ readInt64BigInt(): bigint {
488
+ const result = this.buffer.readBigInt64BE(this.pos);
485
489
  this.pos += 8;
486
490
  return result;
487
491
  }
488
492
 
489
- readInt128() {
493
+ readInt128(): bigint {
490
494
  var high = this.buffer.readBigUInt64BE(this.pos)
491
495
  this.pos += 8
492
496
 
@@ -496,6 +500,16 @@ export class XdrReader {
496
500
  return (BigInt(high) << BigInt(64)) + BigInt(low)
497
501
  }
498
502
 
503
+ readInt128Signed(): bigint {
504
+ var high = this.buffer.readBigInt64BE(this.pos)
505
+ this.pos += 8
506
+
507
+ var low = this.buffer.readBigUInt64BE(this.pos)
508
+ this.pos += 8
509
+
510
+ return (BigInt(high) << BigInt(64)) + BigInt(low)
511
+ }
512
+
499
513
  readDecFloat16() {
500
514
  // DECFLOAT(16) - IEEE 754 Decimal64 - 8 bytes
501
515
  // Full IEEE 754-2008 Decimal64 implementation
@@ -3,7 +3,7 @@ import { BlrReader } from './serialize';
3
3
  import { getCodec } from './codepages';
4
4
  import type { TextCodec } from './codepages';
5
5
  import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
6
- import type { RecordCounts } from '../types';
6
+ import type { NumericMode, RecordCounts } from '../types';
7
7
 
8
8
  /***************************************
9
9
  *
@@ -19,6 +19,44 @@ const
19
19
  MsPerMinute = 60000;
20
20
 
21
21
  const EMPTY_BUFFER = Buffer.alloc(0);
22
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
23
+ const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
24
+
25
+ type NumericDecodeOptions = {
26
+ numericMode?: NumericMode;
27
+ };
28
+
29
+ /** Format a signed Firebird integer coefficient without passing through Number. */
30
+ function formatScaledBigInt(value: bigint, scale: number): string {
31
+ const negative = value < 0n;
32
+ let digits = (negative ? -value : value).toString();
33
+ const sign = negative ? '-' : '';
34
+
35
+ if (scale === 0) return sign + digits;
36
+ if (scale > 0) return sign + digits + '0'.repeat(scale);
37
+
38
+ const places = -scale;
39
+ if (digits.length <= places) digits = digits.padStart(places + 1, '0');
40
+ return sign + digits.slice(0, -places) + '.' + digits.slice(-places);
41
+ }
42
+
43
+ function decodeExactNumeric(value: bigint, scale: number, mode: 'safe' | 'string'): number | string {
44
+ if (mode === 'string' || value > MAX_SAFE_BIGINT || value < MIN_SAFE_BIGINT) {
45
+ return formatScaledBigInt(value, scale);
46
+ }
47
+ return scale < 0
48
+ ? Number(value) / Math.pow(10, -scale)
49
+ : Number(value) * Math.pow(10, scale);
50
+ }
51
+
52
+ /** Decode INT128 using the mixed number/string policy of lossy mode. */
53
+ function decodeLossyInt128(value: bigint, scale: number): number | string {
54
+ if (value > MAX_SAFE_BIGINT) {
55
+ return formatScaledBigInt(value, scale);
56
+ }
57
+
58
+ return Number(value) / ScaleDivisor[Math.abs(scale)];
59
+ }
22
60
 
23
61
  /**
24
62
  * Maps Firebird character-set names (upper-case) to the Node.js Buffer
@@ -555,11 +593,15 @@ export class SQLVarShort extends SQLVarInt {
555
593
  //------------------------------------------------------
556
594
 
557
595
  export class SQLVarInt64 extends SQLVarBase {
558
- decode(data: XdrReader, lowerV13: boolean) {
559
- var ret = data.readInt64();
596
+ decode(data: XdrReader, lowerV13: boolean, options?: NumericDecodeOptions) {
597
+ const mode = options?.numericMode || Const.NUMERIC_MODE_LOSSY;
598
+ let ret: number | string;
560
599
 
561
- if (this.scale) {
562
- ret = ret / ScaleDivisor[Math.abs(this.scale)];
600
+ if (mode === Const.NUMERIC_MODE_LOSSY) {
601
+ ret = data.readInt64();
602
+ if (this.scale) ret = ret / ScaleDivisor[Math.abs(this.scale)];
603
+ } else {
604
+ ret = decodeExactNumeric(data.readInt64BigInt(), this.scale, mode);
563
605
  }
564
606
 
565
607
  if (!lowerV13 || !data.readInt()) {
@@ -577,23 +619,11 @@ export class SQLVarInt64 extends SQLVarBase {
577
619
  //------------------------------------------------------
578
620
 
579
621
  export class SQLVarInt128 extends SQLVarBase {
580
- decode(data: XdrReader, lowerV13: boolean) {
581
- var retBigInt = BigInt(data.readInt128())
582
- let ret: string | number;
583
-
584
- if (retBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
585
- ret = retBigInt.toString();
586
-
587
- var integerPart = ret.slice(0, Math.abs(this.scale) * -1)
588
- var decimalPart = ret.slice(Math.abs(this.scale) * -1)
589
-
590
- if (integerPart === '') integerPart = '0'
591
-
592
- ret = `${integerPart}.${decimalPart}`
593
- } else {
594
- ret = Number(retBigInt);
595
- ret = ret / ScaleDivisor[Math.abs(this.scale)];
596
- }
622
+ decode(data: XdrReader, lowerV13: boolean, options?: NumericDecodeOptions) {
623
+ const mode = options?.numericMode || Const.NUMERIC_MODE_LOSSY;
624
+ const ret = mode === Const.NUMERIC_MODE_LOSSY
625
+ ? decodeLossyInt128(data.readInt128(), this.scale)
626
+ : decodeExactNumeric(data.readInt128Signed(), this.scale, mode);
597
627
 
598
628
  if (!lowerV13 || !data.readInt()) {
599
629
  return ret;