Skip to content

Commit 5d18243

Browse files
committed
Restructure the function-coverage TODO into a living docs/claude/backlog.md (completed items removed; organized into missing-features / fidelity-gaps / design-choices-to-revisit / won't-model with a completion-loop preamble), migrate durable conventions and project facts out of local agent-memory into CLAUDE.md + Tests/CLAUDE.md (and the BIN2 code-unit guard into collations.md), trim CLAUDE.md.
1 parent c409e01 commit 5d18243

5 files changed

Lines changed: 101 additions & 175 deletions

File tree

CLAUDE.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ Auto-loaded orientation. `README.md` is for humans.
88

99
`SqlServerSimulator.EFCore` is a sibling package whose only public method is `UseSqlServerSimulator(DbContextOptionsBuilder, DbConnection)`. EF Core's SqlServer provider keeps emitting SQL-Server-flavored SQL; the adapter just registers an `IRelationalTypeMappingSourcePlugin` for the (CLR, store) pairs whose default mappings downcast to `SqlParameter` (since the simulator's connection isn't a `SqlConnection`).
1010

11+
**Packaging:** only `SqlServerSimulator` publishes; `SqlServerSimulator.EFCore` and `Example` are `IsPackable=false` — the adapter stays in-repo-but-unpublished as a deliberate demand signal, so don't pitch publishing it without a user request.
12+
1113
## Operating goal
1214

13-
High-fidelity emulation. Authenticity over desirability — when SQL Server's behavior is quirky or lossy (CP1252 `?` replacement, ANSI trailing-space `=` padding, `LEN` excluding trailing spaces), mirror it. EF Core trusts the simulator end-to-end (`*.Tests.EFCore` is the regression oracle and must stay green). Beyond that floor, priority is broad SQL Server coverage weighted by popularity (user wins) and ease (thoroughness wins). The function-coverage backlog at [`docs/claude/function-coverage-todo.md`](docs/claude/function-coverage-todo.md) is sorted by that weighting and tracks category-completion milestones — read it before pitching a new built-in function.
15+
High-fidelity emulation. Authenticity over desirability — when SQL Server's behavior is quirky or lossy (CP1252 `?` replacement, ANSI trailing-space `=` padding, `LEN` excluding trailing spaces), mirror it. Fidelity has caught real upstream bugs (EF Core 10's `Math.Sign(decimal)` int/decimal mismatch) — when a probe feels wrong, verify against the reference before relaxing the simulator; matching a real cross-stack quirk is the feature, not a bug to paper over. EF Core trusts the simulator end-to-end (`*.Tests.EFCore` is the regression oracle and must stay green). Beyond that floor, priority is broad SQL Server coverage weighted by popularity (user wins) and ease (thoroughness wins). The living [`docs/claude/backlog.md`](docs/claude/backlog.md) — missing features, fidelity gaps, design choices to revisit, deliberate exclusions — is ordered by that weighting but non-authoritatively; read it before picking up new feature work or pitching a built-in.
1416

1517
## Feature-bundle workflow
1618

