Repository navigation
Decimal text beyond 28 significant digits is rounded silently when a literal or JSON array is read #115
Description
Decimal text with more than 28 significant digits is rounded to decimal precision when a literal, a set literal or a JSON array is read, so the value in memory is a different number from the text, and from what PostgreSQL holds for the same text. Measured on main (7f4dc47) against PostgreSQL 17.11:
var r = DecimalRange.Parse("[1.00000000000000000000000000001,2)", null);
r.ToString(); // [1.0000000000000000000000000000,2)
r.Contains(1m); // true
DecimalSet.Parse("{1.00000000000000000000000000001,1}", null).Count; // 1
JsonSerializer.Deserialize<DecimalSet>("[1.00000000000000000000000000001,1]"); // {1.0000000000000000000000000000}SELECT '[1.00000000000000000000000000001,2)'::numrange @> 1::numeric; -- f
SELECT cardinality('{1.00000000000000000000000000001,1}'::numeric[]); -- 2A magnitude that doesn't fit (1e30) throws OverflowException, which is right; only excess precision is folded in silently. That is decimal.Parse's own behaviour (DecimalRange.ParseValue, DecimalSet.ParseValue, ValueSetElementJsonConverter's decimal leg) and the library adds no round-trip check.
Authority. The column path is loud: Npgsql throws OverflowException: Numeric value does not fit in a System.Decimal reading the same numrange, numeric or numeric[] value, which is what docs/architecture.md promises for a value that can't be materialized. The text paths — a JSON document, an API payload, a configuration literal — are the only ones that round.
Fix (10.0.0): refuse a decimal element whose text doesn't round-trip (format the parsed value and compare, or count significant digits) with a FormatException, on Parse/TryParse, the set literal and the JSON element read. Rejects input that parses today → 10.0.0 under the release rule.
Tests: one row per entry point in TemporalLiteralStrictnessContractTests' sibling for numerics, or a new DecimalPrecisionContractTests; a seed is the check removed.
Found in the 10.0.0 bug hunt (code-analyst reading, reproduced here). Severity low: 29-significant-digit inputs are rare, and the fix only turns a rounding into an error.
Activity
CaffeinatedCoder commentedon Oct 17, 2026
Oracle check (main at 3ecfa10): no oracle on main catches this (pending). Verified by a run: every class in
oracles.jsonpasses on 3ecfa10 with the defect present, and no candidate is registered for it.Gap: in
axes.md(Literals and JSON), the Literal corpus axis has no more than 28 significant digits value crossed with Entry point (DecimalRange.Parse/TryParse, theDecimalSetliteral, the JSON array) againstnumeric_in. Claim J06 (decimal grammar) covers1e3,1,5and$1.5, but not precision. Route B step 3 is the sweep expected to cover it.CaffeinatedCoder commentedon Oct 17, 2026
Would be caught today by
LiteralCorpusProbeTests(#135). This is retroactive: the 10.0.0 bug hunt found it first. Row:DecimalRange beyond 28 digits [1.00000000000000000000000000001,). The parse legs agree, but the EF write-back does not: the SQL constant and the JSON document both carry[1.0000000000000000000000000000,), while PostgreSQL reads[1.00000000000000000000000000001,)from the same text. Also caught onRangeSet<DecimalRange>andDecimalSet. Verified against postgres:17 on main + #135, where the defect is still present. The proof is thecandidates.jsonrows{type} beyond 28 digits→ #115.test: literal corpus × entry point against PostgreSQL (route B, step 3)