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
Context layering: split Simulation's conflated server+database role into Simulation (instance) + Database (HeapTables, CompatibilityLevel, VerboseTruncationWarnings, @@DBTS rowversion counter); split ParserContext's parse+runtime role into pure-parse ParserContext + BatchContext (Variables, CurrentUndoLog, owns the parser context) + StatementContext (UtcNow frame mutated per dispatch iteration). SimulatedDbConnection gains a CurrentDatabase pointer into the new Databases dict (single "simulated" entry today; USE <db> populates more later). Late-bind switches from Simulation.ActiveConnection to Simulation.ActiveBatch (AsyncLocal<BatchContext?>) — CurrentTimeFunction / LastIdentityExpression / RowCountExpression / IdentCurrent capture Simulation at parse time and resolve through the executing batch at runtime, so a column default's getutcdate() parsed under one batch reads the running batch's frame on later INSERTs.
Copy file name to clipboardExpand all lines: CLAUDE.md
+11-9Lines changed: 11 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,14 +57,16 @@ Readonly struct, up to 4 inline slots (SQL Server's grammar limit). API: `Leaf`,
57
57
### Expression evaluation
58
58
`Expression.Run(columnResolver)` (runtime) and `Expression.GetSqlType(...)` (static, for projection schema) must agree on result type — drift breaks union/CASE/coalesce schema. `BooleanExpression.Run` returns `bool?` (three-valued); WHERE/MERGE-ON exclude UNKNOWN, CHECK passes UNKNOWN. Aggregates: subclass `Aggregator` (`Add(SqlValue)` / `Result()`), register in `AggregateExpression`'s dispatch.
59
59
60
-
### Known architectural debt — context layering
61
-
Real SQL Server has five concentric scopes — **server / database / session / batch / statement** — and each piece of state has exactly one home. The simulator collapses these to three buckets and several pieces of state squat in the closest neighbor:
60
+
### Context layering
61
+
Five scopes, one home each. **Add new state to whichever class matches its true scope** — when in doubt, the rule of thumb is: who outlives whom?
62
62
63
-
-**`Simulation`** = server *and* database. Server-scoped (system tables, `NEWSEQUENTIALID` anchor, rowversion counter) and database-scoped (`HeapTables`, `CompatibilityLevel`, `VerboseTruncationWarnings`) share one class.
64
-
-**`SimulatedDbConnection`** = session. Cleanly mapped (`LastIdentity`, `LastStatementRowCount`, `IdentityInsertTable`, `TraceFlags`, `CurrentTransaction`) — *plus*`CurrentStatementUtcNow`, which is semantically statement-scoped but lives here because statement scope has no home.
65
-
-**`ParserContext`** = parse-time scratch *and* per-batch runtime context. Pure parse state (`Token`, `AggregateCollector`, `WindowCollector`, `OuterTypeResolver`, `CteBindings`) shares the class with batch-lifetime runtime state (`Variables`, `CurrentUndoLog`).
63
+
-**`Simulation`** = server / instance. Process-shared system tables (`SystemHeapTables`), `NEWSEQUENTIALID` anchor + counter, the `Databases` dictionary, and the `ActiveBatch``AsyncLocal` pointer published by the dispatch loop. Public surface (`Simulation` ctor + `CreateDbConnection()`) stays on this class.
64
+
-**`Database`** (internal) = one database hosted by the server instance. `HeapTables`, `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.
65
+
-**`SimulatedDbConnection`** = session. `CurrentDatabase` pointer, `CurrentTransaction`, `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `LastStatementRowCount` (`@@ROWCOUNT`), `IdentityInsertTable`, `TraceFlags`, `IsVerboseTruncationActive()` (reads its own `TraceFlags` + the current database's compat / scoped-config).
66
+
-**`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`, `CurrentUndoLog`, plus the per-statement frame `CurrentStatement`. Late-bound expressions (`CurrentTimeFunction`, `LastIdentityExpression`, `RowCountExpression`, `IdentCurrent`) capture `Simulation` at parse time and read through `simulation.ActiveBatch` at runtime, so a column default's `getutcdate()` parsed during one batch correctly resolves against the *executing* batch on later INSERTs.
67
+
-**`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.
66
68
67
-
This is a known smell, not an active task. Refactor trigger: the next feature that genuinely needs a boundary the current shape can't express — `USE <db>` / cross-database queries, true session-vs-global trace-flag scope, stored procedures (which introduce a real per-call statement frame), or two or three consecutive "I had to add it to `Simulation` because no better home exists" moments. Until then, splitting layers churns code without changing user-observable behavior. **Don't stack more state into these buckets unthinkingly** — when adding fields, ask which of the five scopes it actually belongs to and note the mismatch if there is one.
69
+
**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.
68
70
69
71
## Conventions that fail builds
70
72
@@ -177,7 +179,7 @@ Unknown keyword → Msg 155 with the calling function's lowercase name embedded.
177
179
### Current-time scalars
178
180
Result types: `GETDATE`/`GETUTCDATE`/`CURRENT_TIMESTAMP` → `datetime`; `SYSDATETIME`/`SYSUTCDATETIME` → `datetime2(7)`; `SYSDATETIMEOFFSET` → `datetimeoffset(7)`. EF emits these from `DateTime.UtcNow`/`Now`/`DateTimeOffset.UtcNow` and `HasDefaultValueSql("getutcdate()")`.
179
181
180
-
**Per-statement freeze**: two `SYSDATETIME()` calls in one SELECT return identical values; an UPDATE that stamps every row writes the same value; successive SELECTs in one batch DO advance. Captured once per statement-loop iteration into `Simulation.CurrentStatementUtcNow`.
182
+
**Per-statement freeze**: two `SYSDATETIME()` calls in one SELECT return identical values; an UPDATE that stamps every row writes the same value; successive SELECTs in one batch DO advance. Captured once per statement-loop iteration into `BatchContext.CurrentStatement.UtcNow`.
181
183
182
184
**UTC == Local** (Azure SQL Database default): no local-time conversion; all six functions return the same UTC instant (rounded per type — datetime variants quantize to 1/300s tick). `SYSDATETIMEOFFSET` reports `+00:00`. Apps depending on `GETDATE` ≠ `GETUTCDATE` differing by zone won't behave like on-prem; matches cloud default.
183
185
@@ -283,9 +285,9 @@ Variable references resolve at runtime via a captured `VariableSlot` — require
283
285
284
286
**`@@ROWCOUNT`**: tracks the most-recently-completed statement's row count via `SimulatedDbConnection.LastStatementRowCount` (per-session state, not shared across connections that fan out from one `Simulation`). SELECT row counts populate after the dispatch materializes rows up-front (so the next statement in the batch sees the final count); DML mutations write their affected count; `SET` / `DECLARE @v = init` write 1; bare `DECLARE @v` (no initializer) preserves the prior count; transaction / DDL statements reset to 0.
285
287
286
-
**Per-session state lives on `SimulatedDbConnection`**: alongside `@@ROWCOUNT`, the connection carries `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `IdentityInsertTable` (active `SET IDENTITY_INSERT` table), `TraceFlags` (`DBCC TRACEON(N)`), `CurrentStatementUtcNow` (per-statement-freeze for the time scalars), and `IsVerboseTruncationActive()` (which reads its own `TraceFlags` plus the simulation's compatibility level / scopedconfig). Two connections fanned from one `Simulation` are isolated — matches SQL Server's session = ADO.NET connection scoping.
288
+
**Per-session state lives on `SimulatedDbConnection`** (see the Context layering section for the full five-scope map): alongside `@@ROWCOUNT`, the connection carries `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `IdentityInsertTable` (active `SET IDENTITY_INSERT` table), `TraceFlags` (`DBCC TRACEON(N)`), `IsVerboseTruncationActive()` (reads its own `TraceFlags` plus the current database's compat / scoped-config), `CurrentDatabase` (per-session pointer into `Simulation.Databases`), and `CurrentTransaction`. Two connections fanned from one `Simulation` are isolated — matches SQL Server's session = ADO.NET connection scoping.
287
289
288
-
**Late-binding the executing connection**: `CurrentTimeFunction`, `LastIdentityExpression`, and `RowCountExpression` capture the **`Simulation`** at parse time and look up the runtime-active connection via `Simulation.ActiveConnection` (an `AsyncLocal<SimulatedDbConnection?>` published by the dispatch loop with try/finally save-restore). Parse-time-capture-the-connection is wrong for any expression embedded in a column default: `HasDefaultValueSql("getutcdate()")` parses once during CREATE TABLE on the connection that ran the CREATE, then every later INSERT runs on whichever connection's batch is dispatching, and the default has to read *that*connection's per-statement freeze, not the long-gone CREATE-time connection's.
290
+
**Late-binding via `Simulation.ActiveBatch`**: `CurrentTimeFunction`, `LastIdentityExpression`, `RowCountExpression`, and `IdentCurrent` capture the **`Simulation`** at parse time and look up the runtime-active batch via `Simulation.ActiveBatch` (an `AsyncLocal<BatchContext?>` published by the dispatch loop with try/finally save-restore) when `Run` fires. Parse-time-capture-the-connection-or-batch is wrong for any expression embedded in a column default: `HasDefaultValueSql("getutcdate()")` parses once during CREATE TABLE, then every later INSERT runs in a different batch, and the default has to read *that*batch's per-statement freeze (`ActiveBatch.CurrentStatement.UtcNow`), not the long-gone CREATE-time batch's.
289
291
290
292
**Compound assignment** (`SET @v += expr` etc.) and **table variables** (`DECLARE @t TABLE (...)`) aren't modeled — rewrite as `SET @v = @v + expr` for the former; the latter is a separate bundle.
0 commit comments