Skip to content

Commit 6b227bb

Browse files
committed
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.
1 parent 3c6ff48 commit 6b227bb

17 files changed

Lines changed: 584 additions & 40 deletions

CLAUDE.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ Five scopes, one home each. **Add new state to whichever class matches its true
6262

6363
- **`Simulation`** = server / instance. Process-shared system tables (`SystemHeapTables`), `NEWSEQUENTIALID` anchor + counter, the `Databases` dictionary. Public surface (`Simulation` ctor + `CreateDbConnection()`) stays on this class.
6464
- **`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).
65+
- **`SimulatedDbConnection`** = session. `CurrentDatabase` pointer, `CurrentTransaction`, `LastIdentity` (`SCOPE_IDENTITY()` / `@@IDENTITY`), `LastStatementRowCount` (`@@ROWCOUNT`), `IdentityInsertTable`, `TraceFlags`, `IsVerboseTruncationActive()`, `TempTables` (per-session `#foo` dictionary, cleared on `Dispose`).
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`. 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`.
6767
- **`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.
6868

6969
**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
247247
- **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.
248248
- `@@TRANCOUNT` reads connection depth as int.
249249
- **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.
250251
- No isolation: uncommitted writes immediately visible to all readers (single-Simulation, single-thread-at-a-time).
251252

252253
### UPDATE / DELETE
@@ -324,6 +325,16 @@ Not emitted by EF / not modeled: `JSON_QUERY`, `ISJSON`, `FOR JSON PATH`/`AUTO`.
324325
- `MERGE INTO target USING (VALUES ...) AS alias (cols) ON predicate WHEN NOT MATCHED THEN INSERT ... [OUTPUT ...]` (multi-row batch).
325326
- `WHEN MATCHED` parses but throws `NotSupportedException` if its predicate ever evaluates true.
326327

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+
327338
### EF Core adapter coverage
328339
`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`.
329340

@@ -354,6 +365,10 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
354365
- Heap allocation tracking (flat page list, no IAM/PFS).
355366
- 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.
356367
- 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).
357372
- T-SQL control flow (`IF` / `WHILE` / `BEGIN ... END` / `BREAK` / `CONTINUE`) — Bundle 2 of scripting.
358373
- `TRY ... CATCH`, `THROW`, `RAISERROR`, `@@ERROR`, `RETURN`, `PRINT`, stored procs / UDFs.
359374
- `hierarchyid`, `geography`, `geometry`.
@@ -369,3 +384,4 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
369384
- **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.
370385
- **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.
371386
- **`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

Comments
 (0)