Skip to content

IsAdjacentTo at the date extremes: memory says adjacent, PostgreSQL does not (MinValue/MaxValue are ±infinity there) #110

Closed

Description

@CaffeinatedCoder

At the extremes of a date domain, IsAdjacentTo answers true in memory and false in PostgreSQL. Npgsql writes DateOnly.MinValue / MaxValue (and NodaTime's LocalDate.MinIsoValue / MaxIsoValue) as -infinity / infinity. On the server no finite date is next to -infinity or infinity, so a range bounded at an extreme is not adjacent to the range one day further in.

var first = DateRange.CreateFinite(DateOnly.MinValue, DateOnly.MinValue);        // [0001-01-01,0001-01-01]
var rest  = DateRange.CreateUnboundedEnd(new DateOnly(1, 1, 2));                  // [0001-01-02,)
first.IsAdjacentTo(rest);                                                          // true

var last  = DateRange.CreateUnboundedEnd(DateOnly.MaxValue);                       // [9999-12-31,)
DateRange.CreateFinite(new DateOnly(9999, 12, 30), new DateOnly(9999, 12, 30)).IsAdjacentTo(last);  // true
-- what EF Core writes for the two pairs above
SELECT daterange('-infinity','-infinity','[]') -|- '[0001-01-02,)'::daterange,   -- f
       '[9999-12-30,9999-12-31)'::daterange -|- '[infinity,)'::daterange;         -- f
SELECT daterange('-infinity','-infinity','[]');   -- [-infinity,-infinity], not empty

Confirmed against PostgreSQL 17 on main (7f4dc47). Through EF Core, column, constant and parameter, the query answers false and memory answers true.

Scope. DateRange.IsAdjacentTo, LocalDateRange.IsAdjacentTo, and RangeSet<…>.IsAdjacentTo(range | set) over both. The continuous temporal types are unaffected: [x,infinity) -|- [infinity,) is adjacent on the server, as it is in memory. Union and Merge read back equal through EF Core, because the reader turns -infinity back into MinValue and canonicalizes again. On the server they still hold a different value ({(,-infinity],[0001-01-02,)} where memory has (,)), so a server-side comparison with that result also disagrees.

Not the documented boundary. docs/architecture.md#domain-width-and-the-boundary covers the integer sentinels. docs/efcore.md says a MaxValue bound becomes infinity and that "the shape checks behave consistently". Neither says adjacency diverges at the extremes.

Fix (10.0.0). Either document the row in the domain-boundary table and pin it in DomainBoundaryDivergenceTests (a documentation change, and the sweep then sets these rows aside as it does the integer boundary), or make the two sides agree, for example by not stepping across a date extreme in the discrete adjacency check. The second changes an in-memory answer, so it is breaking. Remove the DifferentialFuzzParityTests candidates for this family in the fix's PR.

Found by the differential fuzzing sweep (#104): 1 to 2 rows per run at the default 200 rows, about 30 at 2,000.

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
    found-by/oracleFound by the generated-oracle machinery (claims × API surfaces)
    area/rangesRange unions, variants and the shape engines
    on Oct 17, 2026
  3. added
    area/range-setRangeSet and multirange algebra
    package/nodatimeAffects CodoMetis.ValueRanges.NodaTime
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    package/coreAffects CodoMetis.ValueRanges
    severity/lowCosmetic, message or documentation-level difference
    on Oct 17, 2026
  4. CaffeinatedCoder commented on Oct 17, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Oracle check (main at 3ecfa10): would catch. DifferentialFuzzParityTests (Ranges_AgreeWithPostgres) fails on rows DateRange IsAdjacentTo at the domain edge and DateRange> IsAdjacentTo(set) at the domain edge , e.g. [0001-01-02,) ⋄ (,0001-01-01] → memory=true, EF=false, PostgreSQL=false.
    Verified by a run on 3ecfa10. At the default seed, 2 lines report SKIPPED [known candidate, #110] (rows 106 and 142). It was found by this sweep, which samples the extremes, so only 1–2 rows per run hit it.

  5. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Resolved in #169 (fix/date-extremes-adjacency-divergence, stacked on #168, 10.0.0) by the first of the two offered fixes: the row is documented as a domain boundary and pinned, and the in-memory answer stays. Reason: with Npgsql.DisableDateTimeInfinityConversions set the extremes reach the server as dates and -|- is true there, exactly as in memory (InfinityConversionsDisabledTests.AtTheDateExtremes_AdjacencyAgrees_BecauseTheExtremesStayDates), so a model that stopped stepping across an extreme would disagree with that configuration instead. The divergence is a property of Npgsql's infinity mapping, not of the algebra; nothing in src/ changes, and the breaking label is removed.

    What is now true: docs/architecture.md carries the date rows in the boundary table, docs/efcore.md and docs/ranges.md say it in the consumer's voice (claim E44), DomainBoundaryDivergenceTests pins it as a literal, a parameter and through EF Core for DateRange (both extremes) and LocalDateRange (MaxIsoValue), and DifferentialFuzzParityTests sets an adjacency row at a date extreme aside only when it disagrees in exactly the documented shape (memory true, EF and PostgreSQL false), counting it. The two candidates are removed; without the set-aside the sweep reports them again.

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/range-setRangeSet and multirange algebraarea/rangesRange unions, variants and the shape enginesbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/coreAffects CodoMetis.ValueRangespackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/nodatimeAffects CodoMetis.ValueRanges.NodaTimeseverity/lowCosmetic, message or documentation-level differencesilentAnswers 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