Add FULL OUTER JOIN support#38340
Merged
Merged
Conversation
Implement translation of the new LINQ `Queryable.FullJoin` operator to SQL `FULL JOIN`, mirroring the existing RightJoin support but making BOTH the outer and inner sides nullable. Core: - Register `QueryableMethods.FullJoin` (single 6-param overload with an optional `IEqualityComparer<TKey>`) and dispatch it in the navigation-expansion and query-translation visitors. A non-null custom comparer is rejected as untranslatable, matching Left/RightJoin. - Never parameterize `IEqualityComparer<>`/`IComparer<>` operator arguments in the funcletizer (treat them like `[NotParameterized]`): a comparer is structural, not a value, so it must be evaluated as a constant. This lets the translator detect (and reject) a non-null FullJoin comparer instead of seeing a query parameter. Relational: - Add `FullJoinExpression`, `SelectExpression.AddFullJoin`, `JoinType.FullJoin` (both sides nullable, never prunable/to-one) and `QuerySqlGenerator.VisitFullJoin`. - Fix a latent correctness bug shared with RightJoin: when the outer side becomes nullable, an outer `WHERE` predicate must be pushed into a subquery before the join, otherwise it is emitted post-join and incorrectly filters out the unmatched inner rows. Providers: InMemory and Cosmos report FullJoin as untranslatable. Bump the SDK in global.json to a preview.5 build containing the FullJoin operator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d4376cc to
e4cd707
Compare
There was a problem hiding this comment.
Pull request overview
Adds translation support for the new Queryable.FullJoin LINQ operator, mirroring the existing LeftJoin/RightJoin infrastructure. Introduces a new FullJoinExpression, wires it through the query pipeline, and provides provider-specific handling for Relational (translated), InMemory and Cosmos (reported as untranslatable). Also fixes a latent RIGHT JOIN correctness bug where an outer-side predicate was incorrectly applied as a post-join WHERE, which would drop unmatched rows once the outer became nullable.
Changes:
- New
FullJoinExpressionandAddFullJoin/TranslateFullJoin/VisitFullJoinplumbing acrossSelectExpression,QuerySqlGenerator, and the relational/in-memory/cosmos translators, plusQueryableMethods.FullJoinregistration and dispatch inNavigationExpandingExpressionVisitor/QueryableMethodTranslatingExpressionVisitor. ExpressionTreeFuncletizernow refuses to parameterizeIEqualityComparer<>arguments, so the compiler-supplied defaultnullonFullJoinis preserved as a constant and translation can distinguish a translatable from a non-translatable comparer.SelectExpression.AddJoinnow pushes the outer into a subquery when it has aPredicateforRightJoin/FullJoin, fixing a latent RIGHT JOIN bug; SqlServer/Sqlite baselines and one tightenedUpdate_with_RightJoinassertion are updated accordingly, plus newNorthwindJoinQueryTestBasetests cover full join and the right-join regression.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore/Query/QueryableMethods.cs | Registers the Queryable.FullJoin MethodInfo (6-arg overload). |
| src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs | Dispatches FullJoin (null-comparer only) and adds abstract TranslateFullJoin. |
| src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs | Handles FullJoin in navigation expansion; rebuilds the call with trailing null comparer and marks both shapers nullable. |
| src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs | Excludes IEqualityComparer<> parameters from parameterization. |
| src/EFCore/EFCore.baseline.json | API baseline additions for FullJoin/TranslateFullJoin. |
| src/EFCore.Relational/Query/SqlExpressions/FullJoinExpression.cs | New FullJoinExpression mirroring RightJoinExpression. |
| src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs | Adds JoinType.FullJoin, both-sides nullable handling, identifier nullability, and the outer-predicate pushdown fix for RIGHT/FULL JOIN. |
| src/EFCore.Relational/Query/QuerySqlGenerator.cs | Emits FULL JOIN SQL via VisitFullJoin. |
| src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs | Implements relational TranslateFullJoin. |
| src/EFCore.Relational/EFCore.Relational.baseline.json | API baseline additions for the new expression and AddFullJoin/VisitFullJoin/TranslateFullJoin members. |
| src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs | Returns null (untranslatable) for FullJoin. |
| src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs | Reports FullJoin as untranslatable with the cross-document error. |
| test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs | New FullJoin, unmatched-both-sides, custom-comparer, and RightJoin_with_filtered_outer tests. |
| test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs | Tightens Update_with_RightJoin assertion to verify null outer entries for unmatched rows. |
| test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs | Overrides new tests with AssertTranslationFailed. |
| test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs | SqlServer SQL baselines for the new join tests. |
| test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs | Updated SqlServer SQL baselines reflecting the outer-predicate pushdown. |
| test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs | Updated Sqlite SQL baselines reflecting the outer-predicate pushdown. |
| global.json | Bumps SDK to a preview that contains the FullJoin operator. |
Copilot's findings
- Files reviewed: 19/19 changed files
- Comments generated: 0
AndriySvyryd
approved these changes
Jun 1, 2026
Contributor
API review baseline changes for
|
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.
Fixes #37633.
Adds translation support for the new
Queryable.FullJoinLINQ operator (dotnet/runtime#124787), mirroring the existingLeftJoin/RightJoinsupport added in #35451.Summary
QueryableMethods.FullJoinand dispatch it through the query pipeline (NavigationExpandingExpressionVisitor,QueryableMethodTranslatingExpressionVisitor).FullJoinExpressionSQL expression, withSelectExpressionhandling (both sides nullable),QuerySqlGenerator.VisitFullJoin, andAddFullJoinAPIs.TranslateFullJoin; InMemory and Cosmos report the operator as untranslatable.Notes
LeftJoin/RightJoin(which expose separate no-comparer and comparer overloads),FullJoinhas a single overload with an optional trailingIEqualityComparer<TKey>. Every call therefore emits a 6-arg expression with acomparerargument (compiler-suppliednullwhen omitted). We only translate the null-comparer case; a custom comparer produces a translation failure.nullwould normally be parameterized — hiding from the translator whether a (non-translatable) custom comparer was supplied.IsParameterParameterizablenow excludesIEqualityComparer<>arguments from parameterization, treating them like[NotParameterized]. This is the first single-overload-optional-comparer operator EF translates.Latent RIGHT JOIN correctness fix (discovered while adding FULL JOIN)
SelectExpression.AddJoinpreviously left an outerPredicateas a post-joinWHEREfor nullable-outer joins. Once the outer side becomes nullable (RIGHT/FULL JOIN), a post-joinWHEREon an outer column wrongly drops the unmatched rows (where the outer is NULL), collapsing the join toward an inner join. The outer is now pushed into a subquery (so its predicate is applied before the join) forRightJoin/FullJoinwhen it has a predicate.This is the same invariant already handled for the inner side of LEFT JOIN; it was simply never applied to the nullable outer side. The fix corrects a pre-existing, latent RIGHT JOIN bug as well as FULL JOIN.
Consequences:
NorthwindBulkUpdatesRIGHT JOIN tests had their behavior corrected.Update_with_RightJoin's verification now correctly sees the unmatched right-side rows as null outer entities; its assertion was tightened to assert that shape explicitly (all 8 F-customers returned, 6 unmatched/null, 2 matched and updated).Delete_with_RightJoinis unchanged at runtime (the deleted target is the nullable side, so affected-row counts are identical) — only its SQL baseline changed.Tests
New
NorthwindJoinQuerytests (basic full join, unmatched rows on both sides, custom-comparer translation failure, plus a directRightJoin_with_filtered_outerregression test) with provider overrides. Full SqlServer + Sqlite Query and BulkUpdates suites pass.