Repository navigation
fix: keep a comparer out of SQL by what it compares, not its declared type (#452) - #453
Merged
Merged
Conversation
… 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>
This was referenced Oct 25, 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 #452
Problem
#385 keeps a range comparer's
Compareout of SQL, but it decided by the parameter typeComparedeclares. A comparer declared overIRange<T>, the non-genericSystem.Collections.IComparer(includingComparer<DateRange>.Create(…)held as one) and any comparer of value sets declare a typeRangeTypeRegistrydoesn't hold, so EF Core still read the call as the natural order. Withp = [2026-03-01,2026-03-10],Where(r => cmp.Compare(r.Period, p) == 0)becameWHERE r."Period" = @pand returned one row where memory returns three. The changelog row,docs/efcore.md#what-runs-whereand the migration guide all promise that everyIComparerof ranges or sets fails translation.Change
ValueRangesQueryExpressionInterceptor.RangeComparerRewriter: rewrites when either operand is a range, range set or value set. It checks the operand itself and each conversion on its way to the declared type, and reaches a sealed variant through its union. The comparer must still implementIComparer<TDeclared>or, forCompare(object, object), the non-genericIComparer.RangeComparerCall: aRunInMemory(IComparer, object?, object?)overload with the same name, so EF's error message still showsRangeComparerCall.RunInMemory.docs/efcore.md(What runs where) anddocs/migration.md(MoveRangeLowerBoundComparerout of queries) name the three kinds. Claim E31 says the operands decide.CHANGELOG.mdrow already says "any otherIComparerof ranges or sets", so it is unchanged here. The 10.x cherry-pick links this issue from 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396's row, aspr-hygiene.shrequires for asilentPR into a minor.Tests
ClientSideOnlyTranslationTests:WhereandOrderByrows for anIComparer<IRange<DateOnly>>, a non-genericIComparerand anIComparer<StringSet>.LowerBoundComparerQueryTests(live PostgreSQL 16): the same comparers, plusComparer<DateRange>.CreateasIComparerand a comparer class of our own. Each fails translation inWhereandOrderBywithRangeComparerCall.RunInMemoryin the message. In a finalSelecteach gives memory's answer; for value sets the test adds aTagscolumn and compares by count against{z}.Comparefrom SQL. That proves the tests catch the bug.static readonlyfield reaches EF as a constant, and EF never translatesCompareon 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.TranslationSupportProbeTests,CapturedCollectionProbeTests,ClientSideOnlyProjectionTestsandLowerBoundComparerQueryTestslive all pass. The seedlower-bound-comparer-runs-as-sql.patchstill applies.🤖 Generated with Claude Code