Skip to content

Decimal text beyond 28 significant digits is rounded silently when a literal or JSON array is read #115

Open

Description

@CaffeinatedCoder

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[]);     -- 2

A 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

  1. added this to the 10.0.0 milestone on Oct 17, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    breakingChanges an answer or a signature — needs a major
    area/jsonSystem.Text.Json and TypeConverter wire forms
    area/literalsParse, TryParse and formatting
    package/coreAffects CodoMetis.ValueRanges
    severity/lowCosmetic, message or documentation-level difference
    on Oct 17, 2026
  3. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Oracle check (main at 3ecfa10): no oracle on main catches this (pending). Verified by a run: every class in oracles.json passes 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, the DecimalSet literal, the JSON array) against numeric_in. Claim J06 (decimal grammar) covers 1e3, 1,5 and $1.5, but not precision. Route B step 3 is the sweep expected to cover it.

  4. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    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 on RangeSet<DecimalRange> and DecimalSet. Verified against postgres:17 on main + #135, where the defect is still present. The proof is the candidates.json rows {type} beyond 28 digits → #115.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/jsonSystem.Text.Json and TypeConverter wire formsarea/literalsParse, TryParse and formattingbreakingChanges an answer or a signature — needs a majorbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/coreAffects CodoMetis.ValueRangesseverity/lowCosmetic, message or documentation-level differencesilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions