Skip to content

v2.16.1

Choose a tag to compare

@mariuz mariuz released this 20 Sep 08:24
· 37 commits to master since this release

Patch release: correctness fixes for fixed-point and INT128 values, on both the parameter and the result side. Negative INT128 parameters now work at all, batch numeric parameters keep every digit the metadata declares, and scales past 15 no longer decode as NaN. No API changes; drop-in upgrade from 2.16.0.

Fixes

  • Scaled numeric batch parameters keep their precision (#433). The Firebird 4+ batch API requires every message to match the statement's described input format exactly, so the driver — not the server — converts each value to the integer coefficient the column declares. That conversion went through parseFloat and Math.round(n * 10^-scale), which was wrong in four separate ways: exact decimal strings were forced through binary floating point and lost digits; decimal ties rounded down because of the binary representation (2.675 at scale 2 stored 2.67); negative ties rounded asymmetrically; and malformed numeric strings were partially accepted, with parseFloat silently reading 1oops as 1. Values are now parsed into an exact scaled integer coefficient without passing strings or BigInts through Number, and discarded digits round to nearest with ties away from zero, matching Firebird's own conversion of decimal parameter text.
    • 2.675 at scale 2 stores 2.68, -2.675 stores -2.68, 1.00105 at scale 4 stores 1.0011.
    • Exact strings beyond the safe integer range, such as '900719925474.0992', retain every digit.
    • NUMERIC/DECIMAL, BIGINT and INT128 batch parameters accept number, decimal string and bigint; use a string or BigInt when the value is outside JavaScript's safe integer range.
  • Batch numeric values are range-checked before any packet is sent. SMALLINT, INTEGER, INT64 and INT128 parameters are validated against the physical signed range of the target width, so an out-of-range or malformed value fails with a column N (FIELD_NAME) message and leaves the connection usable, instead of writing a truncated or wrapped value onto the wire.
  • Batch numerics are validated before the BLOB pre-pass. executeBatch uploads Buffer/string BLOB values as transaction blobs before building the messages. Validating numerics only at encoding time meant a batch that could never be sent had already written blob state into the transaction. The check now runs first, so nothing is uploaded for a batch that will be rejected.
  • Negative INT128 parameters are encoded correctly. The high word of the 128-bit value was written with writeBigUInt64BE, which cannot represent the negative high word of a negative value. It is now written as signed two's complement, so negative BigInt parameters round-trip through both the batch path and ordinary query/execute.
  • Negative INT128 results are decoded correctly. The default (LOSSY) numeric mode read the coefficient through the unsigned reader, so every negative INT128 came back as a huge positive number or string. It now uses the signed reader in every numeric mode, as SAFE and STRING already did.
  • Unsafe negative INT128 results stay exact. With the signed reader in place, the unsafe-magnitude test in the lossy path — which checked only the upper bound — became reachable from below, so a large negative fell through to Number() and lost precision while its positive twin was returned as an exact string. The check is now two-sided, matching the exact-mode decoder.
    • Before: -1.7014118346046923e+38. After: '-170141183460469231731687303715884105728', symmetric with the positive extreme.
  • Scales beyond 15 no longer decode as NaN. Decoding applied the scale through a lookup table of divisors that stopped at 10^15; any larger scale indexed past the end of the array and produced NaN. That is reachable for INT64-backed columns (NUMERIC(18,18)) and routine for INT128, whose scale runs to 38. The table is replaced by a helper built on Math.pow(10, n), which returns the identical double for every exponent the table did cover.
    • NUMERIC(38,18) coefficient 12345 decodes as 1.2345e-14 instead of NaN.

Behaviour changes

All three are bug fixes, but they change observable behaviour and are worth reading before upgrading:

  • Batch fixed-point parameters now reject booleans, Date objects and plain objects, which previously coerced through Number() — usually to a value nobody intended. Pass a number, decimal string or BigInt.
  • A malformed numeric string in a batch now throws instead of being partially parsed. Code that relied on '12abc' inserting 12 will see an error.
  • Positive scales now multiply rather than divide when decoding. A Firebird coefficient represents value * 10^scale, which the exact-mode decoder and the string formatter already assumed; the lossy table path divided by 10^|scale| and so was off by 10^(2*scale). This affects only the rare positive-scale columns produced by dialect 1 and some legacy metadata.

Documentation

The README's numericMode section previously described the INT128 lossy path only as "a mixed number/string path" whose result type "can depend on the Firebird wire type and value" — wording that was accurate only because the behaviour was asymmetric. It now states the rule directly: a coefficient inside the inclusive safe range decodes to a scaled number, one outside it in either direction to an exact scaled string, with a worked example at both INT128 extrema. A new paragraph records that every declared scale is supported, including NUMERIC(18,18) on INT64 and the 38 digits an INT128 column can declare, and that positive scales multiply. The batch section documents the accepted input forms and the precision limitation of unsafe JavaScript numbers.

Tests

  • Unit coverage for the new coefficient converter: canonical-decimal rounding with ties away from zero, exact string and BigInt inputs, positive scales, signed-zero normalisation, syntax and finiteness validation, exponent bounds, and the signed range of all four widths including the INT128 extrema.
  • INT128 extrema formatted symmetrically in lossy mode; scales of 18 and 38 decoding to finite numbers on both INT64 and INT128; positive scales multiplying across SQLVarInt, SQLVarInt64 and SQLVarInt128.
  • Firebird 4+ integration tests compare batch results against ordinary op_execute — the reference for Firebird's own decimal conversion — and cover exact unsafe scaled strings, INT64 minima and maxima, signed INT128 round trips through both the regular and batch paths, and the BLOB/numeric validation ordering (asserting that no blob is uploaded when a numeric is invalid).
  • Full unit suite: 795 tests across 58 files.

Upgrading

Drop-in replacement for 2.16.0 unless you were passing booleans, Dates or malformed numeric strings as batch fixed-point parameters, or reading positive-scale columns. Applications that worked around negative INT128 values — by casting to VARCHAR, by switching to numericMode: 'string', or by flipping the sign client-side — can drop those workarounds.

Thanks to @Gobbo89 for #433, which carried the batch conversion rewrite and the signed INT128 wire handling.