Repository navigation
A discrete range read as [x,infinity) loses 9999-12-31; UpperBound disagrees between memory and server #117
Description
A discrete range stored with an exclusive infinity upper bound — the form another tool writes with daterange('2024-01-01','infinity'), since [) is the constructor's default — loads one day short, and UpperBound() answers differently in memory and on the server. Measured on main (7f4dc47) against PostgreSQL 17.11:
stored: daterange('2024-01-01','infinity') → [2024-01-01,infinity) upper_inc = f
loads: DateRange [2024-01-01,9999-12-30] LocalDateRange [2024-01-01,9999-12-30]
UpperBound(): memory 9999-12-30 server 9999-12-31 (upper(x) - 1 is still infinity, which Npgsql reads as MaxValue)
UpperBoundInclusive(): memory true server true
PostgreSQL itself: daterange('2024-01-01','infinity') @> '9999-12-31'::date → t
The range the database holds contains 9999-12-31; the loaded value doesn't. Contains(DateOnly.MaxValue) agrees between memory and server (both false) only because the parameter goes out as infinity, which the exclusive bound excludes — so a query can't even ask the server the question PostgreSQL answers with t.
Cause. RangeProviderConversion.FromProvider calls CreateFinite(lower, DateOnly.MaxValue, true, false) for the infinity) bound, and the discrete canonicalization steps the exclusive upper bound back a day. The plugin's own writes are unaffected: a closed [x, 9999-12-31] goes out as [x,infinity] (inclusive) and reads back exactly; TemporalStoreParityTests holds that. Rows written by other tools in the [x,infinity) form are the gap. LocalDateRange behaves the same; YearMonthRange throws instead (loud).
Fix (10.0.0): in FromProvider, for a discrete type whose extreme Npgsql maps to infinity, read an exclusive upper bound at MaxValue as inclusive MaxValue (the special infinity lies beyond the element domain, so "everything below infinity" is "everything up to MaxValue"), and mirror it for -infinity on the lower side. Changes what a row loads as → breaking. The alternative is to document it under docs/efcore.md#rows-written-by-other-tools and keep the one-day loss.
Tests: an integration row inserted as daterange(x, 'infinity') (and '-infinity', and the LocalDateRange / tsrange / tstzrange forms) read through EF and compared with upper(x), upper_inc(x) and x @> max on the server.
Found in the 10.0.0 bug hunt (code-analyst reading, reproduced here). Narrow — only rows another writer produced in that form — hence low.
Also on the literal and JSON-document paths (added 2026-10-18, from LiteralCorpusProbeTests, #135). RangeFormat.Parse hands the same bounds to CreateFinite, so the column fix alone won't close this. Against postgres:17:
| Literal | Library (Parse, JSON, TypeConverter, EF JSON document) |
PostgreSQL |
|---|---|---|
[2026-01-01,infinity) |
[2026-01-01,9999-12-30] |
[2026-01-01,infinity) |
(-infinity,2026-01-01] |
[0001-01-02,2026-01-01]: loses 0001-01-01 and is no longer unbounded below |
(-infinity,2026-01-02) |
[-infinity,infinity) |
[0001-01-01,9999-12-30] |
[-infinity,infinity) |
LocalDateRange behaves the same, except that its lower-side form [-9998-01-02,…] is refused by the server when written back (loud). The same holds for both multiranges. Inclusive infinite bounds ([x,infinity], [-infinity,x)) and the continuous types agree.
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.DifferentialFuzzParityTestscomparesUpperBound()withupper(x), but only on values the plugin writes, and those write[x,infinity]inclusive. So it never reads the exclusive[x,infinity)form.Gap: this is the
axes.mdEF Core pair another writer's spelling × every read path (column, JSON document,ExecuteUpdate). Its claim,docs/efcore.md#rows-written-by-other-tools, has only E21, which covers arrays, so ranges have no claim. Route B steps 2 and 3 don't vary it, so it needs its own probe.CaffeinatedCoder commentedon Oct 17, 2026
Would be caught today by
LiteralCorpusProbeTests(#135) on the literal and EF JSON-document read paths. This is retroactive: the 10.0.0 bug hunt found it on the column path, which is not an entry point of this sweep. Row:DateRange infinity bound [2026-01-01,infinity). The library reads[2026-01-01,9999-12-30]through every entry point, and its SQL constant and JSON document read[2026-01-01,9999-12-31)on the server, which reads[2026-01-01,infinity). The lower-side mirror(-infinity,2026-01-01]→[0001-01-02,2026-01-01]fails too. Verified against postgres:17 on main + #135, where the defect is still present. The proof is thecandidates.jsonrows{DateRange, LocalDateRange, both multiranges} infinity bound→ #117. I added the literal path to the description, because a fix only inFromProviderwould leave these rows failing.test: infinity-bound and quoted-whitespace rows in the literal corpus…