Skip to content

Commit 8bb597d

Browse files
committed
Table-variable feature gaps closed: IDENTITY / UNIQUE / CHECK / computed / rowversion in @t unblocked by collapsing ParseDeclareTableVariable onto the CREATE TABLE column-list parser via a shared Simulation.ParseColumnList(isTableVariable) helper (flag gates the probe-confirmed @T-ONLY CONSTRAINT-named + REFERENCES rejections); statement-level atomicity via parallel per-statement BatchContext.CurrentTableVarUndoLog (RunMutation allocates fresh, rolls back on exception, drops on success — disjoint from the tx-scoped log so ROLLBACK TRAN still skips @t while mid-row failures roll partial multi-row writes back); OUTPUT INTO accepts regular tables alongside @t with undo-log routing by IsTableVariable and unfilled-column DEFAULT eval; side effect: fixes pre-existing CREATE TABLE bug where bare-nullable columns referenced by a table-level PK incorrectly raised Msg 8111 instead of promoting to NOT NULL.
1 parent b9583c3 commit 8bb597d

13 files changed

Lines changed: 670 additions & 441 deletions

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Five scopes, one home each. **Add new state to whichever class matches its true
6464
- **`Database`** (internal) = one database hosted by the server instance. `Schemas` (named-schema dict, pre-seeded with `dbo`), `CompatibilityLevel`, `VerboseTruncationWarnings`, `rowVersionCounter` (per-DB `@@DBTS`). Every `Simulation` ships with one entry named `Simulation.DefaultDatabaseName` (`"simulated"`); a future `USE <db>` adds entries to the dictionary.
6565
- **`Schema`** (internal) = one namespace inside a database. `HeapTables` (per-schema table dict), `Functions` (UDFs — abstract `UserDefinedFunction` keyed by name, runtime-typed as either `ScalarFunction` or `InlineTableValuedFunction`), `Views` (per-schema view dict), `Procedures` (per-schema stored-procedure dict). Future sequences / triggers land here too. Schema-qualified references (`SELECT * FROM audit.t`, `SELECT audit.fn(x)`, `FROM audit.tvf(x)`, `FROM audit.view1`, `EXEC audit.proc1`) route through `Database.Schemas["audit"]`; unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`).
6666
- **`SimulatedDbConnection`** = session. `CurrentDatabase` pointer, `CurrentTransaction`, `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `LastStatementRowCount` (`@@ROWCOUNT`), `LastErrorNumber` (`@@ERROR`), `NestingLevel` (UDF / proc / trigger / view recursion depth, capped at 32), `IdentityInsertTable`, `TraceFlags`, `IsVerboseTruncationActive()`, `TempTables` (per-session `#foo` dictionary, cleared on `Dispose`).
67-
- **`BatchContext`** (internal, in `Parser/`) = one command execution. Owns the `ParserContext` (parse-time-only scratch — `Token`, `AggregateCollector`, `WindowCollector`, `OuterTypeResolver`, `CteBindings`, `InDefaultClause`, `AllowsWindowExpressions`) and holds batch-lifetime runtime state: `Variables`, `TableVariables` (per-batch `DECLARE @t TABLE`–backed `HeapTable` dict, shared namespace with scalar `Variables` for the Msg-134 uniqueness check), `CurrentUndoLog`, `UdfFrame` (non-null when this batch is a scalar-UDF body being dispatched — gates value-form `RETURN` and lands the return value for the caller), `ProcFrame` (non-null when this batch is a stored-procedure body — same gate for value-form `RETURN`, plus a return-code slot the caller reads), plus the per-statement frame `CurrentStatement`. Exposes `TryResolveTable(MultiPartName)` — the routing rule that dispatches `#foo` leaves to `Connection.TempTables` regardless of qualifier and `@t` leaves to `TableVariables` (1-part only — `dbo.@t` returns false; the caller raises Msg 102 at parse via `acceptTableVariable` gating); everything else routes through the named schema (or `dbo` for an unqualified reference), with `SystemHeapTables` reachable only as a flat 1-part fallback. `TryResolveFunction(MultiPartName)` resolves 2-/3-part dotted names against the named schema's `Functions` dict; 1-part names return false (real SQL Server rejects bare UDF calls with Msg 195). `TryResolveProcedure(MultiPartName)` resolves through the same schema-lookup path but accepts 1-part names (probe-confirmed: `EXEC p1` finds `dbo.p1`). `TryResolveSchema(MultiPartName)` exposes the dict-bearing schema for CREATE / DROP / TRUNCATE / SELECT INTO. `ParseObjectName(ParserContext, bool acceptTableVariable = false)` parses the 1–4-segment dotted form, leaves cursor on the last name segment (standard parser contract), and compresses empty middle segments (so `tempdb..#foo` returns a 2-part name); the `acceptTableVariable` opt-in routes `@t` to a 1-part-with-`@`-prefix leaf for DML / FROM-source sites, and rejects `@t` everywhere else (so ALTER TABLE / DROP TABLE / TRUNCATE / CREATE / SELECT INTO surface Msg 102 matching probe). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`. Scalar-UDF and procedure invocation each allocate a child `BatchContext` via the corresponding body constructor: parameters pre-seed `Variables`, the matching frame is set, the body source text (captured at CREATE FUNCTION / CREATE PROCEDURE time) is re-tokenized through a synthesized `SimulatedDbCommand`, and the same dispatch loop runs the body. UDF bodies discard yielded result sets at the call site (Msg 444 territory); procedure bodies forward them through to the outer caller's iterator.
67+
- **`BatchContext`** (internal, in `Parser/`) = one command execution. Owns the `ParserContext` (parse-time-only scratch — `Token`, `AggregateCollector`, `WindowCollector`, `OuterTypeResolver`, `CteBindings`, `InDefaultClause`, `AllowsWindowExpressions`) and holds batch-lifetime runtime state: `Variables`, `TableVariables` (per-batch `DECLARE @t TABLE`–backed `HeapTable` dict, shared namespace with scalar `Variables` for the Msg-134 uniqueness check), `CurrentUndoLog` (per-tx/per-statement, regular tables), `CurrentTableVarUndoLog` (per-statement-only, `@t` writes — kept disjoint from the tx-scoped log so `ROLLBACK TRAN` skips `@t` while statement-atomic rollback covers it), `UdfFrame` (non-null when this batch is a scalar-UDF body being dispatched — gates value-form `RETURN` and lands the return value for the caller), `ProcFrame` (non-null when this batch is a stored-procedure body — same gate for value-form `RETURN`, plus a return-code slot the caller reads), plus the per-statement frame `CurrentStatement`. Exposes `TryResolveTable(MultiPartName)` — the routing rule that dispatches `#foo` leaves to `Connection.TempTables` regardless of qualifier and `@t` leaves to `TableVariables` (1-part only — `dbo.@t` returns false; the caller raises Msg 102 at parse via `acceptTableVariable` gating); everything else routes through the named schema (or `dbo` for an unqualified reference), with `SystemHeapTables` reachable only as a flat 1-part fallback. `TryResolveFunction(MultiPartName)` resolves 2-/3-part dotted names against the named schema's `Functions` dict; 1-part names return false (real SQL Server rejects bare UDF calls with Msg 195). `TryResolveProcedure(MultiPartName)` resolves through the same schema-lookup path but accepts 1-part names (probe-confirmed: `EXEC p1` finds `dbo.p1`). `TryResolveSchema(MultiPartName)` exposes the dict-bearing schema for CREATE / DROP / TRUNCATE / SELECT INTO. `ParseObjectName(ParserContext, bool acceptTableVariable = false)` parses the 1–4-segment dotted form, leaves cursor on the last name segment (standard parser contract), and compresses empty middle segments (so `tempdb..#foo` returns a 2-part name); the `acceptTableVariable` opt-in routes `@t` to a 1-part-with-`@`-prefix leaf for DML / FROM-source sites, and rejects `@t` everywhere else (so ALTER TABLE / DROP TABLE / TRUNCATE / CREATE / SELECT INTO surface Msg 102 matching probe). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`. Scalar-UDF and procedure invocation each allocate a child `BatchContext` via the corresponding body constructor: parameters pre-seed `Variables`, the matching frame is set, the body source text (captured at CREATE FUNCTION / CREATE PROCEDURE time) is re-tokenized through a synthesized `SimulatedDbCommand`, and the same dispatch loop runs the body. UDF bodies discard yielded result sets at the call site (Msg 444 territory); procedure bodies forward them through to the outer caller's iterator.
6868
- **`StatementContext`** (internal, in `Parser/`) = the dispatch loop's per-statement frame. Allocated once per batch and overwritten in place at the top of each iteration; holds `UtcNow` (the per-statement-freeze the time scalars read). Stored-proc EXEC / TRY-CATCH frames slot in here when added.
6969