17-
1. **Probe.** Behavior questions get answered against the real SQL Server 2025 reference instance (connection details in user memory). Probe scaffolds live in `/tmp/<probe-name>/`; deleted after the bundle. Only graduated regression tests land in `*.Tests` / `*.Tests.EFCore`.
18-
2. **Surface decisions.** Before writing code, surface 2–3 concrete design choices and recommend one each.
19-
3. **Implement + test.** `*.Tests` exercises public API; `*.Tests.EFCore` validates the oracle. `*.Tests.Internal` only for things genuinely unreachable from public SQL.
19+
1. **Probe.** Behavior questions get answered against the real SQL Server 2025 reference instance (connection details in user memory). Probe scaffolds live in `/tmp/<probe-name>/`; deleted after the bundle. Only graduated regression tests land in `*.Tests` / `*.Tests.EFCore`. EF Core probes against the reference clean up with `DROP TABLE IF EXISTS`, **never** `Database.EnsureDeleted()` (the login can drop but not recreate the reference DB). A probe reveals *server* behavior, not *doc* content: don't write "Microsoft's docs claim X but…" without actually reading the MSDN page.
20+
2. **Surface decisions.** Before writing code, surface 2–3 concrete design choices and recommend one each. Before pitching a built-in as net-new, grep `Parser/Expression.cs:ResolveBuiltIn` + `Parser/Expressions/` — the What's-modeled catalog lags the code, and variants ride a flag on the existing class (`tryMode` on Convert, `isBig` on DateDiff, `kind` discriminators), not a new one.
21+
3. **Implement + test.** `*.Tests` exercises public API; `*.Tests.EFCore` validates the oracle. `*.Tests.Internal` only for things genuinely unreachable from public SQL. `*.Tests.EFCore` must drive EF Core's LINQ→SQL emission (and C#-side surface like `HasTrigger` / `UseHiLo` / `UseTpcMappingStrategy` that shifts the emit shape across EF versions), **not** hand-written SQL through `FromSql*` / `SqlQuery` — that's parser testing in disguise (covered once by `EFCoreFromSql.cs`).
2022
4. **Update CLAUDE.md and `docs/claude/`.** Move bullets between What's-modeled / Not-modeled as scope changes. Deep-dive catalogs — *and feature-specific quirks/divergences* — live under `docs/claude/`; CLAUDE.md's Quirks section is reserved for cross-cutting divergences with no single feature-doc home (keep it short).
21-
5. **Single-sentence commit.** Squashes capture end state. Don't run `git commit` — the user holds signing credentials.
23+
5. **Single-sentence commit.** Squashes capture end state. Focus the message on *what changed* and *why*, plus probe-confirmed facts (Msg numbers, wording, semantic decisions) future-me would otherwise re-derive from the diff; omit CI-visible status (test counts, build / `dotnet format` state) — GitHub surfaces that and it goes stale. Don't run `git commit` — the user holds signing credentials.
2224

2325
Behavioral claims below were probed against a live SQL Server 2025 reference instance unless flagged otherwise.
2426

