Skip to content

fix: declare lower() and upper() NULL for a non-null range (10.x, port of #349) - #367

Merged
CaffeinatedCoder merged 2 commits into
release/10.xfrom
fix/354-bound-null-propagation-10.x
Oct 24, 2026
Merged

CaffeinatedCoder merged 2 commits into
release/10.xfrom
fix/354-bound-null-propagation-10.x

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #354

The 10.x port of #349 for 10.2.0 (#358): git cherry-pick -x of #352's commit (2c44472) and #353's fix commit (89a7a38), both merged into main. Against release/10.x the closing keyword closes nothing, so #354 is closed by hand after the merge.

Problem

10.1.0 declares lower() and upper() with argumentsPropagateNullability: [true], so EF Core takes a bound as NULL only when its column is. PostgreSQL also returns NULL for the empty range and for an unbounded side, as the in-memory LowerBound() and UpperBound() return null. Over a required column:

  • LowerBound() == null became WHERE FALSE;
  • != null and HasValue filtered nothing;
  • ?? fallback lost its COALESCE, so open-ended rows fell out of a filter and a projection threw.

On a nullable column, the bound was tested with the column's own IS NULL. This affects every range type and RangeSet.

Change

Tests

Local, SDK 10.0.402, Docker:

  • Full suite, EF Core 10.0.0 floor, postgres:16-alpine: all eight projects pass (core 1397, NodaTime 238, EF 305, EF NodaTime 91, ASP.NET Core 92, conventions 406, integration 282, infinity-disabled 4).
  • ExecutedQueryTests on postgres:18-alpine: 15/15.
  • EF Core 10.0.13 with Npgsql 10.0.3 (-p:EfFloat=10.0.13 -p:NpgsqlEfFloat=10.0.3, versions confirmed in deps.json): translation tests 305/305, ExecutedQueryTests 15/15.
  • Fix reverted on this branch ([true]): the bound sweep fails on 234 rows (117 range, 117 RangeSet), none a known candidate, and both translation pins fail. Restored afterwards.
  • pr-hygiene.sh on a simulated event with silent: "Labels and issue link are in order", so the [10.2.0] ### Changed answers row links 10.x: LowerBound() and UpperBound() in a query: == null becomes WHERE FALSE and ?? loses its COALESCE (port of #349) #354.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 2 commits October 24, 2026 13:44
Two sweeps in ExecutedQueryTests now end in Candidates.Verdict, compared with
LINQ over the rows as loaded. BoundsComparedWithNull runs LowerBound() and
UpperBound() against null, HasValue, ?? in a filter and a projection, and a
filter on a projected bound over nine shapes in thirteen range columns; all 117
(operation, column) pairs disagree today, one candidate per operation under
nullable DateRange column holding NULL and every shape, one line per row, so
only the NULL row is quarantined; one candidate per operation under #350.

E70 quotes the bound-null sentence of docs/efcore.md, held by ExecutedQueryTests.

With argumentsPropagateNullability: [false] in BoundFunction applied locally,
the nine #349 candidates report candidate fixed and nothing else fails; the fix
is a separate PR.

Closes #351

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2c44472)
BoundFunction declared lower() and upper() with argumentsPropagateNullability
[true], which tells EF Core the bound is NULL only when the column is. PostgreSQL
also returns NULL for the empty range and for an unbounded side, so EF Core folded
LowerBound() == null on a required column to WHERE FALSE, dropped the COALESCE of
a ??, and tested a nullable column's bound with the column's own IS NULL. The flag
is now [false] for both, on ranges and on RangeSet.

The bound sweep in ExecutedQueryTests now runs every operation on the RangeSet
column of each family too (13 + 13 columns), and its nine #349 candidates are
removed. With the old flag it fails on 234 rows, 117 per kind; with the new one
every row agrees. Two translation tests pin the SQL without a database. Claim E70
covers RangeSet's bounds and quotes the efcore.md sentence that now promises the
query answer.

Found-By: US
Defect-Class: SQL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 89a7a38)
@CaffeinatedCoder CaffeinatedCoder added this to the 10.2.0 milestone Oct 24, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working silent Answers wrongly without throwing — fixes go in the next major, never deferred breaking Changes an answer or a signature — needs a major 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/high Wrong results, crash or failed writes in a realistic scenario labels Oct 24, 2026
@CaffeinatedCoder
CaffeinatedCoder merged commit 7581ab7 into release/10.x Oct 24, 2026
10 of 12 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/354-bound-null-propagation-10.x branch October 24, 2026 11:49
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 breaking Changes an answer or a signature — needs a major bug Something isn't working package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL package/efcore-nodatime Affects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime severity/high Wrong results, crash or failed writes in a realistic scenario 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