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
Local temp tables (#foo): per-session TempTables dict on SimulatedDbConnection, BatchContext.TryResolveTable routing #-names to it, # tokenizer support, DROP TABLE [IF EXISTS] (new — also covers regular tables), transactional CREATE/DROP via UndoLog's new polymorphic UndoEntry hierarchy, auto-drop on Dispose. ##foo / SELECT INTO #foo / ALTER #foo / TRUNCATE #foo / OBJECT_ID('tempdb..#foo') not modeled; 3-part names accepted in DROP only.
Copy file name to clipboardExpand all lines: CLAUDE.md
+18-2Lines changed: 18 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -62,8 +62,8 @@ Five scopes, one home each. **Add new state to whichever class matches its true
62
62
63
63
-**`Simulation`** = server / instance. Process-shared system tables (`SystemHeapTables`), `NEWSEQUENTIALID` anchor + counter, the `Databases` dictionary. Public surface (`Simulation` ctor + `CreateDbConnection()`) stays on this class.
64
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`. Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch` (see "Expression runtime context" below).
-**`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`. Exposes `TryResolveTable(name)` — the single routing rule that dispatches `#foo` to `Connection.TempTables` and everything else to `CurrentDatabase.HeapTables` + `SystemHeapTables`; nine lookup sites consume it (Selection FROM, Insert/Update/Delete/Merge targets, SET IDENTITY_INSERT, IDENT_CURRENT). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`.
67
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.
68
68
69
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.
@@ -247,6 +247,7 @@ Three entry points share one per-connection undo log: implicit (statement-level
247
247
-**Explicit txs**: `BEGIN TRAN` increments `TranCount`; only outermost `COMMIT` actually commits; `ROLLBACK` zeroes `TranCount` and walks the entire log. `SAVE TRAN <name>` + `ROLLBACK TRAN <name>` is the EF SaveChanges path inside an explicit tx. Parallel `BeginTransaction` → `InvalidOperationException`. `COMMIT`/`ROLLBACK` with no active tx → Msg 3902/3903.
248
248
-`@@TRANCOUNT` reads connection depth as int.
249
249
-**Identity counters and the database-scoped rowversion counter bypass the log** — both keep advancing through rollback. Orphaned LOB chains for rolled-back inserts also leak.
250
+
-**Temp-table CREATE/DROP participates in the log** via `TempTableCreation` / `TempTableRemoval``UndoEntry` subtypes (rollback removes from / restores into the connection's `TempTables` dict). Regular CREATE/DROP TABLE is NOT logged — see the corresponding quirk.
250
251
- No isolation: uncommitted writes immediately visible to all readers (single-Simulation, single-thread-at-a-time).
251
252
252
253
### UPDATE / DELETE
@@ -324,6 +325,16 @@ Not emitted by EF / not modeled: `JSON_QUERY`, `ISJSON`, `FOR JSON PATH`/`AUTO`.
324
325
-`MERGE INTO target USING (VALUES ...) AS alias (cols) ON predicate WHEN NOT MATCHED THEN INSERT ... [OUTPUT ...]` (multi-row batch).
325
326
-`WHEN MATCHED` parses but throws `NotSupportedException` if its predicate ever evaluates true.
326
327
328
+
### Local temp tables (`#foo`)
329
+
Per-connection `Dictionary<string, HeapTable> TempTables` on `SimulatedDbConnection`; routed by `BatchContext.TryResolveTable` (`#`-prefix → connection dict, else current DB + system tables). Auto-cleared on `Dispose`, matching real SQL Server's session-close drop. Lifecycle, cross-conn isolation, and Msg 208 from other sessions all probe-confirmed against SQL Server 2025.
330
+
331
+
-**`CREATE TABLE #foo`** reuses the full CREATE TABLE grammar (constraints, identity, computed, defaults). **`##foo`** raises `NotSupportedException` at parse — not modeled. Tokenizer: `#` is now a leading-identifier char (`Parser/Tokenizer.cs:ParseHashPrefixedName`), so `#foo` / `##foo` / bare `#` all lex as a `Name` (CheckReserved short-circuits because no keyword begins with `#`); semantic rejection of `##` happens at CREATE.
332
+
-**`DROP TABLE [IF EXISTS] name[, name...]`** — new statement (`Simulation.Drop.cs`). Routes by `#`-prefix; comma-list form supported. Missing → **Msg 3701 St 5 Class 11** verbatim; `IF EXISTS` suppresses. Also covers regular (non-`#`) tables — first user-visible DROP TABLE support in the simulator.
333
+
-**Three-part name in DROP**: `tempdb..#foo`, `tempdb.dbo.#foo`, and even `claude..#foo` all resolve to the session's `#foo` (DB qualifier is cosmetic on `#` names — probe-confirmed). Other statements (FROM / INSERT / UPDATE / DELETE / MERGE / SET IDENTITY_INSERT) accept bare `#foo` only; deferred for a future bundle.
334
+
-**Transactional CREATE / DROP**: probe-confirmed that `BEGIN TRAN; CREATE TABLE #foo; ROLLBACK` undoes the table, and DROP TABLE inside a tran is similarly reversible. `UndoLog` (re-shaped to a polymorphic `UndoEntry` hierarchy) records `TempTableCreation` and `TempTableRemoval` entries alongside the existing slot-mutation kinds.
335
+
-**Persists across batches** in the same connection; **invisible to other connections** (Msg 208). Two sessions can independently hold a `#foo` of the same name — real SQL Server mangles internally; the simulator achieves the same effect by giving each connection its own dict (user-visible names stay un-mangled).
336
+
-**Bare `#`** is a valid temp-table name (one-char `#`). Identity / SCOPE_IDENTITY work identically to regular tables. CTE prefix, JOINs across multiple `#`-tables, all queries against `#foo` flow through the same Selection / Insert / Update / Delete / Merge machinery via `TryResolveTable`.
337
+
327
338
### EF Core adapter coverage
328
339
`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`.
329
340
@@ -354,6 +365,10 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
354
365
- Heap allocation tracking (flat page list, no IAM/PFS).
355
366
- 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.
356
367
- Table variables (`DECLARE @t TABLE (...)`) — separate feature with its own storage / scope / lifecycle.
368
+
-**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.
369
+
-**`SELECT ... INTO #foo`** (schema-inferring create) — separate feature; users must `CREATE TABLE #foo (...)` then `INSERT INTO #foo SELECT ...`. Schema-inference rules (nullability of derived columns, identity propagation) are the deferred design work.
370
+
-**`ALTER TABLE #foo`**, **`TRUNCATE 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.
371
+
-**Three-part name resolution outside DROP TABLE**: `tempdb..#foo` in FROM / INSERT / UPDATE / DELETE / MERGE / SET IDENTITY_INSERT raises `InvalidObjectName` (Msg 208) on the qualifier; use bare `#foo`. DROP TABLE alone tolerates the qualifier (probe pattern).
357
372
- T-SQL control flow (`IF` / `WHILE` / `BEGIN ... END` / `BREAK` / `CONTINUE`) — Bundle 2 of scripting.
@@ -369,3 +384,4 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
369
384
-**DELETE / UPDATE leak LOB chains**: orphaned LOB chains stay in `Heap.LobPages`. Other rows reference LOB pages by stable index, so list compaction would corrupt them.
370
385
-**Mass-shift UPDATE on a unique key**: `UPDATE t SET k = k + 1` where k is PK/UNIQUE may spuriously raise Msg 2627 — the two-phase validator compares each affected row's new key against other affected rows' new keys, so post-shift values overlapping pre-shift values trigger a false positive.
371
386
-**`GetBytes`/`GetChars` materialize, don't stream**: each call decodes the full column value via `RowDecoder` and slices into the caller's buffer. Behavior matches per-call observation; the streaming-memory guarantee doesn't.
387
+
-**Temp-table DDL is transactional, regular-table DDL isn't**: `CREATE TABLE #foo` / `DROP TABLE #foo` inside `BEGIN TRAN` participate in the undo log (matching real SQL Server); the same statements on a regular table commit immediately regardless of an active transaction. Asymmetric, but no real workload depends on transactional regular-DDL (EF doesn't do schema changes through SaveChanges, and migrations run outside transactions on real SQL Server too).
0 commit comments