@@ -91,12 +93,15 @@ Six scopes, one home each. **Add new state to whichever class matches its true s
9193
- **No temporal words in code comments** — "currently", "now", "yet", "new" age badly.
9294
- **No comments inside expression chains** — IDE0055 fails on comments in ternary chains or between `=>` and body. Restructure or hoist to XML doc.
9395
- **Fields over auto-properties on non-public types** (SSS001 generalized).
96+
- **No internal `<see cref>` in public-API XML docs** — a cref to an internal type dangles in consumer IntelliSense and implicitly promises stability for a name we're free to rename; state the contract in prose instead (`"an unrecognized collation name raises ArgumentException"`, not a cref to internal `Collation.IsRecognized`).
97+
- **No conversation-scratch framing in code/docs/commits** — "Camp A/B", "this bundle", "Stage 1/2", "as we discussed" carry no meaning to a future reader; describe behavior/motivation absolutely, and cross-reference a sibling by the behavior it names, not the work-stage that produced it. Pre-existing repo terms (e.g. the transactions feature's "Bundle 1/2") are load-bearing, not transient — leave them.
9498
- **AssemblyHooks**: each test project has `AssemblyHooks.cs` with a `static [TestClass] [AssemblyInitialize]` to warm shared initialization paths once before the parallel test run. Without it, the first batch of tests races to initialize hot shared state and serializes on contention. The analyzer-tests' Roslyn-cache warm-up is the most extreme case observed (~3x slowdown), but the pattern generalizes to any expensive first-touch shared resource.
9599

96100
## SimulatedSqlException vs NotSupportedException
97101

98102
- `SimulatedSqlException` for behavior matching SQL Server: invalid SQL, type mismatches, constraint violations, oversize columns, truncation. Mirrors number/class/state/message.
99103
- `NotSupportedException` for valid SQL Server features the simulator hasn't built. Name the unmodeled feature.
104+
- **Control flow signals via flags, not exceptions.** BREAK / CONTINUE / RETURN / THROW set a typed flag on `BatchContext` (`LoopControl` + the skip predicate), never a signal exception — `yield return` inside try/catch composes badly with the iterator-based dispatch, and the flag reuses the same skip-mode plumbing that no-ops un-taken IF branches. Parse-time structural checks (BREAK outside WHILE → Msg 135) still fire regardless of skip state. Exceptions stay for true error conditions.
100105

101106
## What's modeled
102107

@@ -107,17 +112,17 @@ The `*.Tests` and `*.Tests.EFCore` suites are the authoritative behavior contrac
107112

108113
### Constraints
109114
- `CHECK`: inline single-column and table-level forms; Msg 547 per row on definitely-false predicate. Inline column-level CHECK predicates may only reference their owning column — peer references raise **Msg 8141** at CREATE TABLE (probe-confirmed verbatim wording). The walker is structural via `Expression.VisitColumnReferences` + `BooleanExpression.VisitOperandExpressions`; coverage is currently limited to common container subclasses (`Reference`, `Parenthesized`, `TwoSidedExpression`, `Cast`, `Length`) — peer refs buried in less-common containers (`DATEPART`, `SUBSTRING`, nested `CASE`, etc.) silently escape the CREATE-TABLE check and surface at INSERT instead. Table-level CHECK has no peer restriction.
110-
- `PRIMARY KEY` / `UNIQUE` / secondary `CREATE INDEX`: linear scan (O(N) per insert); no B-tree. Reads get **incrementally-maintained** per-`Heap` seek acceleration (no per-mutation warm-up), identical across keys and non-clustered indexes: equality / IN on the longest leading prefix, range on a leading key column, ORDER BY elimination, and keyset pagination. The same narrowing accelerates `UPDATE` / `DELETE` target scans and `MERGE` (via loop inversion). See [`indexes.md`](docs/claude/indexes.md) for the seek shapes, mutation/MERGE seeking, journal mechanics, decline rules, and the residual-WHERE correctness invariant.
111-
- `FOREIGN KEY`: inline / table-level / named forms; all four referential actions on `ON DELETE` / `ON UPDATE`; enforced at INSERT / UPDATE / DELETE / MERGE; full `sys.foreign_keys` / `sys.foreign_key_columns`. Enforcement **seeks the shared `HeapSeekCache`** (the same per-`Heap` index the query path uses): child-insert parent-existence probes the parent's PK/UNIQUE; parent-delete/update cascades seek the child's FK columns — both verifying candidates against live bytes (no residual WHERE), with a full-scan fallback only for non-stored key columns. Referential-action, cascade-cycle, PK/UNIQUE-target, and NULL-skip rules + Msg numbers in [`foreign-keys.md`](docs/claude/foreign-keys.md).
115+
- `PRIMARY KEY` / `UNIQUE` / secondary `CREATE INDEX`: linear scan (O(N) per insert), no B-tree; reads and `UPDATE` / `DELETE` / `MERGE` target scans get **incrementally-maintained** per-`Heap` seek acceleration (equality / IN / leading-column range / ORDER BY elimination / keyset). Seek shapes, mutation/MERGE seeking, journal mechanics, decline rules, residual-WHERE invariant in [`indexes.md`](docs/claude/indexes.md).
116+
- `FOREIGN KEY`: inline / table-level / named forms; all four referential actions on `ON DELETE` / `ON UPDATE`; enforced at INSERT / UPDATE / DELETE / MERGE; full `sys.foreign_keys` / `sys.foreign_key_columns`. Enforcement **seeks the shared `HeapSeekCache`** (live-byte verified, no residual WHERE). Referential-action, cascade-cycle, PK/UNIQUE-target, and NULL-skip rules + Msg numbers in [`foreign-keys.md`](docs/claude/foreign-keys.md).
112117

113118
### Transactions
114119
Three entry points share one per-connection undo log: implicit (statement-level atomicity), SqlClient API (`BeginTransaction()`/`Commit()`/`Rollback()`), SQL-text (`BEGIN`/`COMMIT`/`ROLLBACK`/`SAVE TRANSACTION`).
115120

116121
- **Statement-level atomicity**: a single mutation throwing mid-execution rolls back its partial writes. Multi-row INSERT failing on row 3 leaves zero rows.
117122
- **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.
118123
- `@@TRANCOUNT` reads connection depth as int.
119-
- **Identity counters and the database-scoped rowversion counter bypass the log** — both keep advancing through rollback. (A rolled-back INSERT's off-row LOB chain and its heap-page row-payload bytes are both reclaimed — the undo marks the slot reclaimable, since rollback is terminal and an uncommitted insert is invisible to every snapshot.)
120-
- **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 (the temp-vs-regular DDL asymmetry is detailed in [`temp-tables.md`](docs/claude/temp-tables.md)).
124+
- **Identity counters and the database-scoped rowversion counter bypass the log** — both keep advancing through rollback. (A rolled-back INSERT's off-row LOB chain and heap-page bytes are reclaimed — rollback is terminal, so an uncommitted insert is invisible to every snapshot.)
125+
- **Temp-table CREATE/DROP participates in the log** via `TempTableCreation` / `TempTableRemoval` `UndoEntry` subtypes. Regular CREATE/DROP TABLE is NOT logged the temp-vs-regular DDL asymmetry is detailed in [`temp-tables.md`](docs/claude/temp-tables.md).
121126
- Locking + MVCC: full 8-mode matrix, row-X writers + row-mode readers per hints/iso, RR/SER/UPDLOCK/XLOCK/TABLOCK/HOLDLOCK/REPEATABLEREAD/NOLOCK/READPAST hints, escalation at 5000 row-locks, Msg 1205 deadlock detection, Msg 1222 timeouts, SNAPSHOT + RCSI with version chains + GC + DMVs. See [`docs/claude/locking.md`](docs/claude/locking.md).
122127

123128
### EF Core adapter coverage
@@ -177,7 +182,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
177182
- **Key-range locks** — sole remaining phase 4+ deferral. See [`locking.md`](docs/claude/locking.md) for what does ship (full 8-mode matrix, SNAPSHOT/RCSI, DMVs, Msg 1205/1222/3952/3960).
178183
- `BEGIN DISTRIBUTED TRANSACTION` raises `NotSupportedException` at dispatch. `BEGIN TRANSACTION <name> WITH MARK 'm'` raises **Msg 319** at parse (the parser doesn't accept `WITH` here); bare named transactions (`BEGIN TRAN t1`) ship.
179184
- **`CREATE DATABASE`** / **`CREATE ASSEMBLY`** — Msg 102 at parse. Adding databases routes through `Simulation.ImportBacpac`; CLR assemblies aren't modeled at all.
180-
- **Cross-database / cross-server DML** (`INSERT`/`UPDATE`/`DELETE`/`MERGE` through a 3- or 4-part name targeting a different database or linked server) raises `NotSupportedException` via `BatchContext.RejectCrossDatabaseMutation` — issue `USE <db>` and use a 1-/2-part name. Cross-database and four-part-name *reads* (SELECT / JOIN, the latter through the remote `Simulation`'s full ADO.NET pipeline) ship; catalog-view reads through four-part names (`srv.db.sys.tables`) fall through to Msg 208 — query the remote `Simulation` directly. See [`schemas.md`](docs/claude/schemas.md), [`linked-servers.md`](docs/claude/linked-servers.md).
185+
- **Cross-database / cross-server DML** (`INSERT`/`UPDATE`/`DELETE`/`MERGE` through a 3-/4-part name to another database or linked server) raises `NotSupportedException` via `BatchContext.RejectCrossDatabaseMutation` — issue `USE <db>` first. Cross-database / four-part-name *reads* (SELECT / JOIN) ship; catalog-view reads through four-part names (`srv.db.sys.tables`) fall through to Msg 208. See [`schemas.md`](docs/claude/schemas.md), [`linked-servers.md`](docs/claude/linked-servers.md).
181186
- **`SET <option>` accept-list** (`Simulation.Set.cs`) covers XACT_ABORT, all ANSI/session-state toggles, `STATISTICS {IO|TIME|XML|PROFILE}`, value-taking options (`TEXTSIZE`/`DATEFIRST`/etc.) — all parse-and-discard. Unknown SET → Msg 195. `SET @v`, `IDENTITY_INSERT`, `NOCOUNT`, `LOCK_TIMEOUT`, `TRANSACTION ISOLATION LEVEL` carry semantic effect.
182187
- **`ALTER DATABASE … SET` / `COLLATE` surface** — see [`database-options.md`](docs/claude/database-options.md). Most options parse-and-discard; `COMPATIBILITY_LEVEL`, `ALLOW_SNAPSHOT_ISOLATION`, `READ_COMMITTED_SNAPSHOT` are load-bearing.
183188
- `RANGE BETWEEN <N> PRECEDING/FOLLOWING` numeric-offset — Msg 4194, matching real SQL Server's licensed-feature rejection. `ROWS` numeric-offset ships. Default frame with ORDER BY is `RANGE UNBOUNDED PRECEDING TO CURRENT ROW`; without it, whole partition. LAST_VALUE matches real SQL Server's default-frame semantic.

SqlServerSimulator.Tests/CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public void TableLevel_Composite_Match_Succeeds()
2121

2222
`Simulation.ExecuteScalar` / `ExecuteNonQuery` accept a multi-statement raw string; the final `SELECT`'s first value is the scalar return. Don't split CREATE + INSERT + SELECT into three calls unless a later assertion needs an intermediate observation.
2323

24+
**Batching trap — `ExecuteNonQuery` returns the *sum* of rows-affected across all DML in the batch** (faithful to TDS DONE tokens; DDL like CREATE TABLE doesn't contribute). So `AreEqual(1, sim.ExecuteNonQuery("…; insert 2 rows; delete 1 row"))` measures 3, not the DELETE's 1, and fails for the wrong reason. Safe to fold setup into the asserted call only for `ExecuteScalar` (DML counts don't leak in) and `AssertSqlError` (throws). When the assertion *is* the `ExecuteNonQuery` row count, keep setup in a separate call so the measured statement is isolated.
25+
2426
Canonical example: [`CheckConstraintTests.cs`](CheckConstraintTests.cs). The full set of helpers ships in [`Extensions.cs`](Extensions.cs).
2527

2628
## Multi-assertion shape
@@ -82,6 +84,12 @@ _ = context.Items.Add(new Item { ... }); // returns EntityEntry
8284

8385
`ExecuteScalar` returning `object?` only needs `_ =` when discarded — wrap with `AreEqual(...)` when asserting.
8486

87+
## Collation / Unicode fixtures
88+
89+
- **Literal `N'…'`** for any character with a distinctive visible glyph (``, `ƒ`, `Ÿ`, ``, `café`) — the literal *is* the explanation, no `U+xxxx` comment needed.
90+
- **`nchar(N)` (decimal, never `0xNN`)** only for invisible / ambiguous characters: NBSP (U+00A0, renders like a space), Private Use Area (no glyph), surrogate pairs (`nchar(55357) + nchar(56832)` for 😀). Hex `nchar(0x…)` fails — the parser reads `0xNN` as varbinary and the varbinary→int coercion isn't implemented.
91+
- **Skip the `cast(<literal> as varchar(N))` / `char(N)` on INSERT** — assignment-time coercion handles `N'…'``varchar(N)` and `nchar(N)``char(N)` automatically; the cast is noise.
92+
8593
## Probe code is not a style reference
8694

8795
Probe scaffolds in `/tmp/<feature>/` are throwaway data-collection scripts: bare `using` blocks, `Console.WriteLine`, no analyzer enforcement. When the graduated tests land in this directory, **don't mirror the probe's shape** — go directly to the dense form above. Probes get deleted after the feature bundle commits.

0 commit comments

Comments
 (0)