Skip to content

An inverted range literal parses as empty on every entry point; PostgreSQL rejects it #114

Closed

Description

@CaffeinatedCoder

A range literal whose lower bound is greater than its upper bound parses as empty on every entry point, where PostgreSQL rejects the text. Measured on main (7f4dc47) against PostgreSQL 17.11:

DateRange.Parse("[2026-06-30,2026-01-01]", null);              // empty
Int32Range.Parse("[5,1)", null);                               // empty
Int32Range.TryParse("[5,1)", null, out var r);                 // true, r = empty
RangeSet<Int32Range, int>.Parse("{[5,1),[7,9]}", null);        // {[7,9]} — the inverted element vanishes
JsonSerializer.Deserialize<Int32Range>("\"[5,1)\"");           // empty
TypeDescriptor.GetConverter(typeof(Int32Range)).ConvertFromInvariantString("[5,1)"); // empty
// minimal API: GET /union?seats=[5,1)  → 200, seats = empty
SELECT '[5,1)'::int4range;                 -- ERROR: range lower bound must be less than or equal to range upper bound
SELECT '{[5,1),[7,9]}'::int4multirange;    -- the same error
SELECT '[2026-06-30,2026-01-01]'::daterange; -- the same error
SELECT '(5,5)'::numrange, '[5,5)'::numrange; -- empty | empty  (equal bounds, not both inclusive: empty on both sides, correctly)

Authority. docs/serialization.md#literals-parsing-and-json says the text is "the PostgreSQL literal, the same text the server itself reads and prints". The server refuses this text. docs/ranges.md#create-a-range documents the empty-on-inverted rule for CreateFinite() (claim R01) — a factory call with computed bounds — not for Parse, and LiteralGrammarParityTests has no inverted literal in its lists.

Cause. src/CodoMetis.ValueRanges/Internals/RangeFormat.cs hands the two parsed bounds to TRange.CreateFinite, which applies the factory's rule.

Consequence. A swapped period in a query string, a JSON body or a configuration value becomes "no period" instead of a 400 or a FormatException; a search over it matches nothing, silently. In a multirange literal the inverted element disappears from the set.

Fix (10.0.0): refuse lower > upper in RangeFormat.Parse with a FormatException on every entry point (Parse, TryParse → false, JSON, TypeConverter, and so RangeSet.Parse). Keep the factory rule unchanged, and keep equal bounds that are not both inclusive as empty, as PostgreSQL does. It rejects input that parses today, so it belongs in 10.0.0 under the release rule; docs/migration.md needs a v9.x line.

Tests: add [5,1) and {[5,1),[7,9]} (and a temporal inverted literal) to LiteralGrammarParityTests; a seed is the fix reverted.

Found in the 10.0.0 bug hunt (code-analyst reading, reproduced here). No oracle sweep found it.

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/mediumInconsistency or failure in a narrow scenario
    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 inverted bounds value crossed with Entry point (Parse, TryParse, JSON, TypeConverter, RangeSet.Parse) against range_in. Neither RangeLiteralGrammarContractTests nor LiteralGrammarParityTests has an inverted literal. No claim says Parse refuses what range_in refuses. R01 states the empty-on-inverted rule only for CreateFinite, and the docs/serialization.md sentence ("the same text the server itself reads") isn't quoted by any claim. Route B step 3 (literal corpus × entry point) 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: Int32Range inverted [2,1), library empty, PostgreSQL refuses. All seven entry points agree with each other on empty (Parse/TryParse over a string and a span, JSON, TypeConverter, EF JSON document), and the same happens for every inverted literal of all 10 range and 10 multirange types. Verified against postgres:17 on main + #135, where the defect is still present. The proof is the candidates.json rows {type} inverted → #114, which fail as "candidate fixed" once this lands.

  5. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed in #171. RangeFormat refuses a finite literal whose lower bound is greater than its upper bound, comparing the bounds as written, before discrete closing. That covers Parse, TryParse (false), JSON, the TypeConverter, RangeSet.Parse and the EF JSON document reader. Equal bounds that aren't both inclusive stay empty, and CreateFinite keeps its rule. The 20 inverted candidates are gone, claim J35 and the seed inverted-literal-reads-empty.patch are added, and LiteralGrammarParityTests asks PostgreSQL about [5,1) and {[5,1),[7,9]}.

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/mediumInconsistency or failure in a narrow scenariosilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions