Skip to content

Commit 3ec7be4

Browse files
committed
sys.columns + INFORMATION_SCHEMA.TABLES / COLUMNS / SCHEMATA: extends the catalog-view infrastructure to a second namespace. CatalogView registry rekeys to fully-qualified names ("sys.tables", "INFORMATION_SCHEMA.COLUMNS") so a single TryResolveCatalogView resolver serves both schemas. SqlType gains SystemTypeId (byte) + UserTypeId (sysname=256) matching real SQL Server's sys.types catalog ids. sys.columns ships the load-bearing 12-column subset (object_id / name / column_id / system_type_id / user_type_id / max_length / precision / scale / is_nullable / is_identity / is_computed / collation_name); IS.COLUMNS ships the full 23-column ISO shape so tooling's SELECT * works (DOMAIN_*, CHARACTER_SET_SCHEMA, etc. always NULL). Per-type metadata extraction (GetSysColumnMetadata / GetInformationSchemaColumnMetadata) handles all 22 modeled types with the simulator's length-pinned variable-string singletons. INFORMATION_SCHEMA.SCHEMATA lists only the schemas modeled (no role-principal padding). COLUMN_DEFAULT always NULL until expression-to-SQL serialization lands (documented fidelity gap).
1 parent 3492650 commit 3ec7be4

5 files changed

Lines changed: 920 additions & 32 deletions

File tree

CLAUDE.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,12 @@ Probe-confirmed semantics (2026-05-11) the simulator handles correctly because e
403403
- **`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.
404404
- **`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).
405405
- **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.
407407
- **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.
409409

410410
### 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`.
412412

413413
**`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.
414414

@@ -417,17 +417,26 @@ Every `HeapTable` carries a stable per-database `int ObjectId` assigned at CREAT
417417
- **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.
418418
- **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.
419419

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.
420+
### `sys.*` and `INFORMATION_SCHEMA.*` catalog views
421+
`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.
422422

423+
Shipped views:
423424
- **`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.
424425
- **`sys.tables`** projects user heap tables only: `object_id`, `name sysname`, `schema_id`, `type char(2)` (always `'U '` — trailing-space padded, probe-confirmed), `type_desc nvarchar(60)` (`USER_TABLE`), `create_date datetime`, `modify_date datetime`, `is_ms_shipped bit` (always 0).
425426
- **`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'`.
426431
- **`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.
430437
- **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.
431440

432441
### Local temp tables (`#foo`)
433442
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
469478
- Table variables (`DECLARE @t TABLE (...)`) — separate feature with its own storage / scope / lifecycle.
470479
- **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.
471480
- **`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.
472-
- **`DROP SCHEMA`**, **`ALTER SCHEMA … TRANSFER`**, **`sys.schemas`** / **`INFORMATION_SCHEMA.SCHEMATA`** — deferred. CREATE SCHEMA + schema-qualified resolution ships; lifecycle and metadata views don't yet.
481+
- **`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).
473482
- **`CREATE SCHEMA AUTHORIZATION <owner>`**`NotSupportedException` (simulator has no user / principal model).
474483
- **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.
476485
- T-SQL `GOTO` / labels — `IF` / `BEGIN…END` / `WHILE` / `BREAK` / `CONTINUE` / `RETURN` (bare) ship; unconditional jumps don't.
477486
- `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.
478487
- **`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

Comments
 (0)