Skip to content

fix: keep a comparer out of SQL by what it compares, not its declared type (#452, 10.x) - #454

Merged
CaffeinatedCoder merged 3 commits into
release/10.xfrom
fix/452-comparer-by-operand-10.x
Oct 25, 2026
Merged

CaffeinatedCoder merged 3 commits into
release/10.xfrom
fix/452-comparer-by-operand-10.x

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #452

Cherry-pick (-x) of #453 into release/10.x, for 10.3.0. Closes doesn't act on a release branch, so #452 is closed by hand once both PRs merge.

Problem

#385 keeps a range comparer's Compare out of SQL, but it decided by the parameter type Compare declares. A comparer declared over IRange<T>, the non-generic System.Collections.IComparer (including Comparer<DateRange>.Create(…) held as one) and any comparer of value sets declare a type RangeTypeRegistry doesn't hold, so EF Core still read the call as the natural order. With p = [2026-03-01,2026-03-10], Where(r => cmp.Compare(r.Period, p) == 0) became WHERE r."Period" = @p and returned one row where memory returns three. The changelog row, docs/efcore.md#what-runs-where and the migration guide all promise that every IComparer of ranges or sets fails translation.

Change

Tests

  • ClientSideOnlyTranslationTests: Where and OrderBy rows for an IComparer<IRange<DateOnly>>, a non-generic IComparer and an IComparer<StringSet>.
  • LowerBoundComparerQueryTests (live PostgreSQL 16; reverted and proven on main, fix: keep a comparer out of SQL by what it compares, not its declared type (#452) #453): the same comparers, plus Comparer<DateRange>.Create as IComparer and a comparer class of our own. Each fails translation in Where and OrderBy with RangeComparerCall.RunInMemory in the message. In a final Select each gives memory's answer; for value sets the test adds a Tags column and compares by count against {z}.
  • Reverted: with the old declared-type check put back, the six new translation rows fail, the live filter query returns rows instead of throwing, and the projection reads the NULL row's Compare from SQL. That proves the tests catch the bug.
  • A trap the tests avoid: a comparer held in a static readonly field reaches EF as a constant, and EF never translates Compare on a constant, so those rows fail with or without the fix. The comparers in both tests are captured locals, so they become query parameters, which is the case the review reproduced.
  • On .NET 10 SDK 10.0.402: EF unit tests (342), the conventions tests (420), and TranslationSupportProbeTests, CapturedCollectionProbeTests, ClientSideOnlyProjectionTests and LowerBoundComparerQueryTests live all pass. The seed lower-bound-comparer-runs-as-sql.patch still applies.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 2 commits October 25, 2026 20:00
… type (#452)

#385 rewrote comparer.Compare(x, y) into RangeComparerCall.RunInMemory only when the
declared parameter type was a registered range or range-set type. A comparer declared
over IRange<T>, the non-generic IComparer (Comparer<T>.Create held as one) and a
comparer of value sets declared some other type, so EF Core still read the call as
the natural order: Where(cmp.Compare(r.Period, p) == 0) became WHERE "Period" = @p
and selected one row where memory selects three.

The rewriter now looks at the operands under their conversions to the declared type
(and a sealed variant through its union), and RunInMemory gains an overload for the
non-generic IComparer. The test comparers are captured locals: EF Core leaves Compare
on a constant comparer untranslated in any case, so a static field would pass with
or without the fix.

Found-By: pre-release sweep of release/10.x (differential lens)
Defect-Class: declared-type dispatch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 42ffccb)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 10.3.0 milestone Oct 25, 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/value-sets Value set families and wrapper arities package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL severity/low Cosmetic, message or documentation-level difference labels Oct 25, 2026
@CaffeinatedCoder
CaffeinatedCoder merged commit 8bb4314 into release/10.x Oct 25, 2026
9 of 10 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/452-comparer-by-operand-10.x branch October 25, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ranges Range unions, variants and the shape engines area/value-sets Value set families and wrapper arities bug Something isn't working package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL 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