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
MERGE source bare-table form — USING tbl [AS] alias [WITH (hints)] ships alongside the existing parenthesized USING (VALUES … / SELECT …) AS alias shape. Simulation.Merge.cs:ParseMergeSource forks on the first post-USING token: ( routes to the existing parenthesized parser (carved out as ParseParenthesizedMergeSource for clarity, behavior unchanged); anything else routes to the new ParseBareTableMergeSource, which calls BatchContext.ParseObjectName(context, acceptTableVariable: true), resolves through TryResolveView then TryResolveTable (same precedence as FROM-source), and builds the materializer accordingly — Selection.ForView(resolvedView).Execute(...) for views; heapTable.Heap.EnumerateRows() + DecodeFullRow + EvaluateComputedColumns for heap tables (regular / temp # / table-variable @t all flow through this path uniformly via TryResolveTable). Source schema is the resolved table's full Columns list (including computed) with column names exposed for ON-predicate / SET / INSERT-projection name resolution through the existing sourceColumnNames / sourceSchema arrays. Alias is optional and defaults to the table's leaf name when absent (probe-confirmed: USING src ON src.id = tgt.id works, references go via the bare table name). Selection.ConsumeOptionalAlias promoted from private to internal so both the FROM-source heap-table path and the new MERGE bare-table path can share it. Optional WITH (hint [, …]) table hints sit alias-then-hint (same placement as FROM source); Selection.ParseOptionalTableHints gains a third parameter commitOnLegacyParen (default false to preserve existing FROM/JOIN-RHS behavior), which the MERGE bare-table call site passes true so the legacy bare-paren (NOLOCK) form commits to hint-clause interpretation on ( — matches real SQL Server's behavior: USING src AS s (nolock) works as a hint, USING src AS s (x, y) raises Msg 321 with "x is not a recognized table hints option" rather than falling through to a generic Msg 102 (probed against SQL Server 2025 on 2026-05-14). 12 new MergeTests cover: heap table with explicit alias (AS s), bare alias (no AS keyword), no alias (referenced via the table name), schema-qualified au.src, alias-then-WITH (NOLOCK) accepted as no-op, alias-then-legacy-bare-paren-(nolock) accepted, alias-then-(x, y) column-rename rejection (Msg 321 — verbatim probe wording), temp-table source, table-variable source, view source, missing-table (Msg 208), missing-table-variable (Msg 1087). CLAUDE.md MERGE grammar entry expanded to call out the bare-table source kinds and the per-source-form column-rename rule; "MERGE source hints aren't parsed" gap removed from query-hints; new note documents the remaining FROM-source (unknown) commit-on-paren divergence (Msg 102 instead of Msg 207 / Msg 321) which the new flag doesn't yet propagate back to FROM-source callers. docs/claude/dml.md MERGE syntax grammar updated; docs/claude/query-hints.md site table now lists MERGE source (bare-table) and its hint position, with explanatory text replacing the previous "not modeled" note.
Copy file name to clipboardExpand all lines: CLAUDE.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -118,7 +118,7 @@ Three entry points share one per-connection undo log: implicit (statement-level
118
118
119
119
### MERGE / OUTPUT
120
120
-`INSERT ... OUTPUT INSERTED.<col>` (single-row).
121
-
-`MERGE INTO target [AS alias] USING (<source>) AS alias [(cols)] ON predicate <when-clause>+ [OUTPUT …]` — source can be `VALUES`, `SELECT`, or a set-op chain. WHEN clauses: `WHEN MATCHED [AND <cond>] THEN UPDATE SET …` / `DELETE`, `WHEN NOT MATCHED [BY TARGET] [AND <cond>] THEN INSERT (…) VALUES (…)`, `WHEN NOT MATCHED BY SOURCE [AND <cond>] THEN UPDATE SET …` / `DELETE`. Multiple AND-conditioned clauses per family allowed; unconditional must be last (Msg 5324). `WHEN NOT MATCHED [BY TARGET]` admits at most one clause (Msg 10714). Trailing `;` required (Msg 10713). `$action` in OUTPUT projects uppercase `INSERT` / `UPDATE` / `DELETE` per affected row. Multi-match: a target row matched by more than one source row raises Msg 8672 only when the chosen action is UPDATE (DELETE is forgiving — multiple matches collapse to one delete). Triggers fire INSERT → UPDATE → DELETE, each kind once with its combined affected rows. See [`docs/claude/dml.md`](docs/claude/dml.md).
121
+
- `MERGE INTO target [WITH (hints)] [AS alias] USING <source> [AS alias] [(cols)] ON predicate <when-clause>+ [OUTPUT …]` — source can be `(VALUES …)`, `(SELECT …)`, a set-op chain, or a bare-table / view / temp-table / table-variable / schema-qualified reference (the bare-table form ships independently of the parenthesized form; alias is optional and defaults to the table's leaf name; trailing column-rename list `(c1, c2)` isn't legal on the bare-table form and parses as a hint clause — Msg 321 matches real SQL Server). WHEN clauses: `WHEN MATCHED [AND <cond>] THEN UPDATE SET …` / `DELETE`, `WHEN NOT MATCHED [BY TARGET] [AND <cond>] THEN INSERT (…) VALUES (…)`, `WHEN NOT MATCHED BY SOURCE [AND <cond>] THEN UPDATE SET …` / `DELETE`. Multiple AND-conditioned clauses per family allowed; unconditional must be last (Msg 5324). `WHEN NOT MATCHED [BY TARGET]` admits at most one clause (Msg 10714). Trailing `;` required (Msg 10713). `$action` in OUTPUT projects uppercase `INSERT` / `UPDATE` / `DELETE` per affected row. Multi-match: a target row matched by more than one source row raises Msg 8672 only when the chosen action is UPDATE (DELETE is forgiving — multiple matches collapse to one delete). Triggers fire INSERT → UPDATE → DELETE, each kind once with its combined affected rows. See [`docs/claude/dml.md`](docs/claude/dml.md).
122
122
123
123
### EF Core adapter coverage
124
124
`UseSqlServerSimulator(...)` covers seven SqlParameter-downcast pairs: `DateOnly→date`, `DateTime→date`, `DateTime→smalldatetime`, `TimeOnly→time(N)`, `TimeSpan→time(N)`, `decimal→money`, `decimal→smallmoney`. Without the adapter those mappings throw at SaveChanges. MAX-string family flows through plain `UseSqlServer`.
@@ -166,7 +166,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
166
166
-`LIKE` with `COLLATE` override (default collation only).
167
167
-`CONVERT` / `TRY_CONVERT` style codes other than `0` / `1` / `10` / `12` / `23` / `101` / `102` / `103` / `110` / `112` / `120` / `121` / `126` / `127` (datetime ⇄ string, both directions; ISO 8601 + regional formats) and `0` / `1` / `2` (money → string). See [`docs/claude/casting.md`](docs/claude/casting.md).
- MERGE source as a CTE-headed SELECT (`USING (WITH cte AS … SELECT …)`) — Selection.Parse's CTE entry doesn't reach the USING-clause grammar; wrap the CTE inside a regular SELECT instead. MERGE inside a CTE body, multi-statement MERGE WHEN-clause bodies, and `MERGE INTO <view>` (real SQL Server's updatable-view MERGE) are also deferred.
169
+
- MERGE source as a CTE-headed SELECT (`USING (WITH cte AS … SELECT …)`) — Selection.Parse's CTE entry doesn't reach the USING-clause grammar; wrap the CTE inside a regular SELECT instead. MERGE inside a CTE body, multi-statement MERGE WHEN-clause bodies, and `MERGE INTO <view>` (real SQL Server's updatable-view MERGE) are also deferred. (Bare-table USING ships as of 2026-05-14 — regular tables, views, temp-tables, table-variables, schema-qualified references, with alias-then-hint WITH (NOLOCK) support — and removes a previously-listed gap from the query-hints surface.)
170
170
-`UNIQUE` on a *non-persisted* computed column (`NotSupportedException`) — `PRIMARY KEY` / `UNIQUE` on a `PERSISTED` computed column ships (inline + table-level + ALTER ADD CONSTRAINT shapes; PK on non-persisted raises Msg 1711 at CREATE TABLE / Msg 8111 at ALTER ADD per probe). Non-persisted UNIQUE would need per-row expression evaluation in `EnforceKeyConstraints` / `EnforceUniqueIndexes` rather than storage-ordinal lookup. The orthogonal Msg 4936 determinism gate for PERSISTED computed columns themselves (rejecting GETDATE / NEWID etc.) isn't enforced either; non-deterministic PERSISTED computed columns silently materialize.
171
171
- Heap allocation tracking (flat page list, no IAM/PFS).
172
172
-**Table-variable named constraints / foreign keys** — `DECLARE @t TABLE (...)` shares its column-list parser with CREATE TABLE (see `docs/claude/table-variables.md`); column features (IDENTITY / UNIQUE / inline + table-level CHECK / computed columns / rowversion) all ship, alongside per-statement-atomic mutations and `OUTPUT … INTO <target>` for both `@t` and regular tables. Named constraints (`CONSTRAINT pk1 PRIMARY KEY`) and `FOREIGN KEY` raise Msg 102 (matches probe — real SQL Server's grammar disallows both shapes inside `DECLARE @t TABLE`). Multi-variable DECLARE with a table variable (`DECLARE @t1 TABLE (...), @t2 TABLE (...)`) and mixed scalar+table DECLARE also raise Msg 102/156. `SET IDENTITY_INSERT @t ON` likewise raises Msg 102 (probe-confirmed: there's no way to force a specific value into a table-variable identity column).
@@ -180,7 +180,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
180
180
- **Public `InfoMessage` event** — `SimulatedDbConnection.InfoMessage` ships as an `internal` event (with `internal SimulatedInfoMessageEventArgs` carrying `Message`, `LineNumber`, `Source`) and a `BatchContext`-side buffer that coalesces multiple `PRINT`s per command into one firing (joined with `\n`, line number = first contributing `PRINT`'s line). The internal surface is exercised through `SqlServerSimulator.Tests.Internal/PrintInfoMessageTests.cs`. Going *public* — exposing the event on the `SimulatedDbConnection` consumer surface for application use — is deferred pending the public-API shape decision (mirror SqlClient's `SqlInfoMessageEventArgs` exactly? trim to a minimal shape? add a `DbConnection`-compatible extension method instead of a typed event?). Subquery-in-operand still silently evaluates instead of raising Msg 1046 ("Subqueries are not allowed in this context"); non-string-value formatting routes through `SqlValue.CoerceTo(varchar(8000))` rather than SQL Server's PRINT-specific style 0 conventions (datetime → ISO instead of `"May 14 2026 12:00AM"`; money → `F4` instead of 2-decimal). The 8000-byte ANSI / 4000-character Unicode PRINT truncation isn't enforced — long strings pass through verbatim.
181
181
- **`ALTER TABLE` grammar beyond SET / ADD / DROP / ALTER COLUMN / CHECK / NOCHECK CONSTRAINT** — `SET (SYSTEM_VERSIONING = OFF)` (see [`docs/claude/temporal-tables.md`](docs/claude/temporal-tables.md)), `[WITH CHECK | WITH NOCHECK] ADD [CONSTRAINT name] (PRIMARY KEY | UNIQUE | FOREIGN KEY | CHECK | DEFAULT) …`, `DROP CONSTRAINT [IF EXISTS] name [, …]`, `[WITH CHECK | WITH NOCHECK] (CHECK | NOCHECK) CONSTRAINT (ALL | name [, …])` (trust toggling for bulk imports — sets / clears `is_disabled` + `is_not_trusted`), `ADD [COLUMN] col TYPE [, …]` (multi-column add — inline DEFAULT / IDENTITY / CHECK / FK / computed all supported; Msg 4901 on NOT NULL without DEFAULT on non-empty table; eager row rewrite with NULL backfill for nullable adds and constant-snapshot DEFAULT for NOT NULL), `DROP COLUMN [IF EXISTS] col [, …]` (Msg 4924 missing column, Msg 5074 dependency rejection with multi-blocker enumeration across PK/UQ/FK/CHECK/DEFAULT/index, atomic multi-drop), and `ALTER COLUMN col TYPE[(prec[,scale])] [COLLATE coll] [NULL|NOT NULL]` (single-column shape — type widening / narrowing routed through `SqlValue.CoerceTo` so Msg 220 / 245 / 241 / 8115 / 2628 surface verbatim; nullability flip raises Msg 515 on existing NULL; Msg 4928 for COMPUTED / rowversion; Msg 5074 multi-blocker enumeration across PK / UQ / FK both directions / computed-column dependencies / type-changing index references — length widening within the same `SqlType` family allowed under an index; COLLATE clause parse-accepted then ignored; identity / DEFAULT / inline CHECK preserve through the column instance swap) all ship (see [`docs/claude/alter-table.md`](docs/claude/alter-table.md)). DROP PERIOD FOR SYSTEM_TIME, REBUILD, SWITCH PARTITION, the SET-versioning-on direction, the `ALTER COLUMN col ADD/DROP {PERSISTED|MASKED|ROWGUIDCOL|SPARSE}` sub-clause forms, ALTER COLUMN on an identity column targeting a non-integer type, and every other shape raise `NotSupportedException`. Multi-constraint ADD (`ADD CONSTRAINT pk1 PK (a), CONSTRAINT fk1 FK …`) also raises.
182
182
-`hierarchyid`, `geography`, `geometry`.
183
-
- **Query-hint gaps** — table hints (`WITH (NOLOCK [, …])` on FROM sources / JOIN-RHS / INSERT / UPDATE / DELETE / MERGE targets) and statement-level `OPTION (…)` hints ship via parse-and-discard (see [`docs/claude/query-hints.md`](docs/claude/query-hints.md)). The legacy bare-paren `(hint)` form is FROM / JOIN-RHS only — INSERT treats `(` as the column-list opener (probe-confirmed Msg 207 on the would-be hint name), and UPDATE / DELETE / MERGE all raise Msg 102 on the bare-paren form (controlled via `allowLegacyParenForm: false` at those call sites). **MERGE target uses hint-then-alias placement** — opposite of FROM / UPDATE / DELETE — and alias-then-hint raises Msg 156 there (probe-confirmed). **INSERT / MERGE on a `@t` target rejects WITH outright** (Msg 156 on real SQL Server; the simulator falls through to its generic Msg 102 since hint parsing is skipped for `@t`). Closed accept-list raises `Msg 321` for unknown table hints (verbatim probe wording), `Msg 102` for unknown OPTION hints (probe-confirmed: SQL Server has no dedicated unknown-OPTION-hint code). `MAXRECURSION N` retains its runtime effect on in-scope CTE bindings; every other recognized hint is a pure no-op. Remaining gaps: (1) **MERGE source hints** (`USING t AS s WITH (NOLOCK)`) aren't parsed — the simulator only supports the parenthesized `USING (SELECT/VALUES …)` form, and probe-confirmed real SQL Server rejects WITH on that shape anyway (Msg 156); bare-table MERGE USING is its own deferred grammar; (2) **hint-conflict and DML-target-specific rejections** (`Msg 1047` for `NOLOCK + XLOCK`, `Msg 1065` for `NOLOCK` / `READUNCOMMITTED` on any DML target, `Msg 1069` for `INDEX(…)` on any DML target, `Msg 308` for unknown `INDEX(name)`) aren't enforced — the simulator has no lock state or index dispatch to conflict over. (The shape `FROM t NOLOCK` without parens is not a deprecated hint form — `nolock` / `readpast` / etc. aren't reserved keywords, so it parses as the bare-alias form `FROM t <alias>` on both the simulator and real SQL Server; identical behavior, no gap.)
183
+
- **Query-hint gaps** — table hints (`WITH (NOLOCK [, …])` on FROM sources / JOIN-RHS / INSERT / UPDATE / DELETE / MERGE targets) and statement-level `OPTION (…)` hints ship via parse-and-discard (see [`docs/claude/query-hints.md`](docs/claude/query-hints.md)). The legacy bare-paren `(hint)` form is FROM / JOIN-RHS only — INSERT treats `(` as the column-list opener (probe-confirmed Msg 207 on the would-be hint name), and UPDATE / DELETE / MERGE all raise Msg 102 on the bare-paren form (controlled via `allowLegacyParenForm: false` at those call sites). **MERGE target uses hint-then-alias placement** — opposite of FROM / UPDATE / DELETE — and alias-then-hint raises Msg 156 there (probe-confirmed). **INSERT / MERGE on a `@t` target rejects WITH outright** (Msg 156 on real SQL Server; the simulator falls through to its generic Msg 102 since hint parsing is skipped for `@t`). Closed accept-list raises `Msg 321` for unknown table hints (verbatim probe wording), `Msg 102` for unknown OPTION hints (probe-confirmed: SQL Server has no dedicated unknown-OPTION-hint code). `MAXRECURSION N` retains its runtime effect on in-scope CTE bindings; every other recognized hint is a pure no-op. Remaining gaps: **hint-conflict and DML-target-specific rejections** (`Msg 1047` for `NOLOCK + XLOCK`, `Msg 1065` for `NOLOCK` / `READUNCOMMITTED` on any DML target, `Msg 1069` for `INDEX(…)` on any DML target, `Msg 308` for unknown `INDEX(name)`) aren't enforced — the simulator has no lock state or index dispatch to conflict over. **FROM-source `(unknown)` without alias** diverges minorly: real SQL Server raises Msg 207 (parses the paren as a TVF-arg-list attempt) while the simulator's peek-and-restore falls through to Msg 102; with-alias FROM/JOIN-RHS `alias (unknown)` also raises Msg 102 instead of real SQL Server's Msg 321 (commit-on-paren is wired only on MERGE bare-table source, not on FROM-source). (The shape `FROM t NOLOCK` without parens is not a deprecated hint form — `nolock` / `readpast` / etc. aren't reserved keywords, so it parses as the bare-alias form `FROM t <alias>` on both the simulator and real SQL Server; identical behavior, no gap.)
184
184
185
185
## Quirks (modeled, not byte-identical to SQL Server)
0 commit comments