You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rewrite comma-join / CROSS JOIN carrying a WHERE equi-predicate into an INNER JOIN at parse time, so FROM a, b WHERE a.k = b.k rides the equi-join seek/hash path instead of the O(L×R) nested loop.
-**`Selection`, aggregates, window functions, set ops, CASE, OFFSET/FETCH** → [`query.md`](docs/claude/query.md).
140
-
-**JOIN / APPLY** — INNER / LEFT / RIGHT / FULL / CROSS + CROSS/OUTER APPLY, ANSI-89 comma-FROM, EF `LeftJoin`/`RightJoin` routing (no LINQ `FullJoin`, so FULL is raw-SQL-only); `JoinDriver` equi-join hash fast path + `SqlValueKey` keying, nested-loop fallback, RIGHT/FULL materialization + derived-table-right, `JoinDiagnostics` strategy guard → [`joins.md`](docs/claude/joins.md).
140
+
-**JOIN / APPLY** — INNER / LEFT / RIGHT / FULL / CROSS + CROSS/OUTER APPLY, ANSI-89 comma-FROM, EF `LeftJoin`/`RightJoin` routing (no LINQ `FullJoin`, so FULL is raw-SQL-only); `JoinDriver` equi-join hash fast path + `SqlValueKey` keying, nested-loop fallback, parse-time comma/`CROSS JOIN`+WHERE → equi-`INNER` rewrite, RIGHT/FULL materialization + derived-table-right, `JoinDiagnostics` strategy guard → [`joins.md`](docs/claude/joins.md).
141
141
-**`PIVOT` / `UNPIVOT`** table operators — PIVOT desugars to grouped conditional aggregation (implicit grouping = all inner columns except FOR + aggregate-arg), UNPIVOT is a NULL-skipping unfold; both attach as a postfix FROM-source wrapper and ride the derived-table `LateralPlan` seam → [`pivot.md`](docs/claude/pivot.md).
if(windows.Count>0&&(aggregates.Count>0||fromClause.GroupingSets.Count>0||fromClause.Havingis not null))
114
114
thrownewNotSupportedException("Combining window functions with GROUP BY / HAVING / aggregates in the same SELECT isn't modeled. EF Core 10 doesn't emit this shape.");
115
+
116
+
// Convert a comma-join / CROSS JOIN carrying an equi-join predicate in
117
+
// WHERE into an INNER JOIN, so it rides the equi-join seek / hash path
118
+
// instead of the O(L×R) nested loop. Value-independent, so it's done
119
+
// once here and the rewritten array is captured in the cached plan.
Copy file name to clipboardExpand all lines: docs/claude/joins.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,6 +8,12 @@ INNER / bare `JOIN` (= INNER) / LEFT [OUTER] / RIGHT [OUTER] / FULL [OUTER] / CR
8
8
9
9
**Quirk — back-reference across a comma silently succeeds** (e.g. `FROM a, b JOIN c ON c.id = a.id`): real SQL Server binds `b JOIN c ON …` as its own scope and raises Msg 4104 because `a` isn't visible there; the simulator doesn't do parse-time scope-checking on ON predicates (column refs resolve at runtime through `ResolveAcrossTuple` across all FROM sources in the tuple), so the query runs and returns the Cartesian-filtered rowset. The common shapes — basic `FROM a, b WHERE …`, multi-comma chains, comma + derived table, explicit JOIN followed by comma — all match real SQL Server byte-for-byte; only this rare back-reference-across-comma case diverges, toward "more permissive" rather than wrong rowset.
10
10
11
+
### Cross→Inner equi-join rewrite
12
+
13
+
A bare `JoinKind.Cross` (from either a comma or an explicit `CROSS JOIN`) carries no ON predicate, so `ApplyJoin` never computes an equi-plan for it — it would always fall to the O(L×R) nested loop, even when WHERE carries a clean `a.k = b.k`. `RewriteCommaJoinsToEquiJoins` (run once at parse time, inside `BuildSqlProjection`, so the rewritten `joins[]` is captured in the cached plan) closes that gap: for each Cross level with no ON and a re-enumerable (non-lateral) right side, it scans the WHERE conjuncts for equi-keys connecting a prior source to that level (the same `TryExtractEquiKey` classifier the explicit-JOIN equi-plan uses), synthesizes them into an ON via `BooleanExpression.And`, and flips the level to `JoinKind.Inner`. It then rides `EquiJoinSeekOrHash` / `HashEquiJoin` exactly like an explicitly-written `INNER JOIN … ON`.
14
+
15
+
`FROM a, b WHERE a.k = b.k` ≡ `a INNER JOIN b ON a.k = b.k` — the textbook inner-join ON/WHERE identity. **Correctness is anchored by the residual invariant**: every pulled conjunct *stays* in the WHERE excluders (never removed), so flipping Cross→Inner can only drop rows the WHERE would drop anyway. The post-WHERE result is therefore provably unchanged regardless of outer joins elsewhere in the chain — converting a `Cross` level mixed with a preceding LEFT JOIN's null-extended side, or feeding a later LEFT JOIN, leaves the surrounding outer-join semantics intact (the rewrite only ever touches Cross levels, never one that already has an ON). Only equi-keys are pulled; a non-equi WHERE term (`b.id > 10`) stays a post-join filter, so the synthesized ON's residual count is 0. A derived-table right side after a comma keeps its `LateralPlan`, so it's skipped (can't be seeked/hashed anyway) and stays on the nested loop.
16
+
11
17
## JoinDriver
12
18
13
19
`JoinDriver` is a fold over `joins[]`: the leftmost rowset is wrapped with each join's operator in turn to produce the final enumerator. `ApplyJoin` picks the operator per join level — and is the single point where the strategy (hash vs nested loop) is decided.
0 commit comments