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
AreEqual("Internal error: Server stack limit has been reached. Please look for potentially deep nesting in your query, and try to simplify it.",ex.Message);
Copy file name to clipboardExpand all lines: docs/claude/backlog.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,6 +57,7 @@ Real bugs / limitations against shipped behavior — fixes are concrete work, no
57
57
-**`SimulatedSqlException` ERROR tokens carry `Server=''` / `Line 0`** — real SQL Server fills the server name and a 1-based line number on every error; the simulator's query errors surface blank/0 both in-process and over the wire (sqlcmd renders `Server , Line 0`). Inconsistently, `NotSupportedException`-derived wire errors *do* carry `Server=SIMULATED, Line 1`. Engine-level: the fix is stamping `SimulatedError.Server` (from `SERVERPROPERTY('ServerName')` = `SIMULATED`) at construction and threading real line numbers from the tokenizer — the latter is the large part. Harvested from the go-sqlcmd shakedown (2026-07-14).
58
58
-**String literals typed at container width, not value width** — `SELECT 'included'` advertises `varchar(8000)` (and `@@VERSION``nvarchar(4000)`) in projection schema; real types literals at exact length (`varchar(8)`). Correct data, but clients that size display columns from COLMETADATA (sqlcmd) render absurdly wide output, and `GetSchemaTable`-driven consumers see wrong sizes. Fix lives in literal `SqlType` inference (`Literal` → length-parameterized type), with CONCAT/CASE/set-op width-combination ripple effects — probe real's width algebra before starting. Harvested from the go-sqlcmd shakedown (2026-07-14).
59
59
- **Skip-mode deferred name resolution abandons the enclosing statement mid-parse** — a statement skipped because the batch is in error/skip mode (e.g. inside an un-taken `IF`) whose deferred table/column resolution misses raises Msg 207/208 and *throws out of the middle of the parse* (throw-and-recover) instead of parsing the whole statement and discarding it the way real SQL Server compiles-then-skips. So `IF EXISTS (SELECT … FROM <missing>) <then> ELSE <else>` inside a skipped region orphans the `<then>` / `ELSE` fragments, which the dispatch loop then re-enters as bare statements — surfacing spurious Msg 102/156 (a bare `ELSE` is a structural error). The dispatch-loop non-progress guard (`Simulation.DispatchStatementsUntil`) bounds the damage; pre-guard this infinite-looped the wire path (the SSMS Query Store probe crash of 2026-07-15, since the abandoned recovery scan advanced zero tokens with the cursor already on a statement-boundary token). The real fix is parse-continuation: skip-mode resolvers returning placeholder metadata instead of throwing, so a skipped statement parses to completion and is discarded whole. Open. See [`control-flow.md`](control-flow.md).
60
+
-**Expression-chain depth ceiling far below real's** — expression parsing recurses per binary operator with fat `Expression.Parse` frames, so the Msg 8631 stack-probe guard (see [`grammar.md`](grammar.md) "Expression nesting limits") fires near 750 chained operators on a default 1 MB thread where the reference server handles 3000+ (its own Msg 8631 fires between 3000 and 6000). Graceful today (no process-fatal overflow — the guard shipped 2026-07-15 after `LargeStatement_NtextValue_DecodesExactly`'s 3000-term chain stack-overflowed Windows Release test runs), but lifting the ceiling toward real's requires restructuring the binary-operator parse from ctor-recursion to iterative precedence-climbing (which would also obsolete `AdjustForPrecedence`'s post-hoc rotation) and bounding `Run` / `GetSqlType` tree recursion. Sizeable parser-core change; low real-world demand (generated SQL rarely nests past tens).
60
61
-**Trailing-space MIN/MAX representative** — for a group of values differing only in trailing spaces (sort-equal under SQL Server), MIN/MAX returns a different byte-variant than the live server's scan-order representative. Surfaced by the AdventureWorks crosscheck on synthetic XML data (`vJobCandidateEducation._max_Edu_Loc_CountryRegion`). Needs trailing-space-insensitive compare + SQL Server's unspecified MAX-tie scan-order. See [`collations.md`](collations.md) "byte-exact sort" trailing-space note. **Deferred** — synthetic data, and the representative is unspecified scan-order on the live side.
61
62
- **Leaked-connection session cleanup** — a `SimulatedDbConnection` that's never `Dispose`d never reclaims its session state: an open transaction holds its locks and pins the MVCC version store, `##temp` tables linger, session-owned app locks stay held, and the SPID accumulates. Real SqlClient's GC-finalization eventually closes a leaked connection and the server resets the session, so this is a genuine fidelity divergence. **Scope correction (2026-07-11): the fix is bigger than "weaken the registry."** Investigation found *three* global strong-reference cycles that pin exactly the connections that hold session state, so GC can't collect them and a finalizer never fires: (1) `LockResource.Hold.Owner` is a strong `SimulatedDbConnection` (reachable Database → table → lock → hold) — pins any lock- or session-app-lock-holding connection; (2) `HeapTable.OwnerConnection` is strong and `Simulation.GlobalTempTables` holds the table — pins any `##temp` owner; (3) `Database.ActiveSnapshotTxs` holds the transaction, which strongly refs its connection — pins any open-snapshot session. Weakening `Simulation.Connections` alone accomplishes nothing because the resource *is* the pin. A correct fix must break all three cycles — cleanest via a one-way `SessionToken` indirection (resources reference a lightweight token identity; the connection references the token, not vice versa) plus a finalizer that enqueues a **deferred teardown** drained on a normal worker thread (next `CreateDbConnection` / version-store GC) so transaction rollback stays off the finalizer thread. This is a broad, mechanical owner-indirection refactor landing on the most regression-sensitive subsystem (lock manager × GC timing × threading). Payoff is bounded (EF disposes scrupulously; only buggy consumer code leaks), so it's **deliberately deferred** as high-risk / low-frequency. Eventual home: [`locking.md`](locking.md).
62
63
-**Workload-harness divergence reporting quirks** (`.vs/workload/Program.cs`, local-only) — the parity report's example line rebuilds parameters from the op seed and can mismatch the actual divergent instance, and divergent instances aren't re-run single-threaded to classify transient-vs-stable. Both made the 2026-07-10 shared-plan-state hunt slower than it needed to be (the fixed bug class itself — instance-bound aggregate/window results, baked TOP/OFFSET counts, frozen RAND, unstamped replay clock — is documented in [`plan-cache.md`](plan-cache.md)'s shared-plan contract section).
-**Per-object creation-time QI capture is NOT modeled.** Real SQL Server stamps procedures / views / triggers / tables with the `QUOTED_IDENTIFIER` in effect at CREATE time (`sys.sql_modules.uses_quoted_identifier`, `OBJECTPROPERTY(id, 'IsQuotedIdentOn')`) and executes their bodies under that captured setting regardless of the caller's session. The simulator re-parses bodies under the **executing session's** current setting instead. Rare legacy pattern (creating an object under a non-default QI and relying on the stamp); most code runs everything under the default ON.
44
44
-**Multi-statement-TVF bodies treat a `SET QUOTED_IDENTIFIER` as top-level** rather than rejecting it (real SQL Server disallows `SET QUOTED_IDENTIFIER` inside a function body).
45
+
46
+
## Expression nesting limits (Msg 8631 / Msg 191)
47
+
48
+
Expression parsing recurses per binary operator and grouping level (`TwoSidedExpression`'s parsing ctor re-enters `Expression.Parse` for its right side), and a .NET stack overflow is uncatchable and process-fatal — unacceptable for an in-process library handed a pathological query. Two graceful limits ship instead, both probed against SQL Server 2025 (2026-07-15):
49
+
50
+
-**Msg 8631, Class 17** (`ServerStackLimitReached()`): `Internal error: Server stack limit has been reached. Please look for potentially deep nesting in your query, and try to simplify it.` Raised from a stack probe (`RuntimeHelpers.EnsureSufficientExecutionStack()`) at the top of `Expression.Parse` — the faithful mechanism, since real's Msg 8631 is likewise a genuine stack probe with a stack-dependent threshold (reference: a 3000-term `1 + 1 + …` chain succeeds, 6000 fails). Every recursive parse path (boolean chains, CASE, function arguments, subquery projections) passes through `Expression.Parse` at least once per nesting level, so the single guard site bounds them all, and it composes across nested proc/dynamic-SQL batches because it measures the real stack.
51
+
-**Msg 191, Class 15** (`StatementNestedTooDeeply()`): `Some part of your SQL statement is nested too deeply. Rewrite the query or break it up into smaller queries.` Structural counter on grouped-expression parens (`ParserContext.GroupingDepth`, limit 512 in `Expression.MaxGroupingDepth`).
52
+
53
+
**Divergences**: the simulator's tolerated operator-chain depth is well below real's because its parse frames are fat — measured on a default 1 MB thread, Msg 8631 fires near 750 levels (Release) / 600 (Debug), where the reference server handles 3000+ terms. The Msg-191 paren limit (512) is likewise below real's (1000 parens succeed there, 2000 fail) so the structural error deterministically beats the stack probe on 1 MB threads. Lifting the chain-depth ceiling toward real's requires the iterative/precedence-climbing parse restructure tracked in the backlog.
0 commit comments