Repository navigation
fix: read an exclusive infinite date bound as memory loads it (#489) - #503
Merged
Merged
Conversation
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>
…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>
…he migration and changelog (#489) 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. The v10.x migration list and a section for it, the Breaking lines of 11.0.0-preview.1 in the root and both EF changelogs, claim E87 (and E06 following its reworded sentence), and the clone waiver for the definitions' new argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #489
Problem
PostgreSQL canonicalizes a finite discrete bound but leaves an infinite one as it is, so a
daterangecan hold[2026-01-01,infinity)or(-infinity,2026-01-01). Another tool can write these forms, and the server's own algebra produces them:(,) - (,-infinity]is(-infinity,), and(,) - [infinity,)is(,infinity)(measured on PostgreSQL 16.15 and 18.1). Memory loads them one day inside the extreme, as[2026-01-01,9999-12-30]and[0001-01-02,2025-12-31]. In a query:UpperBound()readupper(x) - 1, which is stillinfinity, so it returned 9999-12-31 where memory gives 9999-12-30.Clamp(DateOnly.MaxValue)did the same.LowerBound()readlower(x), which is-infinity, so it returned 0001-01-01 where memory gives 0001-01-02.Clamp(DateOnly.MinValue)did the same.LowerBoundInclusive()readlower_inc, which isfalsefor(-infinity, where memory's closed form starts inclusive. The issue didn't list this one; the sweep found it.RangeSet's bound readers use the same helpers, so they had the same faults.ClampFunctiononrelease/10.xreads the same helpers, and 10.4.0's changelog saysClamp"answers as memory does, at the extremes too", so #502 ports this into 10.4.0.Change
ExclusiveInfinity(new, internal) andIRangeTypeDefinition.ExclusiveInfinity. A discrete definition names the extremes Npgsql writes as infinities, and the bounds memory loads for an exclusive one.RangeTypeDefinitiontakes it only whileinfinityTextmaps the extremes, so it isnullwithNpgsql.DisableDateTimeInfinityConversionsset.DateRangegets both sides,(MinValue, 0001-01-02, MaxValue, 9999-12-30).LocalDateRangegets the upper side only, because memory starts(-infinityon -9998-01-02, which is below PostgreSQL's dates.ValueRangesMethodCallTranslator: the newLowerBoundFunctionand the changedUpperBoundFunctionwrap the bound inCASE WHEN bound = <extreme> AND NOT <inclusive>(x) THEN <inside> ELSE … END(InsideExclusiveInfinity).LowerBound(),UpperBound(),Clampand theRangeSetreaders all use these two.LowerInclusiveFunctionmirrorsUpperInclusiveFunctionon discrete types:NOT lower_inf(x) AND NOT isempty(x). Types with noExclusiveInfinity(integers,YearMonthRange, continuous types) produce the same SQL as before, apart fromLowerBoundInclusive()on a discrete type.LocalDateRangelower side and the disabled-conversions mode. The discrete-bounds note now namesLowerBoundInclusive(), and architecture.md's domain-boundary section explains the CASE. Migration from v10.x gets a bullet and the section Check date bounds at an exclusive infinity in queries. Each of the three changelogs (root, EF Core, EF Core NodaTime) gets a line under### Breakingin 11.0.0-preview.1.ExclusiveInfinityBoundParityTestsis registered as a differential oracle with two seeds, and there's a clone waiver for the new definition argument. Two seeds whose context lines moved are regenerated.It changes answers, so it's labelled
silent. On 10.x it ships in the 10.4.0 minor under### Changed answers(#502).Tests
ExclusiveInfinityBoundParityTests(new, integration). It writes each of the eight infinite forms by raw SQL and loads the row. Each reader then runs as a predicate,reader(r) == memory's answer, so it can't fall back to the client. Readers covered:DateRange: all seven, i.e. the four bound accessors andClampat the minimum, the maximum and a mid value.LocalDateRange: the same seven, with the documented lower-side exception.Exceptat the extremes.YearMonthRangecan't load these forms.InfinityConversionsDisabledTests: with the switch set, the SQL stays as it was, and a projected bound throws exactly as loading the row does.CASEforDateRangeandLocalDateRange(upper side only); noCASEforInt32RangeorYearMonthRange;LowerBoundInclusive()on a discrete type; plus updates to the tests that asserted the old SQL.dotnet run tools/oracles.cs seedscatches both new seeds (5 of 7 and 2 of 7 tests fail) and both regenerated ones.🤖 Generated with Claude Code