7070
**Don't stack misfit state into these buckets unthinkingly**: when adding fields, ask which scope it actually belongs to. If none fits, that's the signal to introduce the missing scope rather than squat on a neighbor. Multi-database is structurally supported but exercised only by the default entry; `USE <db>` is the trigger to populate the dictionary properly.
@@ -144,7 +144,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
144144
- **Adding or changing system metadata surfaces** (sys.* / INFORMATION_SCHEMA.*) → [`docs/claude/catalog-views.md`](docs/claude/catalog-views.md).
145145
- **Extending scalar UDFs, inline TVFs, views, updatable-view DML routing, stored procedures (CREATE/ALTER/DROP/EXEC), or dynamic SQL (`EXEC (@sql)` / `sp_executesql`)**[`docs/claude/programmable.md`](docs/claude/programmable.md).
146146
- **Touching `#foo` routing, DROP TABLE, TRUNCATE TABLE**[`docs/claude/temp-tables.md`](docs/claude/temp-tables.md).
147-
- **Touching `DECLARE @t TABLE`, table-variable DML routing, `OUTPUT … INTO @t`**[`docs/claude/table-variables.md`](docs/claude/table-variables.md).
147+
- **Touching `DECLARE @t TABLE`, table-variable DML routing, `OUTPUT … INTO <target>` (`@t` or regular)**[`docs/claude/table-variables.md`](docs/claude/table-variables.md).
148148
- **Adding a new top-level statement parser or changing the dispatch loop's statement-separator rules**[`docs/claude/grammar.md`](docs/claude/grammar.md) + [`docs/claude/control-flow.md`](docs/claude/control-flow.md).
149149

