Skip to content

A discrete range read as [x,infinity) loses 9999-12-31; UpperBound disagrees between memory and server #117

Open

Description

@CaffeinatedCoder

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

  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/rangesRange unions, variants and the shape engines
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    package/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
    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. DifferentialFuzzParityTests compares UpperBound() with upper(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.md EF 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.

  4. added
    found-by/oracleFound by the generated-oracle machinery (claims × API surfaces)
    package/coreAffects CodoMetis.ValueRanges
    package/nodatimeAffects CodoMetis.ValueRanges.NodaTime
    area/literalsParse, TryParse and formatting
    on Oct 17, 2026
  5. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    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 the candidates.json rows {DateRange, LocalDateRange, both multiranges} infinity bound → #117. I added the literal path to the description, because a fix only in FromProvider would leave these rows failing.

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/literalsParse, TryParse and formattingarea/rangesRange unions, variants and the shape enginesbreakingChanges an answer or a signature — needs a majorbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/coreAffects CodoMetis.ValueRangespackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTimepackage/nodatimeAffects CodoMetis.ValueRanges.NodaTimeseverity/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