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
+19-10Lines changed: 19 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -403,12 +403,12 @@ Probe-confirmed semantics (2026-05-11) the simulator handles correctly because e
403
403
-**`CREATE TABLE schema.t`** where `schema` doesn't exist → **Msg 2760** (target schema for the create must already exist). Distinct from FROM / INSERT / UPDATE / DELETE / MERGE / DROP / TRUNCATE access which use 208 / 3701 / 4701 respectively.
404
404
-**`AUTHORIZATION owner`** and the embedded `<schema_element>` list (CREATE TABLE / VIEW / GRANT nested inside CREATE SCHEMA) aren't modeled — `AUTHORIZATION` raises `NotSupportedException`; trailing statement-starting tokens (CREATE / SELECT / INSERT / etc.) parse as their own statements in the same batch (deviates from real SQL Server's strict greedy-consume but reaches the same end state for the common idiom).
405
405
-**No "first in batch" enforcement** — real SQL Server raises Msg 111 if CREATE SCHEMA isn't the first statement, tied to its greedy schema_element grammar. The simulator's dispatch already treats each statement as independent, so CREATE SCHEMA in any position works.
406
-
-**`sys`schema hosts catalog views** (sys.schemas / sys.tables / sys.objects — see the dedicated section below). Adding a user table via `CREATE TABLE sys.foo (…)` raises `NotSupportedException` ("Cannot CREATE TABLE in the built-in 'sys' schema"); same rejection for `INFORMATION_SCHEMA`. The `sys``Schema`entry exists in `Database.Schemas` to carry its conventional id and to be reachable from `sys.schemas`, but its`HeapTables`dict stays empty — catalog views live in a separate `Simulation.CatalogViews` registry.
406
+
-**`sys`and `INFORMATION_SCHEMA` host catalog views** (sys.schemas / sys.tables / sys.objects / sys.columns / INFORMATION_SCHEMA.TABLES / .COLUMNS / .SCHEMATA — see the dedicated section below). Adding a user table via `CREATE TABLE sys.foo (…)` raises `NotSupportedException` ("Cannot CREATE TABLE in the built-in 'sys' schema"); same rejection for `INFORMATION_SCHEMA`. Both `Schema`entries exist in `Database.Schemas` to carry their conventional ids and to be reachable from `sys.schemas`, but their`HeapTables`dicts stay empty — catalog views live in a separate `Simulation.CatalogViews` registry.
407
407
-**Error wording**: Msg 208 wraps the qualified name in single quotes (`Invalid object name 'badschema.t'.`); Msg 3701 (DROP) does the same; Msg 4701 (TRUNCATE) carries only the leaf (probe-confirmed asymmetric — distinct error path).
408
-
-**DROP SCHEMA, ALTER SCHEMA TRANSFER, INFORMATION_SCHEMA.SCHEMATA**: not modeled.
408
+
-**DROP SCHEMA, ALTER SCHEMA TRANSFER**: not modeled.
409
409
410
410
### Object identifiers + `OBJECT_ID()`
411
-
Every `HeapTable` carries a stable per-database `int ObjectId` assigned at CREATE time from `Database.AllocateObjectId()` (a `Database`-scoped `Interlocked.Increment` counter seeded at 100). DROP-then-recreate yields a fresh id, matching real SQL Server (probe-confirmed 2026-05-11 — counter never reuses values). The counter bypasses transaction rollback: a rolled-back CREATE TABLE still consumed an id, matching the identity-counter rule. System tables (`SystemHeapTables`) carry a sentinel `ObjectId = -1` — they're process-shared, sit outside per-DB id space, and aren't reachable through `OBJECT_ID()` anyway. Backs `OBJECT_ID()`today; pre-positioned for the upcoming`sys.objects.object_id` catalog column.
411
+
Every `HeapTable` carries a stable per-database `int ObjectId` assigned at CREATE time from `Database.AllocateObjectId()` (a `Database`-scoped `Interlocked.Increment` counter seeded at 100). DROP-then-recreate yields a fresh id, matching real SQL Server (probe-confirmed 2026-05-11 — counter never reuses values). The counter bypasses transaction rollback: a rolled-back CREATE TABLE still consumed an id, matching the identity-counter rule. System tables (`SystemHeapTables`) carry a sentinel `ObjectId = -1` — they're process-shared, sit outside per-DB id space, and aren't reachable through `OBJECT_ID()` anyway. Backs `OBJECT_ID()`plus `sys.tables` / `sys.objects` /`sys.columns.object_id`.
412
412
413
413
**`OBJECT_ID(name [, type])`** scalar (`Parser/Expressions/ObjectId.cs`): returns the `int` ObjectId of the named object, or NULL when not found / wrong type / malformed name. The name is a runtime string parsed as a 1–3-part dotted identifier with bracket-quoting (`'[dbo].[foo]'`, `'dbo.foo'`, `'simulated.dbo.foo'` all resolve identically); 4-segment names return NULL (linked-server form unmodeled). The type filter is case-insensitive but whitespace-sensitive — `'U'` and `'u'` match user tables; `' U '`, `'XX'`, `''` all → NULL; other documented codes (`V`/`P`/`F`/`FN`/...) → NULL until those features land. A NULL on any argument propagates NULL.
414
414
@@ -417,17 +417,26 @@ Every `HeapTable` carries a stable per-database `int ObjectId` assigned at CREAT
417
417
-**Bracket-handling fidelity gap**: the runtime-string name parser strips bracket pairs at segment level (`'[dbo].[foo]'` → `dbo`+`foo`) and decodes `]]` → `]` inside brackets — but bracketed segments containing a literal `.` (`'[a.b].[c]'`, the literal-dot case) don't parse correctly (split on `.` happens before bracket-aware tokenization). Rare in practice; revisit if a real app hits it.
418
418
-**Arity**: too-few-args (`OBJECT_ID()`) currently surfaces as Msg 102 (the inner Parse failure path) rather than Msg 174 — same pattern as other built-ins; the simulator doesn't enforce min-args. Too-many-args raises Msg 174 verbatim.
419
419
420
-
### `sys.*` catalog views
421
-
`Simulation.CatalogViews` is a process-static dict of virtual `sys.<view>` projections — currently `sys.schemas`, `sys.tables`, `sys.objects`. Each `CatalogView` carries a fixed `HeapColumn[]` schema and a `Func<BatchContext, IEnumerable<SqlValue[]>>` row generator that runs against live `Database` / `Schema` / `HeapTable` metadata; rows aren't cached, so CREATE / DROP / TRUNCATE changes made earlier in the same batch are visible on the next read. The FROM-source parser detects `sys.<view>` references via `BatchContext.TryResolveCatalogView` (case-insensitive on `sys`, also accepts the 3-part `<currentDb>.sys.<view>`form), wraps the view in `Selection.ForCatalogView`, and threads it as the `FromSource.LateralPlan` — so each Execute re-runs the generator. The `RowEncoder.EncodeRow(HeapColumn[], SqlValue[])` overload bridges the SqlValue-array generator output into the byte stream the FromSource consumes.
`Simulation.CatalogViews` is a process-static dict of virtual catalog-view projections keyed by fully-qualified name (`"sys.tables"`, `"INFORMATION_SCHEMA.COLUMNS"`, etc.) so one resolver serves both namespaces without per-schema dispatch. Each `CatalogView` carries a fixed `HeapColumn[]` schema and a `Func<BatchContext, IEnumerable<SqlValue[]>>` row generator that runs against live `Database` / `Schema` / `HeapTable` metadata; rows aren't cached, so CREATE / DROP / TRUNCATE changes made earlier in the same batch are visible on the next read. The FROM-source parser detects catalog views via `BatchContext.TryResolveCatalogView` (case-insensitive on the qualifier, 2-part or `<currentDb>.qualifier.<view>`3-part), wraps the view in `Selection.ForCatalogView`, and threads it as the `FromSource.LateralPlan` — so each Execute re-runs the generator. The `RowEncoder.EncodeRow(HeapColumn[], SqlValue[])` overload bridges the SqlValue-array generator output into the byte stream the FromSource consumes.
422
422
423
+
Shipped views:
423
424
-**`sys.schemas`** projects `name sysname`, `schema_id int`, `principal_id int NULL` (always NULL — no principal model). Always lists dbo / INFORMATION_SCHEMA / sys plus every user CREATE SCHEMA addition.
-**`sys.objects`** is the superset: one row per `HeapTable` plus one row per `KeyConstraint` (type `PK` / `UQ`) and `CheckConstraint` (type `C `) with `parent_object_id` linking to the owning table. Constraint object_ids allocate from the same `Database.AllocateObjectId` counter as tables, so PK / UQ / CHECK constraints get globally-unique ids that `sys.objects.object_id` surfaces.
427
+
-**`sys.columns`** projects per-column metadata: `object_id`, `name sysname`, `column_id` (1-based), `system_type_id tinyint`, `user_type_id int`, `max_length smallint` (byte-length — `nvarchar(50)→100`, `char(5)→5`, `-1` for the MAX form, `16` for text/ntext/image LOB pointers, `256` for sysname), `precision` / `scale tinyint` (decimal/numeric carry their declared (p,s); date/time fractional types follow `(time(N): 8+N, N)` / `(datetime2(N): 19+N, N)` / `(datetimeoffset(N): 26+N, N)`; 0 for everything else), `is_nullable` / `is_identity` / `is_computed bit`, `collation_name sysname` (set only for string types). Backed by `SqlType.SystemTypeId` (byte-typed switch on `this` matching real SQL Server's `sys.types.system_type_id`) and `SqlType.UserTypeId` (== `SystemTypeId` except `sysname=256`). `system_type_id` covers the 22 base types modeled.
428
+
-**`INFORMATION_SCHEMA.TABLES`** (4 cols): TABLE_CATALOG / TABLE_SCHEMA / TABLE_NAME / TABLE_TYPE. TABLE_TYPE is `'BASE TABLE'` for every user table (views not yet modeled).
429
+
-**`INFORMATION_SCHEMA.COLUMNS`** (full 23-col ISO shape): the always-NULL columns (DOMAIN_*, CHARACTER_SET_SCHEMA, COLLATION_CATALOG, etc.) ship anyway since tooling does `SELECT *`. IS_NULLABLE is `varchar(3)``'YES'`/`'NO'` (not bit). CHARACTER_MAXIMUM_LENGTH is declared **chars** (`nvarchar(50)→50`); CHARACTER_OCTET_LENGTH is **bytes** (`nvarchar(50)→100`). Text-family sentinels: text/image = `2147483647`; ntext = `1073741823` chars / `2147483646` bytes. NUMERIC_PRECISION_RADIX is 10 for integer/decimal/money, 2 for float/real; NUMERIC_SCALE is NULL for float/real, otherwise the actual scale. DATETIME_PRECISION carries the fractional-seconds digit count (0 for date/smalldatetime, 3 for datetime, N for datetime2/time/datetimeoffset). CHARACTER_SET_NAME: `'UNICODE'` for nvarchar/nchar/ntext/sysname; `'iso_1'` for varchar/char/text; NULL for binary/varbinary/image.
430
+
-**`INFORMATION_SCHEMA.SCHEMATA`** (6 cols): only the schemas the simulator models (no role-principal padding — real SQL Server lists 13 schemas because of `db_owner`/`db_datareader`/etc., and we have no principal model). SCHEMA_OWNER mirrors SCHEMA_NAME. DEFAULT_CHARACTER_SET_NAME is `'iso_1'`.
426
431
-**`SCHEMA_ID([name])`** scalar: no-arg returns `Database.DboSchemaId` (=1) — the simulator's "caller default schema" (no user model means dbo is universal). With an arg, returns the schema's id or NULL.
427
-
-**Column subset**: real SQL Server's `sys.tables` / `sys.objects` have 30+ columns each; the simulator ships the load-bearing subset that EF / migration tooling and the probe queried. `SELECT *` returns fewer columns than real SQL Server — apps that depend on a specific full-column shape will surface gaps, address those as needed.
428
-
-**Temp tables not in `sys.tables`**: the per-connection `TempTables` dict isn't walked by the row generators (real SQL Server lists temp tables in `tempdb.sys.tables`, which the simulator's single-database model doesn't separate). Catalog views show user tables in `dbo` + any user schema only.
429
-
-**No write paths**: `INSERT sys.tables …` / `UPDATE sys.tables …` / `DROP TABLE sys.tables` etc. all raise Msg 208 — catalog views aren't in `Schema.HeapTables`, so the regular table-lookup miss path fires.
432
+
433
+
Cross-cutting notes:
434
+
-**Column subset (sys.* only)**: real SQL Server's `sys.tables` / `sys.objects` / `sys.columns` have 30+ columns each; the simulator ships the load-bearing subset that EF / migration tooling and the probe queried. `SELECT *` returns fewer columns than real SQL Server — apps that depend on a specific full-column shape will surface gaps, address those as needed. INFORMATION_SCHEMA views ship the full ISO column set.
435
+
-**Temp tables not in `sys.tables` / `INFORMATION_SCHEMA.TABLES`**: the per-connection `TempTables` dict isn't walked by the row generators (real SQL Server lists temp tables in `tempdb.sys.tables`, which the simulator's single-database model doesn't separate). Catalog views show user tables in `dbo` + any user schema only.
436
+
-**No write paths**: `INSERT sys.tables …` / `UPDATE sys.tables …` / `DROP TABLE INFORMATION_SCHEMA.COLUMNS` etc. all raise Msg 208 — catalog views aren't in `Schema.HeapTables`, so the regular table-lookup miss path fires.
430
437
-**Constraint object_ids**: `KeyConstraint.ObjectId` and `CheckConstraint.ObjectId` are now allocated at CREATE TABLE alongside the table's own id (via `Database.AllocateObjectId()`). The order is: schema resolution → allocate constraint ids (inside `ResolveKeyConstraints` / `ResolveCheckConstraints`) → allocate table id → construct `HeapTable`. The constraint resolvers take a `Database` parameter to thread the allocation.
438
+
-**`COLUMN_DEFAULT` always NULL** (fidelity gap): real SQL Server renders default expressions as parenthesized text (`(sysdatetime())`). Serializing arbitrary `Expression`s back to SQL is a separate (sizable) bundle; the column ships as NULL until that lands.
439
+
-**`precision` is a reserved keyword in the simulator's parser**: `select precision from sys.columns` raises Msg 102; bracket it (`[precision]`) or alias it. Real SQL Server accepts the bare name. Minor fidelity gap — fix would loosen `Keyword.Precision` to a contextual-keyword classification.
431
440
432
441
### Local temp tables (`#foo`)
433
442
Per-connection `Dictionary<string, HeapTable> TempTables` on `SimulatedDbConnection`; routed by `BatchContext.TryResolveTable` (`#`-prefix leaf → connection dict, ignoring any qualifier; else the named schema's heap-table dict + flat `SystemHeapTables`). Auto-cleared on `Dispose`, matching real SQL Server's session-close drop. Lifecycle, cross-conn isolation, and Msg 208 from other sessions all probe-confirmed against SQL Server 2025.
@@ -469,10 +478,10 @@ Full `DbDataReader` contract. Typed accessors read `SqlValue` directly via the c
469
478
- Table variables (`DECLARE @t TABLE (...)`) — separate feature with its own storage / scope / lifecycle.
470
479
-**Global temp tables (`##foo`)** — `NotSupportedException` at parse. Local `#foo` works; the lifecycle for global temps (drops when creator session closes, visible across sessions) is the deferred scope.
471
480
-**`ALTER TABLE #foo`**, **`OBJECT_ID('tempdb..#foo')`** — none modeled (none of those exist for regular tables either yet). The common `IF OBJECT_ID(...) IS NOT NULL DROP TABLE` cleanup pattern works via `DROP TABLE IF EXISTS #foo` instead.
-**`DROP SCHEMA`**, **`ALTER SCHEMA … TRANSFER`**— deferred. CREATE SCHEMA + schema-qualified resolution ships; lifecycle doesn't yet (catalog views — `sys.schemas`, `INFORMATION_SCHEMA.SCHEMATA` — do ship as of the catalog-view-expansion bundle).
473
482
-**`CREATE SCHEMA AUTHORIZATION <owner>`** — `NotSupportedException` (simulator has no user / principal model).
474
483
-**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`.
475
-
-**`sys`and`INFORMATION_SCHEMA` schemas as accessible namespaces** — `CREATE SCHEMA sys`/`INFORMATION_SCHEMA`raises Msg 2760 (matching real SQL Server), but `select * from sys.tables` / `select * from INFORMATION_SCHEMA.COLUMNS`raise Msg 208 since those schemas don't exist in the simulator. System-table access stays via bare 1-part names (`select * from systypes`) only.
484
+
-**`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.
-`TRY ... CATCH`, `THROW`, `RAISERROR`, stored procs / UDFs. `BEGIN TRY` / `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch (peeked after `BEGIN`). Value-form `RETURN N` raises Msg 178 (reserved for the stored-proc / function scope, neither modeled yet). `@@ERROR` parses + returns 0 (see batch-state section); live tracking lands with TRY/CATCH.
478
487
-**`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