Commit 37f5bfe
committed
Locking phase 1a — table-level Shared / Exclusive data locks, transaction-scoped X retention,
WITH (NOLOCK) / WITH (HOLDLOCK) hint semantics, cross-thread waiter-graph cycle detection (Msg 1205), and auto-rollback of the deadlock victim's transaction. LockResource refactored from "encapsulates its own gate + Acquire/Release logic" into a passive data carrier (just the Holders list); the logic moves into a new LockManager class held at Simulation.LockManager with a single per-Simulation gate. The single-gate model is the load-bearing simplification that makes cross-resource cycle detection straightforward — every connection's WaitingOnResource is readable consistently under the same lock, no inter-gate ordering issues. LockMode extended with Shared and Exclusive. Compatibility matrix in LockManager.IsCompatible makes the schema family (Sch-S / Sch-M) and the data family (S / X) orthogonal — Sch-S × S / X compatible; Sch-M conflicts with everything; within the data family only S × S is compatible. Same-owner re-entrance always succeeds (handles the ALTER pattern's Sch-S then Sch-M from the same connection plus DML-target double-resolution). BatchContext.AcquireTransactionLock is the new tx-scoped acquire — when CurrentTransaction is non-null the (resource, mode) tuple lands in SimulatedDbTransaction.HeldLocks; otherwise it falls back to the statement-scoped list. BatchContext.AcquireDataLockIfApplicable(table, hints, isWrite) is the central routing helper called from FROM-source resolution in Selection.cs, MERGE bare-table source in Simulation.Merge.cs, and INSERT / UPDATE / DELETE / MERGE target sites; bypasses for table variables / local temp tables / system tables (none cross-connection contention points). Reads acquire Shared (skipped on NOLOCK / READUNCOMMITTED; promoted to tx-scoped on HOLDLOCK / SERIALIZABLE / REPEATABLEREAD); writes always acquire Exclusive, tx-scoped when BEGIN TRAN is active and released at Commit / Rollback / dispose-implicit-rollback in SimulatedDbTransaction.ReleaseAllLocks. Savepoint partial-rollback (ROLLBACK TRAN <savepoint>) intentionally does NOT release locks, matching real SQL Server (probe-confirmed via the EF SaveChanges path on 2026-05-14). Selection.ParseOptionalTableHints now returns a TableHintInfo (NoLock / HoldLock flags) instead of being void; every callsite either captures the value for downstream lock-routing or discards via _ =. Cross-thread cycle detection: LockManager.WouldCreateCycle walks the wait-for graph from each conflicting holder (SimulatedDbConnection.WaitingOnResource, set under the gate before Monitor.Wait and cleared in finally), DFS with a visited set to break finite cycles in the holder set itself; on cycle the caller is the victim per the always-the-requester policy. The same-thread-deadlock short-circuit from phase 0 remains as a fast path for the "two connections on one thread, holder's command is suspended at the iterator boundary" scenario. Simulation.DispatchOneStatement's catch now intercepts Class-13 exceptions (Msg 1205) and rolls back connection.CurrentTransaction BEFORE the TRY-frame check — both the propagating and TRY-captured paths observe the auto-rollback, matching real SQL Server's @@TRANCOUNT = 0 in CATCH (probe step 3, 2026-05-14). The rollback releases every entry in HeldLocks so the survivor's blocked acquire unblocks naturally on the next pulse. **Practical effect**: phase-1a is *stronger* than real SQL Server's READ COMMITTED — a SELECT blocks on any X-locked row in the table, not just the row being read, because granularity is table-only; phase 1b's row-level dispatch fixes this. 3 new internal LockResourceTests (S × X conflict, transaction-scoped X release at commit / rollback) plus 5 new user-facing LockingTests: cross-thread tx-X blocks concurrent read until commit; LOCK_TIMEOUT 0 → fail-fast Msg 1222; NOLOCK reads through uncommitted X (dirty read); HOLDLOCK keeps S until commit (blocks concurrent write); classic 2-cycle cross-thread deadlock → Msg 1205 on one connection (requester), survivor's UPDATE completes after victim's tx auto-rolls-back. docs/claude/locking.md rewritten as a unified phases-0+1a doc with the new compatibility matrix, the lock-owner / lock-scope table, the cycle-detection algorithm, the hint surface table (modeled vs parse-and-discard), and a phase-1a gaps section: row / page / key locks defer to 1b along with the alias-form UPDATE/DELETE FROM-clause target X acquire; READPAST / UPDLOCK / XLOCK / TABLOCK* / PAGLOCK / ROWLOCK still parse-and-discard; sys.dm_tran_locks views + @@LOCK_TIMEOUT scalar are phase 2; SNAPSHOT / RCSI is phase 3; Heap.Insert cross-table cross-thread mutation isn't yet thread-safe (same-table writes serialize via X naturally, cross-table relies on phase 1b). CLAUDE.md "Not modeled" rewritten to scope to phases 1b+ and call out the table-only-granularity stronger-than-RC quirk; Feature-reference entry expanded with the data-lock APIs and the cycle-detection / auto-rollback surfaces; the "No isolation yet" line replaced by a phase-1a-accurate description of table-level RC.1 parent 2c3a8af commit 37f5bfe
15 files changed
Lines changed: 985 additions & 423 deletions
File tree
- SqlServerSimulator.Tests.Internal
- SqlServerSimulator.Tests
- SqlServerSimulator
- Parser
- Simulation
- docs/claude
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
117 | | - | |
| 117 | + | |
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
157 | | - | |
| 157 | + | |
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
0 commit comments