node-firebird 2.14.3 → 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 +59 -14
- package/lib/index.d.ts +6 -0
- package/lib/index.js +9 -3
- package/lib/types.d.ts +13 -0
- package/lib/uri.js +8 -1
- package/lib/wire/codepages.d.ts +4 -2
- package/lib/wire/codepages.js +52 -16
- package/lib/wire/const.d.ts +3 -0
- package/lib/wire/const.js +6 -0
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +11 -1
- package/lib/wire/xsqlvar.d.ts +6 -3
- package/lib/wire/xsqlvar.js +49 -22
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/types.ts +14 -0
- package/src/uri.ts +8 -1
- package/src/wire/codepages.ts +56 -16
- package/src/wire/const.ts +7 -0
- package/src/wire/serialize.ts +16 -2
- package/src/wire/xsqlvar.ts +55 -25
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.
|
|
@@ -1558,14 +1600,15 @@ fb.attach(_connection, function (err, svc) {
|
|
|
1558
1600
|
|
|
1559
1601
|
Node-Firebird defaults to `UTF-8` for database connections, but fully supports custom client character sets. You can set the connection encoding by specifying `options.encoding` (e.g. `'UTF8'`, `'WIN1252'`, `'ISO8859_1'`, `'LATIN1'`, `'ASCII'`, or `'NONE'`).
|
|
1560
1602
|
|
|
1561
|
-
Commonly used Firebird character sets are
|
|
1603
|
+
Commonly used Firebird character sets are handled through the corresponding Node.js encoding or ICU codec:
|
|
1562
1604
|
|
|
1563
|
-
| Firebird Character Set | Node.js
|
|
1564
|
-
| ---------------------- |
|
|
1565
|
-
| `UTF8`, `UNICODE_FSS` | `utf8`
|
|
1566
|
-
| `WIN1252
|
|
1567
|
-
| `
|
|
1568
|
-
| `
|
|
1605
|
+
| Firebird Character Set | Node.js encoding / ICU codec | Description / Notes |
|
|
1606
|
+
| ---------------------- | ---------------------------- | ------------------- |
|
|
1607
|
+
| `UTF8`, `UNICODE_FSS` | `utf8` | Unicode. Handles character-level truncation automatically based on charset width. |
|
|
1608
|
+
| `WIN1252` | ICU `windows-1252` codec | Windows Western European encoding, including the printable characters in bytes `0x80`–`0x9F`. |
|
|
1609
|
+
| `ISO8859_1`, `LATIN1` | `latin1` | ISO-8859-1-compatible byte mapping; intentionally distinct from Windows-1252. |
|
|
1610
|
+
| `ASCII` | `ascii` | 7-bit ASCII. |
|
|
1611
|
+
| `NONE` | `latin1` | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
|
|
1569
1612
|
|
|
1570
1613
|
Beyond Node's native encodings, the driver ships **codepage codecs** for the
|
|
1571
1614
|
single-byte charsets (decode *and* encode — columns, parameters, SQL
|
|
@@ -1581,7 +1624,9 @@ await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as
|
|
|
1581
1624
|
```
|
|
1582
1625
|
|
|
1583
1626
|
The codecs are built from Node's ICU tables at first use (present in every
|
|
1584
|
-
official Node build).
|
|
1627
|
+
official Node build). If a constrained runtime does not provide a requested
|
|
1628
|
+
codec, the driver throws a descriptive error instead of silently falling back
|
|
1629
|
+
to UTF-8 and corrupting text. `attachOrCreate`/`create` honour `options.encoding`
|
|
1585
1630
|
for the new database's default charset too. Accented characters and
|
|
1586
1631
|
fixed-length `CHAR(N)` whitespace/truncation are handled automatically per
|
|
1587
1632
|
the charset width — and single-byte columns (including charset `NONE`) are
|
|
@@ -1597,7 +1642,7 @@ var options = {
|
|
|
1597
1642
|
database: 'win1252_db.fdb',
|
|
1598
1643
|
user: 'SYSDBA',
|
|
1599
1644
|
password: 'masterkey',
|
|
1600
|
-
encoding: 'WIN1252' //
|
|
1645
|
+
encoding: 'WIN1252' // Uses the WHATWG/ICU Windows-1252 codec
|
|
1601
1646
|
};
|
|
1602
1647
|
|
|
1603
1648
|
Firebird.attach(options, function (err, db) {
|
|
@@ -2296,7 +2341,7 @@ options.blobReadChunkSize = 65535;
|
|
|
2296
2341
|
|
|
2297
2342
|
If your server and client are on the same host, this won't matter much — the slowdown is latency-bound, not throughput-bound.
|
|
2298
2343
|
|
|
2299
|
-
#### How do I use an encoding other than UTF-8 (e.g. WIN1252
|
|
2344
|
+
#### How do I use an encoding other than UTF-8 (e.g. WIN1252 or Latin1)?
|
|
2300
2345
|
|
|
2301
2346
|
Set `options.encoding` — no source changes required (see [Character Set & Encoding Support](#character-set--encoding-support) for the full mapping table):
|
|
2302
2347
|
|
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.
|
|
21
|
-
exports.
|
|
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
|
-
|
|
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
|
package/lib/wire/codepages.d.ts
CHANGED
|
@@ -17,7 +17,9 @@ export interface TextCodec {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function charsetWidthById(id: number | undefined): number;
|
|
19
19
|
/**
|
|
20
|
-
* Codec for a Firebird charset name, or null when the charset is unknown
|
|
21
|
-
* natively handled by Buffer
|
|
20
|
+
* Codec for a Firebird charset name, or null when the charset is unknown or
|
|
21
|
+
* natively handled by Buffer. A known codepage whose ICU table is unavailable
|
|
22
|
+
* throws instead of silently falling back to UTF-8. Successful and unknown
|
|
23
|
+
* lookups are cached; failures are not.
|
|
22
24
|
*/
|
|
23
25
|
export declare function getCodec(charsetName: string | undefined): TextCodec | null;
|
package/lib/wire/codepages.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.getCodec = getCodec;
|
|
|
17
17
|
const ICU_LABELS = Object.freeze({
|
|
18
18
|
WIN1250: 'windows-1250',
|
|
19
19
|
WIN1251: 'windows-1251',
|
|
20
|
+
WIN1252: 'windows-1252',
|
|
20
21
|
WIN1253: 'windows-1253',
|
|
21
22
|
WIN1254: 'windows-1254',
|
|
22
23
|
WIN1255: 'windows-1255',
|
|
@@ -60,30 +61,63 @@ function charsetWidthById(id) {
|
|
|
60
61
|
return CHARSET_WIDTH_BY_ID[id] || 1;
|
|
61
62
|
}
|
|
62
63
|
const cache = new Map();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
/**
|
|
65
|
+
* WHATWG windows-1252 code points for bytes 0x80–0x9F (the only range
|
|
66
|
+
* where it differs from Latin-1). Node's TextDecoder cannot be trusted
|
|
67
|
+
* here: through at least Node 20 the 'windows-1252' label is routed
|
|
68
|
+
* through a latin1 fast path, decoding this range as C1 controls, so
|
|
69
|
+
* the WIN1252 table is built from this fixed spec table instead of the
|
|
70
|
+
* runtime decoder. Later Node majors agree with this table exactly.
|
|
71
|
+
*/
|
|
72
|
+
const WIN1252_C1 = Object.freeze([
|
|
73
|
+
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
|
|
74
|
+
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
|
|
75
|
+
0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
|
76
|
+
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
|
|
77
|
+
]);
|
|
78
|
+
function buildByteToCodeTable(name, label) {
|
|
79
|
+
const toCode = new Uint16Array(256);
|
|
80
|
+
if (name === 'WIN1252') {
|
|
81
|
+
// Latin-1 identity outside 0x80–0x9F; spec table inside. Needs no
|
|
82
|
+
// ICU support at all, so WIN1252 works even on small-icu builds.
|
|
83
|
+
for (let i = 0; i < 256; i++) {
|
|
84
|
+
toCode[i] = (i & 0xE0) === 0x80 ? WIN1252_C1[i - 0x80] : i;
|
|
85
|
+
}
|
|
86
|
+
return toCode;
|
|
67
87
|
}
|
|
68
88
|
let decoder;
|
|
69
89
|
try {
|
|
70
90
|
decoder = new TextDecoder(label);
|
|
71
91
|
}
|
|
72
92
|
catch {
|
|
73
|
-
//
|
|
74
|
-
|
|
93
|
+
// Falling through to DEFAULT_ENCODING (UTF-8) would silently write
|
|
94
|
+
// different bytes from the explicitly requested Firebird codepage.
|
|
95
|
+
// Official Node builds include these ICU tables; constrained builds
|
|
96
|
+
// must fail clearly instead of corrupting text.
|
|
97
|
+
throw new Error(`The requested Firebird encoding ${name} requires the ${label} ICU codec, ` +
|
|
98
|
+
'but this Node.js runtime does not provide it. Use an official full-ICU ' +
|
|
99
|
+
'Node.js build, or connect with encoding NONE and pass explicitly encoded Buffer values.');
|
|
75
100
|
}
|
|
76
|
-
// Build both directions from the decoder, one byte at a time — every
|
|
77
|
-
// byte of a single-byte codepage maps to exactly one BMP character
|
|
78
|
-
// (undefined bytes decode to U+FFFD, which is kept for decoding but
|
|
79
|
-
// never used for the reverse map).
|
|
80
|
-
const toCode = new Uint16Array(256);
|
|
81
|
-
const toByte = new Map();
|
|
82
101
|
const one = Buffer.alloc(1);
|
|
83
102
|
for (let i = 0; i < 256; i++) {
|
|
84
103
|
one[0] = i;
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
toCode[i] = decoder.decode(one).charCodeAt(0);
|
|
105
|
+
}
|
|
106
|
+
return toCode;
|
|
107
|
+
}
|
|
108
|
+
function buildCodec(name) {
|
|
109
|
+
const label = ICU_LABELS[name];
|
|
110
|
+
if (!label) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// Build both directions from one byte→code table — every byte of a
|
|
114
|
+
// single-byte codepage maps to exactly one BMP character (undefined
|
|
115
|
+
// bytes decode to U+FFFD, which is kept for decoding but never used
|
|
116
|
+
// for the reverse map).
|
|
117
|
+
const toCode = buildByteToCodeTable(name, label);
|
|
118
|
+
const toByte = new Map();
|
|
119
|
+
for (let i = 0; i < 256; i++) {
|
|
120
|
+
const ch = String.fromCharCode(toCode[i]);
|
|
87
121
|
if (ch !== '�' && !toByte.has(ch)) {
|
|
88
122
|
toByte.set(ch, i);
|
|
89
123
|
}
|
|
@@ -120,8 +154,10 @@ function buildCodec(name) {
|
|
|
120
154
|
};
|
|
121
155
|
}
|
|
122
156
|
/**
|
|
123
|
-
* Codec for a Firebird charset name, or null when the charset is unknown
|
|
124
|
-
* natively handled by Buffer
|
|
157
|
+
* Codec for a Firebird charset name, or null when the charset is unknown or
|
|
158
|
+
* natively handled by Buffer. A known codepage whose ICU table is unavailable
|
|
159
|
+
* throws instead of silently falling back to UTF-8. Successful and unknown
|
|
160
|
+
* lookups are cached; failures are not.
|
|
125
161
|
*/
|
|
126
162
|
function getCodec(charsetName) {
|
|
127
163
|
if (!charsetName) {
|
package/lib/wire/const.d.ts
CHANGED
|
@@ -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,
|
package/lib/wire/serialize.d.ts
CHANGED
|
@@ -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;
|
package/lib/wire/serialize.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/lib/wire/xsqlvar.d.ts
CHANGED
|
@@ -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 {
|
package/lib/wire/xsqlvar.js
CHANGED
|
@@ -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().
|
|
@@ -39,13 +70,13 @@ const EMPTY_BUFFER = Buffer.alloc(0);
|
|
|
39
70
|
* We must decode raw bytes with the matching Node.js encoding so that
|
|
40
71
|
* characters outside ASCII are reproduced correctly.
|
|
41
72
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
73
|
+
* Other recognized single-byte character sets are handled by the ICU-backed
|
|
74
|
+
* codec path. Only unknown character-set names fall back to the
|
|
75
|
+
* connection-level DEFAULT_ENCODING, typically UTF-8.
|
|
44
76
|
*/
|
|
45
77
|
const FirebirdToNodeEncoding = Object.freeze({
|
|
46
78
|
UTF8: 'utf8',
|
|
47
79
|
UNICODE_FSS: 'utf8',
|
|
48
|
-
WIN1252: 'latin1',
|
|
49
80
|
ISO8859_1: 'latin1',
|
|
50
81
|
LATIN1: 'latin1',
|
|
51
82
|
ASCII: 'ascii',
|
|
@@ -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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
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
|
-
|
|
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/codepages.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface TextCodec {
|
|
|
21
21
|
const ICU_LABELS: Readonly<Record<string, string>> = Object.freeze({
|
|
22
22
|
WIN1250: 'windows-1250',
|
|
23
23
|
WIN1251: 'windows-1251',
|
|
24
|
+
WIN1252: 'windows-1252',
|
|
24
25
|
WIN1253: 'windows-1253',
|
|
25
26
|
WIN1254: 'windows-1254',
|
|
26
27
|
WIN1255: 'windows-1255',
|
|
@@ -68,30 +69,67 @@ export function charsetWidthById(id: number | undefined): number {
|
|
|
68
69
|
|
|
69
70
|
const cache = new Map<string, TextCodec | null>();
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
/**
|
|
73
|
+
* WHATWG windows-1252 code points for bytes 0x80–0x9F (the only range
|
|
74
|
+
* where it differs from Latin-1). Node's TextDecoder cannot be trusted
|
|
75
|
+
* here: through at least Node 20 the 'windows-1252' label is routed
|
|
76
|
+
* through a latin1 fast path, decoding this range as C1 controls, so
|
|
77
|
+
* the WIN1252 table is built from this fixed spec table instead of the
|
|
78
|
+
* runtime decoder. Later Node majors agree with this table exactly.
|
|
79
|
+
*/
|
|
80
|
+
const WIN1252_C1 = Object.freeze([
|
|
81
|
+
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
|
|
82
|
+
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
|
|
83
|
+
0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
|
84
|
+
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
function buildByteToCodeTable(name: string, label: string): Uint16Array {
|
|
88
|
+
const toCode = new Uint16Array(256);
|
|
89
|
+
if (name === 'WIN1252') {
|
|
90
|
+
// Latin-1 identity outside 0x80–0x9F; spec table inside. Needs no
|
|
91
|
+
// ICU support at all, so WIN1252 works even on small-icu builds.
|
|
92
|
+
for (let i = 0; i < 256; i++) {
|
|
93
|
+
toCode[i] = (i & 0xE0) === 0x80 ? WIN1252_C1[i - 0x80] : i;
|
|
94
|
+
}
|
|
95
|
+
return toCode;
|
|
75
96
|
}
|
|
76
97
|
let decoder: TextDecoder;
|
|
77
98
|
try {
|
|
78
99
|
decoder = new TextDecoder(label);
|
|
79
100
|
} catch {
|
|
80
|
-
//
|
|
101
|
+
// Falling through to DEFAULT_ENCODING (UTF-8) would silently write
|
|
102
|
+
// different bytes from the explicitly requested Firebird codepage.
|
|
103
|
+
// Official Node builds include these ICU tables; constrained builds
|
|
104
|
+
// must fail clearly instead of corrupting text.
|
|
105
|
+
throw new Error(
|
|
106
|
+
`The requested Firebird encoding ${name} requires the ${label} ICU codec, ` +
|
|
107
|
+
'but this Node.js runtime does not provide it. Use an official full-ICU ' +
|
|
108
|
+
'Node.js build, or connect with encoding NONE and pass explicitly encoded Buffer values.'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const one = Buffer.alloc(1);
|
|
112
|
+
for (let i = 0; i < 256; i++) {
|
|
113
|
+
one[0] = i;
|
|
114
|
+
toCode[i] = decoder.decode(one).charCodeAt(0);
|
|
115
|
+
}
|
|
116
|
+
return toCode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildCodec(name: string): TextCodec | null {
|
|
120
|
+
const label = ICU_LABELS[name];
|
|
121
|
+
if (!label) {
|
|
81
122
|
return null;
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
// Build both directions from
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
const toCode =
|
|
125
|
+
// Build both directions from one byte→code table — every byte of a
|
|
126
|
+
// single-byte codepage maps to exactly one BMP character (undefined
|
|
127
|
+
// bytes decode to U+FFFD, which is kept for decoding but never used
|
|
128
|
+
// for the reverse map).
|
|
129
|
+
const toCode = buildByteToCodeTable(name, label);
|
|
89
130
|
const toByte = new Map<string, number>();
|
|
90
|
-
const one = Buffer.alloc(1);
|
|
91
131
|
for (let i = 0; i < 256; i++) {
|
|
92
|
-
|
|
93
|
-
const ch = decoder.decode(one);
|
|
94
|
-
toCode[i] = ch.charCodeAt(0);
|
|
132
|
+
const ch = String.fromCharCode(toCode[i]);
|
|
95
133
|
if (ch !== '�' && !toByte.has(ch)) {
|
|
96
134
|
toByte.set(ch, i);
|
|
97
135
|
}
|
|
@@ -130,8 +168,10 @@ function buildCodec(name: string): TextCodec | null {
|
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
/**
|
|
133
|
-
* Codec for a Firebird charset name, or null when the charset is unknown
|
|
134
|
-
* natively handled by Buffer
|
|
171
|
+
* Codec for a Firebird charset name, or null when the charset is unknown or
|
|
172
|
+
* natively handled by Buffer. A known codepage whose ICU table is unavailable
|
|
173
|
+
* throws instead of silently falling back to UTF-8. Successful and unknown
|
|
174
|
+
* lookups are cached; failures are not.
|
|
135
175
|
*/
|
|
136
176
|
export function getCodec(charsetName: string | undefined): TextCodec | null {
|
|
137
177
|
if (!charsetName) {
|
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,
|
package/src/wire/serialize.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/wire/xsqlvar.ts
CHANGED
|
@@ -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
|
|
@@ -29,13 +67,13 @@ const EMPTY_BUFFER = Buffer.alloc(0);
|
|
|
29
67
|
* We must decode raw bytes with the matching Node.js encoding so that
|
|
30
68
|
* characters outside ASCII are reproduced correctly.
|
|
31
69
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
70
|
+
* Other recognized single-byte character sets are handled by the ICU-backed
|
|
71
|
+
* codec path. Only unknown character-set names fall back to the
|
|
72
|
+
* connection-level DEFAULT_ENCODING, typically UTF-8.
|
|
34
73
|
*/
|
|
35
74
|
const FirebirdToNodeEncoding: Readonly<Record<string, string>> = Object.freeze({
|
|
36
75
|
UTF8: 'utf8',
|
|
37
76
|
UNICODE_FSS: 'utf8',
|
|
38
|
-
WIN1252: 'latin1',
|
|
39
77
|
ISO8859_1: 'latin1',
|
|
40
78
|
LATIN1: 'latin1',
|
|
41
79
|
ASCII: 'ascii',
|
|
@@ -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
|
-
|
|
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 (
|
|
562
|
-
ret =
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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;
|