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
Copy file name to clipboardExpand all lines: CLAUDE.md
+28-5Lines changed: 28 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -62,9 +62,9 @@ Five scopes, one home each. **Add new state to whichever class matches its true
62
62
63
63
-**`Simulation`** = server / instance. Process-shared system tables (`SystemHeapTables`), `NEWSEQUENTIALID` anchor + counter, the `Databases` dictionary. Public surface (`Simulation` ctor + `CreateDbConnection()`) stays on this class.
64
64
-**`Database`** (internal) = one database hosted by the server instance. `Schemas` (named-schema dict, pre-seeded with `dbo`), `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
-
-**`Schema`** (internal) = one namespace inside a database. `HeapTables` (per-schema table dict). Future views / procs / sequences land here too. Schema-qualified references (`SELECT * FROM audit.t`) route through `Database.Schemas["audit"].HeapTables`; unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`).
- **`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(MultiPartName)` — the routing rule that dispatches `#foo` leaves to `Connection.TempTables` regardless of qualifier; everything else routes through the named schema (or `dbo` for an unqualified reference), with `SystemHeapTables` reachable only as a flat 1-part fallback. `TryResolveSchema(MultiPartName)` exposes the dict-bearing schema for CREATE / DROP / TRUNCATE / SELECT INTO. `ParseObjectName(ParserContext)` parses the 1–4-segment dotted form, leaves cursor on the last name segment (standard parser contract), and compresses empty middle segments (so `tempdb..#foo` returns a 2-part name). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`.
65
+
-**`Schema`** (internal) = one namespace inside a database. `HeapTables` (per-schema table dict), `Functions` (scalar UDFs). Future views / procs / sequences land here too. Schema-qualified references (`SELECT * FROM audit.t`, `SELECT audit.fn(x)`) route through `Database.Schemas["audit"]`; unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`).
- **`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`, `UdfFrame` (non-null when this batch is a scalar-UDF body being dispatched — gates value-form `RETURN` and lands the return value for the caller), plus the per-statement frame `CurrentStatement`. Exposes `TryResolveTable(MultiPartName)` — the routing rule that dispatches `#foo` leaves to `Connection.TempTables` regardless of qualifier; everything else routes through the named schema (or `dbo` for an unqualified reference), with `SystemHeapTables` reachable only as a flat 1-part fallback. `TryResolveFunction(MultiPartName)` resolves 2-/3-part dotted names against the named schema's `Functions` dict; 1-part names return false (real SQL Server rejects bare UDF calls with Msg 195). `TryResolveSchema(MultiPartName)` exposes the dict-bearing schema for CREATE / DROP / TRUNCATE / SELECT INTO. `ParseObjectName(ParserContext)` parses the 1–4-segment dotted form, leaves cursor on the last name segment (standard parser contract), and compresses empty middle segments (so `tempdb..#foo` returns a 2-part name). Threaded explicitly into every `Expression.Run(RuntimeContext runtime)` call via `runtime.Batch`. Scalar-UDF invocation allocates a child `BatchContext` via the UDF-body constructor: parameters pre-seed `Variables`, `UdfFrame` is set, the body source text (captured at CREATE FUNCTION time) is re-tokenized through a synthesized `SimulatedDbCommand`, and the same dispatch loop runs the body.
68
68
-**`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.
69
69
70
70
**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.
@@ -477,6 +477,29 @@ Per-connection `Dictionary<string, HeapTable> TempTables` on `SimulatedDbConnect
477
477
-**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).
478
478
-**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`.
479
479
480
+
### Scalar user-defined functions
481
+
`CREATE FUNCTION schema.name(@p1 type [= default], ...) RETURNS <type> [WITH RETURNS NULL ON NULL INPUT] AS BEGIN ... END`, called as `SELECT schema.fn(args)`. Probed against SQL Server 2025 (2026-05-11).
482
+
483
+
-**2-part-name required.** Bare `fn(x)` raises **Msg 195** (`'fn' is not a recognized built-in function name.`) — real SQL Server treats unqualified UDF calls as built-in misses, never falls through to user-function resolution. Schema-qualified miss → **Msg 4121** (`Cannot find either column "dbo" or the user-defined function or aggregate "dbo.x", or the name is ambiguous.`).
484
+
-**BEGIN/END required for the body.** Probe-confirmed `as return @x*10` raises Msg 102 (no inline-return form for scalar UDFs). The body source is captured at CREATE time as the raw text between the outer `BEGIN` (exclusive) and matching `END` (exclusive) by token-level nesting (BEGIN TRAN / TRANSACTION / DISTRIBUTED don't open a block).
485
+
-**Multi-statement body**: `DECLARE` / `SET` / `SELECT @v = expr` / `IF` / `WHILE` / `BEGIN…END` / `BREAK` / `CONTINUE` / `RETURN <value>`. Variables declared inside the body are function-scoped (per-call child `BatchContext` with its own `Variables` dict).
486
+
-**`RETURN <value>` legal only inside a UDF body.** Outside, raises **Msg 178** at parse time (the existing batch-scope rejection). Inside, the value coerces to the declared return type and lands in `BatchContext.UdfFrame.ReturnedValue`; `BatchContext.ReturnSignaled` short-circuits the dispatch.
487
+
-**Argument arity errors**: too few → **Msg 313** (`An insufficient number of arguments were supplied for the procedure or function <name>.`); too many → **Msg 8144** (`Procedure or function <name> has too many arguments specified.`).
488
+
-**DEFAULT keyword required for parameter defaults.** Probe-confirmed `fn(default)` works but `fn()` raises Msg 313 even when every parameter has a declared default. The default expression is parsed once at CREATE time and re-evaluated per call in the child `BatchContext`. Bare omission is never legal.
489
+
-**WITH RETURNS NULL ON NULL INPUT**: when any non-DEFAULT argument is NULL, the body is skipped entirely and the function returns typed NULL. DEFAULT slots materialize from their stored expression and don't trigger the short-circuit on their own.
490
+
-**Recursion cap: 32.** Tracked by `SimulatedDbConnection.NestingLevel` (incremented in `Simulation.InvokeScalarFunction`, decremented in `finally`); exceeding raises **Msg 217** verbatim (`Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).`). Stored procs / triggers / views will share the same counter when added.
491
+
-**Catalog surface**: `sys.objects` emits rows with `type='FN'` / `type_desc='SQL_SCALAR_FUNCTION'`. New `sys.parameters` view emits one row per declared parameter (parameter_id=1+, `name='@<name>'`, `is_output=0`) plus one row with `parameter_id=0` for the return type (empty name, `is_output=1`). **`max_length` is 0** for v1 (computing per-type byte width for the bare SqlType — the existing `GetSysColumnMetadata` helper takes a HeapColumn — is fidelity-gap territory; the common queries read name / parameter_id / system_type_id / is_output / is_nullable).
492
+
-**`OBJECT_ID('schema.fn','FN')`** routes to function resolution; `'U'` filter returns NULL for a UDF; no-filter form tries function resolution first, then table. The two new modeled codes (`U` and `FN`) cover the load-bearing object-existence checks; other documented codes (V / P / TF / IF / ...) return NULL pending the corresponding features.
493
+
-**DROP FUNCTION [IF EXISTS] schema.name[, ...]**: same comma-list / IF EXISTS shape as DROP TABLE. Missing target → **Msg 3701** with the `function` wording variant (`Cannot drop the function 'dbo.x', because it does not exist or you do not have permission.` — same Msg/Class/State as DROP TABLE's variant, only the noun differs).
494
+
495
+
**Fidelity gaps** (modeled, document explicitly):
496
+
-**No CREATE-time body validation.** Real SQL Server raises **Msg 455** when the body's last statement isn't `RETURN`, **Msg 443** for `PRINT` / `THROW` / DML on permanent tables in the body, and **Msg 444** for result-set `SELECT` inside the body. The simulator defers all of these to call time — a body falling through without RETURN returns typed NULL; side-effecting statements raise their own errors. Apps that rely on CREATE-time rejection diverge.
497
+
-**No CREATE-FUNCTION-must-be-first-in-batch (Msg 111).** Real SQL Server requires `CREATE FUNCTION` to be the only/first statement in a batch (also true for `CREATE PROCEDURE` / `CREATE VIEW`). The simulator dispatches it like any other statement; preceding `IF OBJECT_ID('dbo.f','FN') IS NOT NULL DROP FUNCTION dbo.f; CREATE FUNCTION ...` in one batch works in the simulator but raises Msg 111 in real SQL Server. Apps must split the batches (no `GO` support).
498
+
-**`WITH SCHEMABINDING` / `WITH ENCRYPTION` / `WITH EXECUTE AS`**: raise `NotSupportedException` (only `WITH RETURNS NULL ON NULL INPUT` is modeled).
499
+
-**Inline TVFs (`RETURNS TABLE AS RETURN (SELECT ...)`)** and **multi-statement TVFs (`RETURNS @t TABLE (...) AS BEGIN ... END`)** aren't modeled — `CREATE FUNCTION` only accepts a scalar `RETURNS <type>` header today. Probe-confirmed real SQL Server treats inline TVFs as a separate object type (`'IF'`, `type_desc='SQL_INLINE_TABLE_VALUED_FUNCTION'`) reachable from a `FROM` clause; that path isn't wired.
500
+
-**`@@ROWCOUNT` inside a UDF body** isn't isolated — the body shares the connection's `LastStatementRowCount` counter with the caller. Real SQL Server preserves the caller's `@@ROWCOUNT` across UDF calls; the simulator currently lets body statements overwrite it. Workloads that read `@@ROWCOUNT` immediately after a UDF-call-in-SELECT-assignment may diverge.
501
+
-**EF Core doesn't emit scalar UDFs** from idiomatic LINQ; they're only reachable from raw SQL (`FromSqlInterpolated` / direct `ExecuteReader`) or via the `[DbFunction]` attribute on a C# method that maps to a UDF (not modeled).
502
+
480
503
### EF Core adapter coverage
481
504
`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`.
482
505
@@ -511,8 +534,8 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
511
534
-**`CREATE SCHEMA AUTHORIZATION <owner>`** — `NotSupportedException` (simulator has no user / principal model).
512
535
-**CREATE SCHEMA's `<schema_element>` greedy form** — real SQL Server consumes trailing CREATE TABLE / VIEW / GRANT as part of the same CREATE SCHEMA statement (and requires CREATE SCHEMA to be the first statement in the batch as a result). The simulator instead dispatches the trailing tokens as their own statements — same end state for the common idiom, but mismatched-grammar trailers (e.g. anything that isn't a recognized statement start) raise `NotSupportedException`.
513
536
-**`CREATE SCHEMA sys` / `INFORMATION_SCHEMA`** — raises Msg 2760 (matching real SQL Server). The schemas themselves exist as catalog-view hosts (`select * from sys.tables` / `select * from INFORMATION_SCHEMA.COLUMNS` work); legacy bare 1-part system-table access (`select * from systypes`) also still works.
-`RAISERROR`, stored procedures, **table-valued functions** (inline `RETURNS TABLE AS RETURN (SELECT ...)` and multi-statement `RETURNS @t TABLE (...) AS BEGIN ... END`), CLR functions, triggers, views, sequences. Scalar UDFs ship — see the "Scalar user-defined functions" section. `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch. Value-form `RETURN N`is legal inside a scalar-UDF body and raises Msg 178 elsewhere (stored-proc scope where it would also be legal isn't modeled). TRY/CATCH + THROW + live `@@ERROR` + `ERROR_*()` functions ship — see the TRY/CATCH section.
516
539
-**`PRINT` message capture** — the statement parses + evaluates the operand (so operand-side errors like Msg 245 surface), but the message is discarded. `DbConnection` has no `InfoMessage` event (that's a `SqlConnection` extension), so adding a public observability surface would mean a new event on `SimulatedDbConnection`. Defer until an application needs it.
0 commit comments