Repository navigation
An inverted range literal parses as empty on every entry point; PostgreSQL rejects it #114
Description
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 = emptySELECT '[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
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 inverted bounds value crossed with Entry point (Parse, TryParse, JSON, TypeConverter,RangeSet.Parse) againstrange_in. NeitherRangeLiteralGrammarContractTestsnorLiteralGrammarParityTestshas an inverted literal. No claim says Parse refuses whatrange_inrefuses. R01 states the empty-on-inverted rule only forCreateFinite, and thedocs/serialization.mdsentence ("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.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:Int32Range inverted [2,1), libraryempty, PostgreSQL refuses. All seven entry points agree with each other onempty(Parse/TryParseover 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 thecandidates.jsonrows{type} inverted→ #114, which fail as "candidate fixed" once this lands.test: literal corpus × entry point against PostgreSQL (route B, step 3)
CaffeinatedCoder commentedon Oct 18, 2026
Fixed in #171.
RangeFormatrefuses a finite literal whose lower bound is greater than its upper bound, comparing the bounds as written, before discrete closing. That coversParse,TryParse(false), JSON, theTypeConverter,RangeSet.Parseand the EF JSON document reader. Equal bounds that aren't both inclusive stayempty, andCreateFinitekeeps its rule. The 20invertedcandidates are gone, claim J35 and the seedinverted-literal-reads-empty.patchare added, andLiteralGrammarParityTestsasks PostgreSQL about[5,1)and{[5,1),[7,9]}.