150150
## Not modeled
@@ -160,12 +160,12 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
160160
- `LIKE` with `COLLATE` override (default collation only).
161161
- `CONVERT` / `TRY_CONVERT` style codes other than `0` / `120` / `121`.
162162
- `LEN(ntext)` raising Msg 8116; legacy `READTEXT` / `WRITETEXT` / `UPDATETEXT`.
163-
- `OUTPUT DELETED.*` / `INSERTED.*` star expansion. (`OUTPUT INTO @t` ships — see Table variables below.)
163+
- `OUTPUT DELETED.*` / `INSERTED.*` star expansion. (`OUTPUT INTO <target>` ships for both `@t` and regular tables — see Table variables below.)
164164
- MERGE source subqueries; MERGE target column refs in `ON`; `WHEN MATCHED` UPDATE/DELETE branches; `$action`.
165165
- `PRIMARY KEY` / `UNIQUE` on a computed column (`NotSupportedException`).
166166
- Heap allocation tracking (flat page list, no IAM/PFS).
167167
- Compound assignment (`SET @v += expr` / `-=` / `*=` etc.) — rewrite as `SET @v = @v + expr`. The arithmetic-operator runtime is locked behind `protected` instance methods on `TwoSidedExpression`; exposing them as static helpers is the prerequisite refactor.
168-
- **Table-variable constraints beyond v1 scope**`DECLARE @t TABLE (...)` ships with columns + `DEFAULT` + `NOT NULL` + inline anonymous PRIMARY KEY + table-level anonymous PRIMARY KEY (single table variable per DECLARE — multi-var DECLARE and mixed scalar+table raise Msg 102/156 matching probe). `IDENTITY`, `UNIQUE`, inline `CHECK`, computed columns, and `rowversion` in table variables raise `NotSupportedException` with the feature name (deferred follow-on). Named constraints (`CONSTRAINT pk1 PRIMARY KEY`) and `FOREIGN KEY` raise Msg 102 / Msg 156 (matches probe — real SQL Server rejects both shapes at parse). `OUTPUT … INTO @t` ships for INSERT / UPDATE / DELETE / MERGE; `OUTPUT … INTO <regular_table>` raises `NotSupportedException` (real SQL Server accepts both, but EF / app usage centers on table variables).
168+
- **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).
169169
- **Global temp tables (`##foo`)**`NotSupportedException` at parse. Local `#foo` works; the lifecycle for global temps (drops when creator session closes, visible across sessions) is the deferred scope.
170170
- **`ALTER TABLE #foo`**, **`OBJECT_ID('tempdb..#foo')`** — none modeled (none of those exist for regular tables either yet). The common `IF OBJECT_ID(...) IS NOT NULL DROP TABLE` cleanup pattern works via `DROP TABLE IF EXISTS #foo` instead.
171171
- **`DROP SCHEMA`**, **`ALTER SCHEMA … TRANSFER`** — deferred. CREATE SCHEMA + schema-qualified resolution ships; lifecycle doesn't yet (catalog views — `sys.schemas`, `INFORMATION_SCHEMA.SCHEMATA` — do ship as of the catalog-view-expansion bundle).

SqlServerSimulator.Tests/KeyConstraintTests.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,21 @@ insert t values (1, 2, 100)
122122
}
123123

124124
[TestMethod]
125-
public void PrimaryKey_TableLevel_OnNullableColumn_RaisesMsg8111()
126-
=> new Simulation().AssertSqlError("create table t (a int, b int, constraint pk_t primary key (a, b))", 8111,
125+
public void PrimaryKey_TableLevel_OnExplicitNullColumn_RaisesMsg8111()
126+
=> new Simulation().AssertSqlError("create table t (a int null, b int, constraint pk_t primary key (a, b))", 8111,
127127
"Cannot define PRIMARY KEY constraint on nullable column in table 't'.");
128128

129+
[TestMethod]
130+
public void PrimaryKey_TableLevel_BareColumns_PromoteToNotNull()
131+
{
132+
// Probe-confirmed against SQL Server 2025: table-level PK promotes
133+
// bare-int columns (no explicit NULL/NOT NULL) to NOT NULL —
134+
// inserting NULL surfaces Msg 515 rather than ignoring the PK.
135+
var simulation = new Simulation();
136+
_ = simulation.ExecuteNonQuery("create table t (a int, b int, constraint pk_t primary key (a, b))");
137+
_ = simulation.AssertSqlError("insert t values (null, 2)", 515);
138+
}
139+
129140
[TestMethod]
130141
public void PrimaryKey_Multiple_RaisesMsg8110()
131142
=> new Simulation().AssertSqlError("create table t (a int primary key, b int primary key)", 8110,

0 commit comments

Comments
 (0)