Skip to content

Commit 3c33478

Browse files
committed
Split oversized CLAUDE.md by migrating sections into their own files under docs/claude, cleaned up stale references.
1 parent a4e96d5 commit 3c33478

17 files changed

Lines changed: 492 additions & 463 deletions

CLAUDE.md

Lines changed: 21 additions & 459 deletions
Large diffs are not rendered by default.

SqlServerSimulator/Errors/SimulatedSqlException.SyntaxErrors.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ internal static SimulatedSqlException NonBooleanInConditionContext(Token? nextTo
101101
/// any enclosing <c>WHILE</c>. Probe-confirmed against SQL Server 2025
102102
/// (2026-05-11): Class 15, State 1, exact wording verbatim. Fires even
103103
/// from un-taken IF branches — SQL Server applies the loop-scope check
104-
/// at compile time, so the simulator does too (distinct from the Q15
105-
/// deferred-name-resolution gap, where un-taken branches escape Msg 208).
104+
/// at compile time, so the simulator does too (distinct from the
105+
/// un-taken-branch deferred-name-resolution gap, where un-taken branches
106+
/// escape Msg 208).
106107
/// </summary>
107108
internal static SimulatedSqlException BreakOutsideLoop() =>
108109
new("Cannot use a BREAK statement outside the scope of a WHILE statement.", 135, 15, 1);

SqlServerSimulator/Parser/BatchContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ internal sealed class BatchContext
6767
/// itself is in skip mode), decremented on exit. BREAK / CONTINUE check
6868
/// this at parse time: when zero, raise Msg 135 / 136 (matches real SQL
6969
/// Server's compile-time loop-scope check — fires even from un-taken IF
70-
/// branches, distinct from the Q15 deferred-name-resolution gap).
70+
/// branches, distinct from the un-taken-branch deferred-name-resolution
71+
/// gap).
7172
/// </summary>
7273
public int LoopDepth;
7374

SqlServerSimulator/Simulation/Simulation.TryCatch.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ partial class Simulation
4949
/// <item>Parse-time name-resolution errors (Msg 208 / Msg 207) inside a
5050
/// TRY body propagate out of the batch — the simulator parses the whole
5151
/// batch eagerly while real SQL Server defers some name resolution.
52-
/// Same root cause as the Q15 un-taken-IF gap.</item>
52+
/// Same root cause as the un-taken-IF deferred-name-resolution gap.</item>
5353
/// <item><c>XACT_STATE()</c> and the XACT_ABORT / doomed-transaction
5454
/// semantics aren't modeled. <c>@@TRANCOUNT</c> behaves correctly
5555
/// (caught errors don't auto-rollback explicit transactions, matching

docs/claude/arithmetic.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Type promotion and decimal arithmetic
2+
3+
## Integer ↔ string promotion
4+
Cross-category `int ↔ string` lands the integer's specific subtype (`tinyint + '3'` stays tinyint; `bigint + '3'` stays bigint). String parses through the integer's CAST path: empty/whitespace → 0, `+`/`-` accepted, leading/trailing whitespace trimmed. **Decimal-shaped strings (`'5.5'`) raise Msg 245** rather than routing through decimal. Hex (`'0x05'`) likewise rejected.
5+
6+
`bit ↔ string` asymmetry: comparison works (`'true'`/`'false'`/empty → true/false/false; non-zero digit string → True regardless of magnitude); `bit + str` rejected — `+`/`-`/`%` → Msg 402, `*`/`/` → Msg 8117 with LEFT operand's type only.
7+
8+
WHERE on a varchar column compared against int halts on the first unparseable row (not isolated as per-row UNKNOWN). SQL Server's lazy-IN quirk (unparseable IN-list value suppressed when another matches) isn't modeled.
9+
10+
`BuildSynthesizedSqlRow` (FROM-less SELECT) runs each expression first (surfacing runtime-only errors with operator-name wording), then `GetSqlType` for schema, then bridges any mismatch via `CoerceTo` — required for mixed-type CASE/Coalesce without a FROM clause.
11+
12+
## Decimal arithmetic precision / scale
13+
Per-operator decimal scale rules differ from the joint-envelope rule used for non-arithmetic uses (comparison / COALESCE / set ops):
14+
- `+` / `-`: `p = max(p1-s1, p2-s2) + max(s1, s2) + 1`, `s = max(s1, s2)`
15+
- `*`: `p = p1 + p2 + 1`, `s = s1 + s2`
16+
- `/`: `s = max(6, s1 + p2 + 1)`, `p = p1 - s1 + s2 + s`
17+
- `%`: `p = min(p1-s1, p2-s2) + max(s1, s2)`, `s = max(s1, s2)`
18+
19+
When precision exceeds 38, scale reduces by the excess down to a floor of `min(originalScale, 6)`; precision clips to 38. The 6-floor stabilizes division (`s ≥ 6` always); for `+ - * %` it binds only when original scale was already ≤ 6.
20+
21+
Integer/money operands canonicalize before formulas apply (bit→(1,0) … bigint→(19,0); money→(19,4); smallmoney→(10,4)). Pure integer-pair, pure money-pair, and float-involving arithmetic skip the decimal path (joint-envelope `Promote` instead).
22+
23+
`SqlType.Promote` (joint-envelope, `scale = max(s1, s2); precision = min(38, max(p1-s1, p2-s2) + scale)`) stays the right rule for non-arithmetic uses.

docs/claude/casting.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# CAST / CONVERT family
2+
3+
## CAST/CONVERT to narrow `varchar` / `nvarchar` / `varbinary`
4+
Per-source-category rule applied after `SqlValue.CoerceTo`:
5+
- String / varbinary / date-time-family source → silent truncation. `CAST('hello world' AS varchar(5))``'hello'`.
6+
- `tinyint`/`smallint`/`int` source → `varchar` too narrow → asterisk fallback (`'*'`). Quirk specific to `varchar`; `nvarchar` raises Msg 8115. `bigint` doesn't get fallback either.
7+
- `decimal`/`numeric` source → Msg 8115 with "numeric" wording (distinct from int/bigint's "expression" wording).
8+
- `money`/`smallmoney` → Msg 234 (`"There is insufficient result space to convert a money value to <target>."` — "money" regardless of source variant).
9+
- `float`/`real` → Msg 232 with formatted source value (F6).
10+
- `uniqueidentifier`: pre-CoerceTo branch (Msg 8170 char/varchar, Msg 8115 nchar/nvarchar).
11+
- `datetimeoffset → varchar` too narrow: real SQL Server raises Msg 241; simulator silently truncates (niche).
12+
13+
**CAST/CONVERT context defaults missing length to 30** for `varchar`/`nvarchar`/`varbinary` (column-context default is 1).
14+
15+
`VarcharSqlType`/`NVarcharSqlType`/`VarbinarySqlType` are per-length singletons via `Get(N)` (parallel to `CharSqlType`); `Unspecified` (length 0) is the runtime sentinel; `MaxForm` (length -1) is the LOB form. **Equality**: `value.Type == SqlType.Varchar` is true only for the unspecified form; "is any varchar" needs `is VarcharSqlType`. The encoder accepts any same-family pair regardless of length (write-time truncation enforced upstream).
16+
17+
## `TRY_CAST` / `TRY_CONVERT`
18+
Wrap regular CAST/CONVERT in try/catch that swallows documented "conversion failed" error numbers (returning typed NULL) while letting structural errors propagate.
19+
20+
Swallow set (`Cast.IsConversionFailure`): **241** (datetime-from-string parse), **242** (datetime out-of-range), **244** (tinyint/smallint INT1/INT2 overflow), **245** (string→numeric parse), **248** (int overflow), **295** (smalldatetime parse), **8114** (decimal conversion), **8115** (generic arithmetic overflow), **8169** (uniqueidentifier-from-string), **8170** (uniqueidentifier→too-narrow-string).
21+
22+
NOT swallowed: Msg 529 (explicit-cast disallowed pair like `int → date`), Msg 243 (unknown target type), and any source-evaluation error that fires before the cast itself runs. `TRY_CAST(1/0 AS INT)` raises Msg 8134 in real SQL Server; the simulator surfaces a raw `DivideByZeroException` (pre-existing fidelity gap orthogonal to TRY_CAST).
23+
24+
String-source truncation isn't a "conversion failure" path either way — `TRY_CAST('hello' AS varchar(3))``'hel'`. EF doesn't emit TRY_CAST/TRY_CONVERT from idiomatic LINQ (raw SQL only).

docs/claude/catalog-views.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# `sys.*` and `INFORMATION_SCHEMA.*` catalog views
2+
3+
`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.
4+
5+
Shipped views:
6+
- **`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.
7+
- **`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).
8+
- **`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.
9+
- **`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.
10+
- **`INFORMATION_SCHEMA.TABLES`** (4 cols): TABLE_CATALOG / TABLE_SCHEMA / TABLE_NAME / TABLE_TYPE. TABLE_TYPE is `'BASE TABLE'` for user heap tables and `'VIEW'` for views (added with the views bundle — see [`programmable.md`](programmable.md)).
11+
- **`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.
12+
- **`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'`.
13+
- **`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.
14+
15+
Cross-cutting notes:
16+
- **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.
17+
- **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.
18+
- **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.
19+
- **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.
20+
- **`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.
21+
- **`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.

0 commit comments

Comments
 (0)