Skip to content

fix: read an exclusive infinite date bound as memory loads it (port of #489, #502) - #504

Merged
CaffeinatedCoder merged 3 commits into
release/10.xfrom
fix/502-exclusive-infinite-date-bounds-10.x
Oct 26, 2026
Merged

CaffeinatedCoder merged 3 commits into
release/10.xfrom
fix/502-exclusive-infinite-date-bounds-10.x

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #502

Problem

On release/10.x, LowerBound(), UpperBound(), LowerBoundInclusive() and Clamp read a date range's exclusive infinite bound as the extreme the database holds, one day wider than memory loads it (#489). On [2026-01-01,infinity), UpperBound() returned 9999-12-31 in a query and 9999-12-30 in memory. On (-infinity,2026-01-01), LowerBound() returned 0001-01-01 instead of 0001-01-02, and LowerBoundInclusive() returned false instead of true. #494 made Clamp on DateRange and LocalDateRange run as SQL in this release, and the 10.4.0 changelog says it "answers as memory does, at the extremes too". Without this port, 10.4.0 would ship a new disagreement next to that sentence.

Change

git cherry-pick -x of PR #503's three commits from main:

  • Fix: ExclusiveInfinity on the date definitions, and the CASE in the bound readers (InsideExclusiveInfinity). LowerBoundInclusive() on a discrete type becomes NOT lower_inf(x) AND NOT isempty(x). LocalDateRange reads the upper side only. With Npgsql.DisableDateTimeInfinityConversions set, the SQL is unchanged.
  • Tests and seeds: ExclusiveInfinityBoundParityTests (registered as an oracle with two seeds), the InfinityConversionsDisabledTests case, the translation tests, and two regenerated seeds.
  • Docs: Rows written by other tools, the discrete-bounds note, the domain-boundary section, claim E87 (and E06), and a clone waiver.
  • 10.x specifics (in the third commit): the changelog rows go under ### Changed answers in [10.4.0] in the root and both EF changelogs, linking 10.x: read an exclusive infinite date bound as memory loads it (port of #489) #502 and the migration section. The NodaTime EF changelog gains that section and its intro line. docs/migration.md adds Check date bounds at an exclusive infinity in queries under ## Changed answers in 10.4.0, next to 10.x: reject a collation on a string set column unless it compares bytes (port of #483) #496's section.

Tests

  • On release/10.x with the .NET 10 SDK: unit, conventions, ASP.NET Core and the EF no-database projects pass. Both integration projects pass on PostgreSQL 16 and 18 at the EF Core 10.0.0 floor, and on CI's newest-patch leg (EF Core 10.0.13, Npgsql EF 10.0.3).
  • With the translator change reverted on this branch, ExclusiveInfinityBoundParityTests fails for the exclusive-infinity rows, as it does on main.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits October 26, 2026 16:05
PostgreSQL canonicalizes a finite discrete bound but leaves an infinite one
as it is, so [2026-01-01,infinity) and (-infinity,2026-01-01) exist: another
tool writes them, and the server's algebra computes them ((,) - (,-infinity]
is (-infinity,), (,) - [infinity,) is (,infinity)). Memory loads them one day
inside, [2026-01-01,9999-12-30] and [0001-01-02,2025-12-31]. In a query,
upper(x) - 1 is still infinity and lower(x) is -infinity, so UpperBound(),
LowerBound() and Clamp at the extremes answered 9999-12-31 and 0001-01-01,
and LowerBoundInclusive() read lower_inc, which is false for (-infinity,
where memory's closed form starts inclusive. RangeSet's bound readers go
through the same helpers.

A definition now names the extremes Npgsql writes as infinities and the
bound memory loads for an exclusive one (ExclusiveInfinity), and the bound
readers test for the exclusive sentinel in a CASE. LowerBoundInclusive()
on a discrete type mirrors UpperBoundInclusive(): a finite lower bound
exists and the range isn't empty. On LocalDateRange only the upper side
applies: memory starts (-infinity on -9998-01-02, below PostgreSQL's dates.
With Npgsql.DisableDateTimeInfinityConversions set no extreme is an
infinity, the SQL stays as it was, and such a row fails to read in a query
as it fails to load. YearMonthRange can't load these rows (partial months).

ExclusiveInfinityBoundParityTests writes every infinite form by raw SQL and
runs each reader as a predicate against memory's answer for the loaded row,
on DateRange, LocalDateRange and both multiranges, plus the server-computed
Except; with the translator change reverted all five parity tests fail for
these rows. Measured on PostgreSQL 16.15 and 18.1.

Found-By: CA
Defect-Class: SQL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 54d34ea)
…or seeds (#489)

ExclusiveInfinityBoundParityTests is registered as a differential oracle with
two seeds: the CASE answering the extreme instead of the day inside it (5 of
7 tests fail), and LowerBoundInclusive() reading lower_inc again (2 of 7).
clamp-reads-the-stored-exclusive-upper-bound and range-isfinite-drops-isempty
had context lines the fix moved; regenerated, both still caught
(ClampParityTests, ClampDomainParityTests; ExecutedQueryTests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a1645b2)
…he migration and changelog (#502)

Rows written by other tools gains the bound readers and the two exceptions
(LocalDateRange's lower side, the infinity conversions disabled); the
discrete-bounds note names LowerBoundInclusive(); the domain-boundary
section says how the readers do it. Claim E87 (and E06 following its
reworded sentence), and the clone waiver for the definitions' new argument.

10.x specifics: the rows go under ### Changed answers in [10.4.0] in the
root and both EF changelogs, linking #502 and the migration section (the
NodaTime EF changelog gains the section and its intro line), and
docs/migration.md lists the section under ## Changed answers in 10.4.0,
beside #496's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e465aa1)
@CaffeinatedCoder CaffeinatedCoder added this to the 10.4.0 milestone Oct 26, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working silent Answers wrongly without throwing — fixes go in the next major, never deferred area/ranges Range unions, variants and the shape engines area/range-set RangeSet and multirange algebra package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL package/efcore-nodatime Affects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime severity/low Cosmetic, message or documentation-level difference labels Oct 26, 2026
@CaffeinatedCoder CaffeinatedCoder mentioned this pull request Oct 26, 2026
@CaffeinatedCoder
CaffeinatedCoder merged commit a958bef into release/10.x Oct 26, 2026
10 of 12 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/502-exclusive-infinite-date-bounds-10.x branch October 26, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/range-set RangeSet and multirange algebra area/ranges Range unions, variants and the shape engines bug Something isn't working package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL package/efcore-nodatime Affects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime severity/low Cosmetic, message or documentation-level difference silent Answers wrongly without throwing — fixes go in the next major, never deferred

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant