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
Joined-source UPDATE / DELETE FROM: extends the EF7+ ExecuteUpdate / ExecuteDelete path to support multi-source FROM clauses with INNER/LEFT/CROSS JOIN and CROSS/OUTER APPLY by reusing the SELECT-side join driver. Target source is identified by matching the leading identifier against each FromSource.Qualifier (Msg 208 if unbound); each unique target row is processed exactly once — heap (page, slot) is recovered via a byte[]→address side-channel map populated by a wrapper around the target source's row enumerator. Selection.ParseSourcesAndJoins / FindSourceColumn / EnumerateJoinedRows promoted to internal. OUTPUT in alias-form multi-source raises NotSupportedException (EF Core 10 doesn't combine those).
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -254,7 +254,8 @@ Identity counters and the database-scoped rowversion counter bypass the log —
254
254
255
255
### UPDATE / DELETE
256
256
-`UPDATE table SET col = expr [, col = expr]* [WHERE pred]` and `DELETE [FROM] table [WHERE pred]`.
257
-
- Multi-table-syntax form (`UPDATE alias SET alias.col = expr FROM table AS alias [WHERE pred]`, `DELETE FROM alias FROM table AS alias [WHERE pred]`) is the EF7+ `ExecuteUpdate` / `ExecuteDelete` shape. Single-source-only — additional sources or joins on the FROM clause raise `NotSupportedException`. Two-pass parsing: collect raw `(columnName, expr)` pairs without resolving ordinals, then bind to the FROM-clause table once known. SET LHS supports both bare `col = expr` and alias-qualified `[a].[col] = expr`; the alias prefix is accepted verbatim and not cross-checked against the FROM-clause's alias since the simulator's row resolvers use `name.Leaf` (the alias is moot for single-source). OUTPUT is only supported on the single-table form (EF doesn't combine OUTPUT with multi-table-syntax) — see `Simulation.Update.cs` / `Simulation.Delete.cs` for the deferred-table-binding pattern.
257
+
- Multi-table-syntax form (`UPDATE alias SET alias.col = expr FROM <sources> [WHERE pred]`, `DELETE FROM alias FROM <sources> [WHERE pred]`) is the EF7+ `ExecuteUpdate` / `ExecuteDelete` shape. Both single-source (`FROM table AS alias`) and joined-source forms (`FROM t AS alias INNER|LEFT|CROSS JOIN u AS b ON ...`, plus `CROSS APPLY` / `OUTER APPLY`) are supported by routing through the SELECT-side `Selection.ParseSourcesAndJoins` + `Selection.EnumerateJoinedRows` machinery. The target source is identified by matching the leading-identifier (alias OR table name) against each source's `FromSource.Qualifier`; missing match → **Msg 208** `"Invalid object name 'X'."` (probe-confirmed against SQL Server 2025; distinct from Msg 4104's multi-part-identifier wording). Two-pass parsing: collect raw `(columnName, expr)` pairs without resolving ordinals, then bind to the target table once known. SET LHS supports both bare `col = expr` and alias-qualified `[a].[col] = expr`; the alias prefix is accepted verbatim. SET RHS can reference any source's columns, qualified or not — resolution goes through `Selection.FindSourceColumn` against the multi-source list.
258
+
- **Joined UPDATE / DELETE: each unique target row processed exactly once.** When the same target row matches multiple join tuples (e.g. a customer with two qualifying orders), SQL Server applies the SET / DELETE once per unique target — using the *first* matching tuple's RHS values for SET (heap-scan order, probe-confirmed). The simulator dedupes by `(page, slot)` of the target heap row, recovered via a side-channel byte[]→address map. A wrapper around the target source's `Rows` enumerator records each yielded byte[]'s address into the map as the join driver consumes it; the dedup lookup is reference-equality fast and works whether the target is on the outer or inner side of a join (for inner-side targets, the wrapper repopulates the map on each restart, since the simulator's heap row enumerators allocate fresh byte[] per yield). LEFT JOIN with no right-side match still surfaces the target tuple (probe C); RHS sees NULL for the unmatched-source columns. OUTPUT clause is supported only when the leading identifier resolves to a real table name up-front; OUTPUT alongside an alias-form multi-source UPDATE / DELETE raises `NotSupportedException` (EF Core 10 doesn't combine those, and supporting it would require deferring OUTPUT parsing past the FROM-target binding).
258
259
-**Multi-column SET evaluates RHS against the pre-update row snapshot** — verified: `UPDATE t SET a = 100, b = a + 1` over `(a=10, b=20)` produces `(a=100, b=11)` (b read pre-update a). Scalar subquery RHS sees the pre-update table state.
-`OUTPUT INTO @table_var`, `OUTPUT DELETED.*` / `INSERTED.*` star expansion. Per-column `OUTPUT INSERTED.<col>` / `OUTPUT DELETED.<col>`*is* supported (UPDATE / DELETE both); only the star-expansion form is missing. `OUTPUT INTO` (sending the projection to a table variable rather than the result set) isn't.
317
-
- Joined-source UPDATE / DELETE FROM clauses (`UPDATE a SET ... FROM t AS a JOIN u AS b ON ...`). The single-source alias form (`UPDATE a SET ... FROM t AS a [WHERE ...]`, `DELETE FROM a FROM t AS a [WHERE ...]`) IS supported — that's what EF7+ `ExecuteUpdate` / `ExecuteDelete` emit, verified against real SQL Server 2025. Adding sources beyond the single aliased target raises `NotSupportedException` so the gap is visible.
318
318
- MERGE source subqueries; MERGE target column refs in `ON`; `WHEN MATCHED` UPDATE/DELETE branches; `$action`. EF Core's batched-update path emits semicolon-separated `UPDATE … OUTPUT …` statements rather than `MERGE WHEN MATCHED`, so SaveChanges fidelity doesn't require it.
319
319
- Msg 8141 (inline CHECK referencing a peer column — SQL Server rejects at CREATE TABLE; simulator allows).
320
320
- Msg 8133 (CASE where every branch is bare `NULL`; simulator returns NULL of `int`).
0 commit comments