Skip to content

Commit 3492650

Browse files
committed
sys.* baseline: ship sys.schemas / sys.tables / sys.objects as virtual catalog views projecting from live metadata. Schemas pre-populate at conventional ids (dbo=1, INFORMATION_SCHEMA=3, sys=4); user schemas start at 5. HeapTable gains SchemaId / CreateDate / ModifyDate; KeyConstraint / CheckConstraint gain ObjectId (allocated at CREATE TABLE alongside the table — so PK/UQ/CHECK rows appear in sys.objects with parent_object_id linking to the table). Catalog views live in Simulation.CatalogViews (process-static); FROM-source parser routes sys. via BatchContext.TryResolveCatalogView and wraps the result as a Selection.LateralPlan that re-runs on each Execute. SCHEMA_ID() scalar ships alongside. CREATE TABLE in sys/INFORMATION_SCHEMA raises NotSupportedException.
1 parent 56da297 commit 3492650

16 files changed

Lines changed: 771 additions & 25 deletions

CLAUDE.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,17 +395,17 @@ Probe-confirmed semantics (2026-05-11) the simulator handles correctly because e
395395
**Transactional rollback is supported** via a single `HeapTruncation` undo entry that snapshots the pre-truncate `Pages` / `LobPages` lists AND each identity column's pre-truncate high-water mark. `BEGIN TRAN; TRUNCATE; ROLLBACK` restores both — including the identity counter. **This diverges from the simulator's general "identity counters bypass the undo log" rule, which is INSERT-only**: INSERT-advanced counters stay advanced through rollback (probe-confirmed for real SQL Server too); TRUNCATE's explicit reset action gets undone on rollback. Outside an explicit transaction, TRUNCATE commits immediately (no log entry — same pattern as DROP TABLE on regular tables).
396396

397397
### Schemas (`CREATE SCHEMA` + schema-qualified resolution)
398-
`CREATE SCHEMA <name>` adds an entry to `Database.Schemas`; subsequent two-part references (`SELECT * FROM audit.t`, `INSERT audit.t VALUES (…)`, every DML / DDL targeting a table) route through it. Unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`), which every `Database` ships pre-populated with. The 9 table-lookup sites (Selection FROM, Insert/Update/Delete/Merge targets, CREATE / DROP / TRUNCATE, SET IDENTITY_INSERT, IDENT_CURRENT, SELECT INTO) all share one parser (`BatchContext.ParseObjectName`) and one resolver pair (`BatchContext.TryResolveTable` for lookup, `BatchContext.TryResolveSchema` for CREATE-shape callsites that need the dict). Probed against SQL Server 2025 (2026-05-11).
398+
`CREATE SCHEMA <name>` adds an entry to `Database.Schemas`; subsequent two-part references (`SELECT * FROM audit.t`, `INSERT audit.t VALUES (…)`, every DML / DDL targeting a table) route through it. Unqualified references fall back to `Database.DefaultSchemaName` (`"dbo"`), which every `Database` ships pre-populated with. The 9 table-lookup sites (Selection FROM, Insert/Update/Delete/Merge targets, CREATE / DROP / TRUNCATE, SET IDENTITY_INSERT, IDENT_CURRENT, SELECT INTO) all share one parser (`BatchContext.ParseObjectName`) and one resolver pair (`BatchContext.TryResolveTable` for lookup, `BatchContext.TryResolveSchema` for CREATE-shape callsites that need the dict). Every `Database` ships with three pre-populated schemas at conventional ids: `dbo=1`, `INFORMATION_SCHEMA=3`, `sys=4`. User schemas allocate ids starting at 5 from `Database.AllocateSchemaId()` (a counter seeded so the next-allocated value is 5). Probed against SQL Server 2025 (2026-05-11).
399399

400400
- **Duplicate `CREATE SCHEMA`** (case-insensitive) → **Msg 2714** (`"There is already an object named '<n>' in the database."` — same factory as duplicate CREATE TABLE; SQL Server shares the namespace).
401401
- **Reserved schema names** (`dbo`, `sys`, `INFORMATION_SCHEMA`) → **Msg 2760** (`"The specified schema name \"<n>\" either does not exist or you do not have permission to use it."`). Wording is quirky for a CREATE (says "does not exist"), but probe-confirmed verbatim — real SQL Server resolves the principal first and these schemas tie to system principals.
402402
- **Three-part `db.schema.t`** validates the db segment against `CurrentDatabase.Name` (case-insensitive); mismatch → Msg 208. **Four-part `server.db.schema.t`** always returns false from `TryResolveSchema` (linked-server names aren't modeled — surfaces as Msg 208 / Msg 3701 / NULL from OBJECT_ID per callsite). Empty middle segment (`tempdb..#foo`) is silently compressed by `ParseObjectName`, so a 2-part name pops out — preserves the existing DROP TABLE behavior for temp-table qualifiers.
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 isn't modeled**: `sys.schemas` / `sys.tables` etc. all raise Msg 208. System tables (`systypes` etc.) remain reachable only as bare 1-part names through `SystemHeapTables`.
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.
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, sys.schemas / INFORMATION_SCHEMA.SCHEMATA**: not modeled.
408+
- **DROP SCHEMA, ALTER SCHEMA TRANSFER, INFORMATION_SCHEMA.SCHEMATA**: not modeled.
409409

410410
### Object identifiers + `OBJECT_ID()`
411411
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.
@@ -417,6 +417,18 @@ 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.
422+
423+
- **`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.
424+
- **`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).
425+
- **`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.
426+
- **`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.
430+
- **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.
431+
420432
### Local temp tables (`#foo`)
421433
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.
422434

0 commit comments

Comments
 (0)