Skip to content

Commit 97f5a62

Browse files
committed
Locking phase 3 — SNAPSHOT isolation + READ_COMMITTED_SNAPSHOT (MVCC). New per-database flags Database.AllowSnapshotIsolation / ReadCommittedSnapshot (default OFF) flipped via ALTER DATABASE … SET (ALLOW_SNAPSHOT_ISOLATION | READ_COMMITTED_SNAPSHOT) { ON | OFF } — bare-name / CURRENT / bracketed forms all parse through the refactored TryParseAlterDatabaseSet switch over three contextual keywords. New Database.AllocateTransactionCommitId() monotonic per-database counter stamps committed versions; readers under SI/RCSI capture their snapshot via CurrentTransactionCommitId. Per-table HeapTable.RowVersions dict holds RowVersionChain { LiveXmin, WriterTx?, IsDeletedLive, Head }; HistoricalVersion carries Payload / Xmin / Xmax / Next for the chain walk. New Heap.ReadSlotBytes / HeapPage.ReadSlotBytes expose raw slot bytes (tombstoned slots included) for pre-mutation snapshotting. Writer-side capture lives in VersionStore.CaptureWrite(batch, table, newRid, oldRid?, oldPayload?, kind) — wired at INSERT (Simulation.Insert.cs after the post-Heap.Insert row-X), UPDATE (Simulation.Update.cs:CommitUpdate via oldBytesPerAffected captured before the delete loop, then captured per row after the re-insert), and DELETE (Simulation.Delete.cs:CommitDelete via Heap.ReadSlotBytes immediately before each DeleteAt). UPDATE eagerly attaches a HistoricalVersion { Xmax = PendingXmax } to the new chain's Head so concurrent SI readers see the pre-write payload before the writer commits; Commit replaces PendingXmax with the real commit Xid, drops the abandoned old-slot chain, stamps LiveXmin. INSERT stamps LiveXmin at commit; DELETE pushes the historical payload + sets IsDeletedLive at commit. Pending entries live on SimulatedDbTransaction.PendingVersionEntries for explicit txs (drained by Commit via VersionStore.FinalizePendingEntries / discarded by Rollback + dispose via DiscardPendingEntries) and on BatchContext.CurrentStatementVersionEntries for auto-commit DML; RunMutation's statement-atomic mid-execution failure also unwinds entries added since versionEntriesMarker. Reader visibility plugs into BatchContext.WrapWithRowConflictChecks via the new ResolveSnapshotXidForRead(table) — returns tx.SnapshotXid (lazy at first user-table read) for SI sessions, BatchContext.RcsiStatementSnapshotXid (lazy at first user-table read this statement, cleared between statements at DispatchOneStatement's top) for default-RC sessions when RCSI is on, null otherwise; the per-row payload routes through VersionStore.ResolveVisibleVersion which returns the live payload / a historical payload / or null (skip — inserted-after-snapshot). SimulatedDbConnection.BeginDbTransaction(IsolationLevel) now overrides SessionIsolationLevel for the tx's duration (SimulatedDbTransaction.PreviousSessionIsolationLevel + OverrodeSessionIsolation carry the save / restore on Commit / Rollback / dispose). Msg 3952 verbatim wording (Snapshot isolation transaction failed accessing database '<db>' because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation. Cls 16 St 1) — probe-confirmed fires at first user-table access in an SI session with ASI=OFF; SET TRANSACTION ISOLATION LEVEL SNAPSHOT is silent, system-catalog reads bypass the gate, both reads + writes trigger it. Msg 3960 verbatim wording (Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table '<schema>.<table>' directly or indirectly in database '<db>' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement. Cls 16 St 2) — VersionStore.CheckSnapshotUpdateConflict at the top of CommitUpdate / CommitDelete; auto-rolls back the SI tx before throwing (probe-confirmed @@TRANCOUNT = 0 in CATCH). Two new error factories on SimulatedSqlException.ConcurrencyErrors.cs, two new contextual keywords (Allow_Snapshot_Isolation, Read_Committed_Snapshot). 14 new SnapshotIsolationTests: ALTER DATABASE flips (bare / CURRENT / bracketed); Msg 3952 verbatim at user-table access; system-catalog access bypass; Msg 3960 verbatim with auto-rollback; Msg 3960 for SI DELETE on RC-updated row; SI reader sees prior committed version after concurrent UPDATE; multi-update prior-committed visibility; SI snapshot baseline refresh after commit; RCSI reader sees committed value without blocking on uncommitted writer; RCSI per-statement snapshot (second statement sees the new committed value, distinct from SI). Known limitations documented in docs/claude/locking.md's new "Snapshot isolation + MVCC (phase 3)" section: DELETE-not-visible-to-SI (heap iteration skips tombstoned slots so SI readers miss rows their snapshot would see); SI-writer-on-RC-deleted-row Msg 3960 (same root cause); intra-tx multi-update history collapse; no version-store GC; no sys.dm_tran_version_store DMV. Real SQL Server's WITH NO_WAIT / WITH ROLLBACK IMMEDIATE termination options on versioning-state changes (probe-confirmed Msg 5083 rejection) aren't modeled — the simulator's flip takes effect immediately, skipping the brief-stabilization-wait + single-connection-required gates. CLAUDE.md "Not modeled" rewritten: phase 3 ships, phase 4+ deferrals tightened to key-range locks proper, ALTER SCHEMA TRANSFER Sch-M, row-lock cleanup on DELETE, and SI-visible DELETE.
1 parent d145adc commit 97f5a62

18 files changed

Lines changed: 1169 additions & 10 deletions

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,12 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
154154
- **Touching `ALTER TABLE … ADD CONSTRAINT` (PK / UQ / FK / CHECK / DEFAULT), `ALTER TABLE … DROP CONSTRAINT [IF EXISTS]`, `ALTER TABLE … (CHECK | NOCHECK) CONSTRAINT (ALL | name [,…])` trust toggling, `ALTER TABLE … ADD [COLUMN]` / `DROP COLUMN` / `ALTER COLUMN`, `WITH CHECK` / `WITH NOCHECK`, the existing-data-validation scan, the constraint-name uniqueness check, the atomic multi-drop / multi-toggle pipeline, the `IsDisabled` / `IsNotTrusted` enforcement gates, the type-change conversion (`SqlValue.CoerceTo` surfacing Msg 220 / 245 / 241 / 8115 / 2628), the nullability-flip Msg 515 scan, the Msg 4928 COMPUTED / rowversion rejection, the Msg 5074 ALTER COLUMN blocker walker (PK/UQ/FK/computed-deps unconditional; index when SqlType-subclass changes), or `sys.check_constraints` / `sys.key_constraints` / `sys.default_constraints`**[`docs/claude/alter-table.md`](docs/claude/alter-table.md).
155155
- **Touching `CREATE [UNIQUE] [CLUSTERED | NONCLUSTERED] INDEX` (with `INCLUDE` / `WHERE` filter / `WITH (options)` clauses), `DROP INDEX [IF EXISTS] name ON table [, …]`, the `HeapTable.Indexes` list, UNIQUE-index enforcement (filter-aware Msg 2601 via `EnforceUniqueIndexes` / `EnforceUniqueIndexesForUpdate`), the PK-or-UQ-name DROP rejection (Msg 3723), or `sys.indexes` / `sys.index_columns`**[`docs/claude/indexes.md`](docs/claude/indexes.md).
156156
- **Touching table hints (`WITH (NOLOCK [, …])` on FROM sources / JOIN-RHS / UPDATE / DELETE targets, the legacy `(hint)` without-WITH form, `Msg 321` unknown-hint rejection), statement-level `OPTION (…)` hints (`RECOMPILE` / `MAXDOP` / `FAST` / `LOOP|HASH|MERGE JOIN` / `FORCE ORDER` / `OPTIMIZE FOR (UNKNOWN)` / `USE HINT(…)` / `MAXRECURSION` with its CTE-binding override), the `TableHintNames` / `OptionHintFirstWords` closed accept-lists, or `SkipBalancedParens` argument consumption**[`docs/claude/query-hints.md`](docs/claude/query-hints.md).
157-
- **Touching the `LockResource` / `LockManager` pair, the 8-mode compatibility matrix (Sch-S / Sch-M / IS / IX / SIX / S / U / X), `BatchContext.AcquireStatementLock` / `AcquireTransactionLock` / `AcquireDataLockIfApplicable` / `AcquireRowLockTxScoped` / `TouchRowForRead` / `WrapWithRowConflictChecks`, the `DataLockPlan` struct that captures the per-row strategy, the row-lock dict on `HeapTable.RowLocks`, lock escalation (5000-threshold via `SimulatedDbTransaction.RowLockCountsByTable` + `EscalatedTables`), `SimulatedDbConnection.SessionIsolationLevel` + `SET TRANSACTION ISOLATION LEVEL` wiring, the hint surface (UPDLOCK / XLOCK / READPAST / TABLOCK / TABLOCKX / HOLDLOCK / SERIALIZABLE / REPEATABLEREAD / NOLOCK), `SimulatedDbConnection.Spid` / `LockTimeoutMillis` / `CurrentExecutingThreadId` / `WaitingOnResource`, the same-thread-deadlock check and cross-thread waiter-graph cycle detection (Msg 1205, auto-rollback of victim's tx), or lock-timeout path (Msg 1222)** → [`docs/claude/locking.md`](docs/claude/locking.md).
157+
- **Touching the `LockResource` / `LockManager` pair, the 8-mode compatibility matrix (Sch-S / Sch-M / IS / IX / SIX / S / U / X), `BatchContext.AcquireStatementLock` / `AcquireTransactionLock` / `AcquireDataLockIfApplicable` / `AcquireRowLockTxScoped` / `TouchRowForRead` / `WrapWithRowConflictChecks` / `ResolveSnapshotXidForRead`, the `DataLockPlan` struct that captures the per-row strategy, the row-lock dict on `HeapTable.RowLocks`, lock escalation (5000-threshold via `SimulatedDbTransaction.RowLockCountsByTable` + `EscalatedTables`), `SimulatedDbConnection.SessionIsolationLevel` + `SET TRANSACTION ISOLATION LEVEL` wiring, the hint surface (UPDLOCK / XLOCK / READPAST / TABLOCK / TABLOCKX / HOLDLOCK / SERIALIZABLE / REPEATABLEREAD / NOLOCK), `SimulatedDbConnection.Spid` / `LockTimeoutMillis` / `CurrentExecutingThreadId` / `WaitingOnResource`, the same-thread-deadlock check and cross-thread waiter-graph cycle detection (Msg 1205, auto-rollback of victim's tx), the lock-timeout path (Msg 1222), or the SNAPSHOT + RCSI machinery (`Database.AllowSnapshotIsolation` / `ReadCommittedSnapshot` / `AllocateTransactionCommitId`, `HeapTable.RowVersions` + `RowVersionChain` + `HistoricalVersion`, the `VersionStore` static helper for `CaptureWrite` / `FinalizePendingEntries` / `DiscardPendingEntries` / `ResolveVisibleVersion` / `CheckSnapshotUpdateConflict`, `SimulatedDbTransaction.SnapshotXid` / `PendingVersionEntries`, `BatchContext.RcsiStatementSnapshotXid`, `ALTER DATABASE … SET (ALLOW_SNAPSHOT_ISOLATION | READ_COMMITTED_SNAPSHOT)`, Msg 3952 + Msg 3960)** → [`docs/claude/locking.md`](docs/claude/locking.md).
158158
- **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).
159159

160160
## Not modeled
161161

162-
- **Key-range locks, MVCC / SNAPSHOT, `ALTER SCHEMA TRANSFER` Sch-M, row-lock cleanup on DELETE** — phase 3 deferrals. **Phases 0 + 1a + 1b + 2 ship**: schema-stability locks (Sch-S / Sch-M) including ALTER PROCEDURE / TRIGGER / SEQUENCE; the full 8-mode data + intent matrix (Sch-S / Sch-M / IS / IX / SIX / S / U / X); row-level X on writers and probe-only / row-S / row-U / row-X on readers depending on hints + isolation level; row-X on history-table / cascade-FK / OUTPUT-INTO / SELECT-INTO destination writes; alias-form `UPDATE x SET … FROM t x JOIN …` table-IX acquired after FROM-target identification; SERIALIZABLE / HOLDLOCK hint upgrades read to table-S tx-scoped (phantom prevention at table granularity since real SQL Server's key-range locks require index range structure the simulator doesn't have); REPEATABLE READ / REPEATABLEREAD hint retains row-S tx-scoped; UPDLOCK retains row-U tx-scoped; XLOCK retains row-X tx-scoped on reads; READPAST skips conflicting rows; TABLOCK / TABLOCKX escalate to table-S / table-X; NOLOCK / READUNCOMMITTED dirty-read; lock escalation at 5000 per-tx per-table row-locks → table-X; cross-thread waiter-graph cycle detection (Msg 1205); same-thread immediate-deadlock short-circuit; `SET LOCK_TIMEOUT N` driving Msg 1222 on expiry; `SET TRANSACTION ISOLATION LEVEL` mutating session state with semantic effect; auto-rollback of the victim's tx on Msg 1205 (probe-confirmed `@@TRANCOUNT = 0` in CATCH); `@@LOCK_TIMEOUT` / `@@SPID` scalar functions; hint-conflict detection (Msg 1047 `NOLOCK + XLOCK/UPDLOCK/HOLDLOCK/…` verbatim, Msg 1065 `NOLOCK on DML target` verbatim, Msg 1069 `INDEX hint on DML target` verbatim); `sys.dm_tran_locks` + `sys.dm_os_waiting_tasks` DMVs projecting GRANT / WAIT rows over the live lock-manager state. See [`docs/claude/locking.md`](docs/claude/locking.md). `BEGIN DISTRIBUTED TRANSACTION` and `BEGIN TRANSACTION ... WITH MARK` raise `NotSupportedException` at dispatch. **Other `SET <option> …` statements are parsed-and-discarded** via a closed accept-list in `Simulation.Set.cs` — `SET XACT_ABORT ON|OFF`, the ANSI / session-state toggles (ANSI_NULLS / QUOTED_IDENTIFIER / ANSI_WARNINGS / ANSI_PADDING / CONCAT_NULL_YIELDS_NULL / ARITHABORT / NUMERIC_ROUNDABORT / …) including the multi-option comma form EF Core emits, value-taking options (TEXTSIZE / DATEFIRST / ROWCOUNT / DATEFORMAT / LANGUAGE / DEADLOCK_PRIORITY / CONTEXT_INFO), and the `SET STATISTICS {IO|TIME|XML|PROFILE} ON|OFF` sub-form. SNAPSHOT isolation level parses-and-discards (behaves as RC). Unknown SET option followed by ON/OFF/value → Msg 195 verbatim (`'<name>' is not a recognized SET option.`); no-trailing-token form → generic Msg 102 (probe-confirmed split). `SET @v = expr`, `SET IDENTITY_INSERT`, `SET NOCOUNT ON|OFF`, `SET IMPLICIT_TRANSACTIONS ON|OFF`, `SET LOCK_TIMEOUT N`, and `SET TRANSACTION ISOLATION LEVEL {…}` have semantic effect; everything else in the accept-list parses-and-discards.
162+
- **Key-range locks, `ALTER SCHEMA TRANSFER` Sch-M, row-lock cleanup on DELETE, SI-visible DELETE** — phase 4+ deferrals (phase 3 ships SNAPSHOT + RCSI for INSERT/UPDATE). **Phases 0 + 1a + 1b + 2 + 3 ship**: schema-stability locks (Sch-S / Sch-M) including ALTER PROCEDURE / TRIGGER / SEQUENCE; the full 8-mode data + intent matrix (Sch-S / Sch-M / IS / IX / SIX / S / U / X); row-level X on writers and probe-only / row-S / row-U / row-X on readers depending on hints + isolation level; row-X on history-table / cascade-FK / OUTPUT-INTO / SELECT-INTO destination writes; alias-form `UPDATE x SET … FROM t x JOIN …` table-IX acquired after FROM-target identification; SERIALIZABLE / HOLDLOCK hint upgrades read to table-S tx-scoped (phantom prevention at table granularity since real SQL Server's key-range locks require index range structure the simulator doesn't have); REPEATABLE READ / REPEATABLEREAD hint retains row-S tx-scoped; UPDLOCK retains row-U tx-scoped; XLOCK retains row-X tx-scoped on reads; READPAST skips conflicting rows; TABLOCK / TABLOCKX escalate to table-S / table-X; NOLOCK / READUNCOMMITTED dirty-read; lock escalation at 5000 per-tx per-table row-locks → table-X; cross-thread waiter-graph cycle detection (Msg 1205); same-thread immediate-deadlock short-circuit; `SET LOCK_TIMEOUT N` driving Msg 1222 on expiry; `SET TRANSACTION ISOLATION LEVEL` mutating session state with semantic effect; auto-rollback of the victim's tx on Msg 1205 (probe-confirmed `@@TRANCOUNT = 0` in CATCH); `@@LOCK_TIMEOUT` / `@@SPID` scalar functions; hint-conflict detection (Msg 1047 `NOLOCK + XLOCK/UPDLOCK/HOLDLOCK/…` verbatim, Msg 1065 `NOLOCK on DML target` verbatim, Msg 1069 `INDEX hint on DML target` verbatim); `sys.dm_tran_locks` + `sys.dm_os_waiting_tasks` DMVs projecting GRANT / WAIT rows over the live lock-manager state; **SNAPSHOT isolation + READ_COMMITTED_SNAPSHOT (phase 3)** via per-table `HeapTable.RowVersions` version chains + monotonic per-database `Database.AllocateTransactionCommitId` Xid allocator + `ALTER DATABASE … SET (ALLOW_SNAPSHOT_ISOLATION | READ_COMMITTED_SNAPSHOT) { ON | OFF }` (the WITH NO_WAIT / WITH ROLLBACK IMMEDIATE termination options that real SQL Server rejects with Msg 5083 on versioning-state changes aren't modeled; the simulator's flip takes effect immediately, skipping real SQL Server's brief-stabilization-wait and single-connection-required gates). SI-session-on-user-table with ASI=OFF raises `Msg 3952` verbatim at first user-table access (probe-confirmed: `SET TRANSACTION ISOLATION LEVEL SNAPSHOT` is silent, system-catalog reads bypass the gate). Snapshot Xid is allocated lazily at first user-table read (per-tx for SI, per-statement for RCSI; carried in `SimulatedDbTransaction.SnapshotXid` / `BatchContext.RcsiStatementSnapshotXid`). Writers under either flag capture pre-mutation payloads to the chain via `VersionStore.CaptureWrite`; INSERT creates a fresh chain entry, UPDATE attaches a pending `HistoricalVersion` carrying the pre-write payload eagerly (so concurrent SI readers see it before the writer commits), DELETE marks the chain as deleted-live at commit. Tx-scoped `SimulatedDbTransaction.PendingVersionEntries` finalizes on COMMIT (one commit Xid for the whole batch, stamps `HistoricalVersion.Xmax` and `RowVersionChain.LiveXmin`) and discards on ROLLBACK (the heap row is restored by the undo log; the chain mark is cleared). SI update-conflict raises `Msg 3960` verbatim from `VersionStore.CheckSnapshotUpdateConflict` at the top of `CommitUpdate` / `CommitDelete` when the live row at the target Rid has `LiveXmin > snapshotXid` or a foreign `WriterTx`; auto-rolls back the SI tx before throwing (probe-confirmed `@@TRANCOUNT = 0`). RCSI default-RC readers consult the chain through `BatchContext.ResolveSnapshotXidForRead` → `VersionStore.ResolveVisibleVersion`. Phase-3 limitations: DELETE-visible-to-SI-snapshot (heap iteration skips tombstoned slots — SI readers can't see committed-deleted rows their snapshot would still see); SI-writer-on-deleted-row Msg 3960 (same root cause); multi-update-within-one-tx history collapse; version-store garbage collection; `sys.dm_tran_version_store` DMV. See [`docs/claude/locking.md`](docs/claude/locking.md). `BEGIN DISTRIBUTED TRANSACTION` and `BEGIN TRANSACTION ... WITH MARK` raise `NotSupportedException` at dispatch. **Other `SET <option> …` statements are parsed-and-discarded** via a closed accept-list in `Simulation.Set.cs` — `SET XACT_ABORT ON|OFF`, the ANSI / session-state toggles (ANSI_NULLS / QUOTED_IDENTIFIER / ANSI_WARNINGS / ANSI_PADDING / CONCAT_NULL_YIELDS_NULL / ARITHABORT / NUMERIC_ROUNDABORT / …) including the multi-option comma form EF Core emits, value-taking options (TEXTSIZE / DATEFIRST / ROWCOUNT / DATEFORMAT / LANGUAGE / DEADLOCK_PRIORITY / CONTEXT_INFO), and the `SET STATISTICS {IO|TIME|XML|PROFILE} ON|OFF` sub-form. SNAPSHOT isolation level parses-and-discards (behaves as RC). Unknown SET option followed by ON/OFF/value → Msg 195 verbatim (`'<name>' is not a recognized SET option.`); no-trailing-token form → generic Msg 102 (probe-confirmed split). `SET @v = expr`, `SET IDENTITY_INSERT`, `SET NOCOUNT ON|OFF`, `SET IMPLICIT_TRANSACTIONS ON|OFF`, `SET LOCK_TIMEOUT N`, and `SET TRANSACTION ISOLATION LEVEL {…}` have semantic effect; everything else in the accept-list parses-and-discards.
163163
- `RIGHT JOIN` / `FULL OUTER JOIN` with a derived-table or lateral right side — base tables / views / system tables on the right ship (see JOINs / APPLY); a derived-table right (always-deferred in this simulator) raises `NotSupportedException` since real SQL Server's Msg 4104 rejection of correlated subqueries on the right of RIGHT/FULL can't be statically distinguished from non-correlated ones.
164164
- Comma-separated FROM (legacy ANSI-89 join syntax).
165165
- `RANGE BETWEEN <N> PRECEDING` / `<N> FOLLOWING` — real SQL Server gates the numeric-offset RANGE form behind a separately-licensed feature surface and the simulator matches that rejection (Msg 4194). `ROWS` numeric-offset frames ship; both modes support the canonical `UNBOUNDED` / `CURRENT ROW` bounds and the single-bound shorthand (`ROWS UNBOUNDED PRECEDING``ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`). The default frame when ORDER BY is present in OVER is `RANGE UNBOUNDED PRECEDING TO CURRENT ROW` (running-total semantic with peer-tie grouping); without ORDER BY, default frame is whole partition. LAST_VALUE ships with the same semantics as real SQL Server (its default frame returns the current row's value or the peer-tie last under RANGE — the intuitive "partition last" form needs explicit `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`).

0 commit comments

Comments
 (0)