Commit 8e1b7f3
committed
DDL trigger
**Storage**: three new per-database collections on `Database` + two new entity types. `DdlTrigger` (`SqlServerSimulator/DdlTrigger.cs`) carries name + object_id + event-type list + body source + is_disabled; `Database.DdlTriggers` is the per-database `ConcurrentDictionary<string, DdlTrigger>` (not per-schema — DDL triggers belong to the database itself, `parent_class=0` in `sys.triggers`). `DatabasePrincipal` (`SqlServerSimulator/DatabasePrincipal.cs`) carries principal_id + name + type_code (`S`=SQL_USER, `R`=DATABASE_ROLE) + type_desc + is_fixed_role + create_date; `DatabasePermission` (`SqlServerSimulator/DatabasePermission.cs`) carries class + major_id + minor_id + grantee/grantor ids + permission_name + 4-char type code + state (`G`/`W`/`D`/`R`). `Database.Principals` is the per-database principal dict (case-insensitive by name), `Database.Permissions` is the per-database permission list, `Database.RoleMembers` is the per-database `(role_id, member_id)` tuple list. Five fixed principals pre-seeded at database construction matching real SQL Server's `sys.database_principals` ids (probe-confirmed 2026-05-14 against the reference instance): public=0, dbo=1, guest=2, INFORMATION_SCHEMA=3, sys=4. User principals start at 5 via `Database.AllocatePrincipalId`.
**Parser** — five new top-level statement parsers and one extended one:
- **`CREATE TRIGGER … ON DATABASE [WITH options] {FOR|AFTER} <event_type_list> AS <body>`**: `Simulation.CreateTrigger.cs::TryParseCreateTrigger` branches after the `ON` keyword — if `DATABASE` follows, dispatch to the new `ParseDdlTriggerBody` helper. Event types parse as bare Name / UnquotedString identifiers (e.g. `DDL_DATABASE_LEVEL_EVENTS`, `CREATE_TABLE`) and store verbatim. Body source captured between `AS` and end-of-batch via the same idiom DML triggers use. CREATE OR ALTER upserts; same-name collision against any per-schema object in the trigger's owner schema raises Msg 2714.
- **`DROP TRIGGER name ON DATABASE`** (also `IF EXISTS`): `Simulation.Drop.cs::DropOneTrigger` extended — peeks for `ON DATABASE` via `SaveCheckpoint` / `RestoreCheckpoint` and routes to `Database.DdlTriggers` instead of the per-schema `Schema.Triggers` dict.
- **`GRANT <perm_list> [ON <securable>] TO <principal_list> [WITH GRANT OPTION] [AS <grantor>]`** + **`REVOKE [GRANT OPTION FOR] <perm_list> [ON <securable>] {FROM|TO} <principal_list> [CASCADE] [AS <grantor>]`** + **`DENY <perm_list> [ON <securable>] TO <principal_list> [AS <grantor>]`** in `Simulation/Simulation.GrantRevokeDeny.cs`. Permission-list parser eats word sequences (any sequence of bare Name / ReservedKeyword text) until the next clause boundary (`,` / `ON` / `TO` / `FROM` / `AS` / `WITH`) — supports multi-word permission names like `VIEW ANY COLUMN ENCRYPTION KEY DEFINITION` without a closed accept-list. ON-clause `OBJECT::name` / `SCHEMA::name` / `DATABASE::name` / `TYPE::name` detected via peek-restore on the `:` operator pair. Grantee names accept either `Name` or `ReservedKeyword` raw text so `public` (which tokenizes as `ReservedKeyword.Public`) works without special-casing. WITH GRANT OPTION sets state to `W`; REVOKE removes matching rows; DENY sets state to `D`. Dispatched alongside CREATE/DROP/ALTER in `Simulation.cs`'s main statement loop; `Grant` / `Revoke` / `Deny` also added to the statement-boundary keyword list so the parser can resume after them.
- **`CREATE USER name [{FOR | FROM} ...] [WITH ...]`** + **`CREATE ROLE name [AUTHORIZATION owner]`** + **`ALTER ROLE name { ADD MEMBER name | DROP MEMBER name | WITH NAME = newname }`** + **`DROP USER [IF EXISTS] name`** + **`DROP ROLE [IF EXISTS] name`** in `Simulation/Simulation.PrincipalDdl.cs`. CREATE USER allocates a SQL_USER principal; CREATE ROLE allocates a DATABASE_ROLE principal; both eat their tail clauses through the next statement boundary via a shared `ConsumeToStatementBoundary` helper (FROM LOGIN / WITH PASSWORD / DEFAULT_SCHEMA / EXTERNAL PROVIDER / AUTHORIZATION all parse-and-discard). ALTER ROLE ADD / DROP MEMBER mutates `Database.RoleMembers`; WITH NAME parse-and-discards. DROP USER / DROP ROLE remove from `Principals` and cascade-drop any `RoleMembers` row referencing the removed id. Routed ahead of the generic `DROP <target>` switch in `Simulation.Drop.cs` (principals don't live in a per-schema dict). `Role` added to `ContextualKeyword` enum; `User` already on the reserved list.
**Catalog views** in `BuiltInResources.cs` (probe-confirmed shipped subsets):
- **`sys.triggers`** extended — the existing per-schema `Schema.Triggers` loop is followed by a per-database `Database.DdlTriggers` loop, yielding rows with `parent_class=0`, `parent_class_desc='DATABASE'`, `parent_id=0`, `type_desc='SQL_TRIGGER'`, `is_instead_of_trigger=0`.
- **`sys.database_principals`** (12-col): name / principal_id / type / type_desc / default_schema_name (NULL) / create_date / modify_date / owning_principal_id (NULL) / sid (NULL) / is_fixed_role / authentication_type (NULL) / authentication_type_desc (NULL). The simulator's principal model doesn't track default_schema or SIDs; surfaced as NULL.
- **`sys.database_permissions`** (10-col): class (tinyint) / class_desc (`DATABASE` / `OBJECT_OR_COLUMN` / `SCHEMA` / `DATABASE_PRINCIPAL`) / major_id / minor_id / grantee_principal_id / grantor_principal_id / type (char(2) — 4-char code right-trimmed) / permission_name (nvarchar(128)) / state (char(1)) / state_desc (`GRANT` / `GRANT_WITH_GRANT_OPTION` / `DENY` / `REVOKE`).
- **`sys.database_role_members`** (2-col, full row): role_principal_id / member_principal_id.
The 4-char permission type code is derived as first-letter-of-each-word right-padded with spaces (e.g. `VIEW ANY COLUMN MASTER KEY DEFINITION` → `VACM`). Approximate — real SQL Server's mapping diverges for short names like `SELECT` → `SL` and `UPDATE` → `UP`; a per-permission lookup table would close the gap and is the polish path.
**Errors enforced verbatim** (probe-confirmed against SQL Server 2025 on 2026-05-14): `Msg 15151` for unknown principal (GRANT/REVOKE/DENY/ALTER ROLE), `Msg 15023` for duplicate CREATE USER / CREATE ROLE name.
**AW emit confirmed** by probing the AW bacpac model.xml on 2026-05-14: `SqlDatabaseDdlTrigger Name="[ddlDatabaseTriggerLog]"` carries a `BodyScript` property with the full T-SQL body + a `SqlTriggerEventTypeSpecifier` list (17 event-type codes 101 / 103 / 138 / 137 / 139 / 248 / 247 / 249 / 205 / 198 / 197 / 199 / 257 / 255 / 258 / 256 / 291 …) representing the expanded `DDL_DATABASE_LEVEL_EVENTS` group; the `HeaderContents` annotation shows the canonical T-SQL `CREATE TRIGGER [ddlDatabaseTriggerLog] ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS`. AW's two `SqlPermissionStatement` elements both target `Grantee=[public]` and `SecuredObject Disambiguator="1"` (database scope), encoding the GRANT-VIEW-ANY-COLUMN-ENCRYPTION-KEY-DEFINITION-TO-PUBLIC + GRANT-VIEW-ANY-COLUMN-MASTER-KEY-DEFINITION-TO-PUBLIC pair. No `CREATE USER` / `CREATE ROLE` elements in AW — only the pre-seeded `public` is referenced.
22 new tests across `DdlTriggerTests.cs` (7) and `PermissionStatementTests.cs` (15) cover: DDL trigger CREATE / DROP / DROP IF EXISTS / multi-event-type / collision Msg 2714 / CREATE OR ALTER upsert / catalog row shape; GRANT to public (AW exact shape) / object-scope GRANT / REVOKE removes prior grant / DENY state / WITH GRANT OPTION state / unknown-principal Msg 15151 / CREATE USER + CREATE ROLE happy paths / duplicate Msg 15023 / ALTER ROLE ADD MEMBER lands in role_members / DROP USER / DROP USER IF EXISTS / DROP ROLE cascade-drops membership / pre-seeded fixed principals visible / multi-permission comma list. Test counts: 4120 main (+22) / 227 internal / 328 EFCore / 58 analyzers — all green Debug + Release.
`docs/claude/bacpac-prerequisites.md` flips both prereq items from `[ ]` to `[x]` with full implementation walkthroughs; the order-of-operations sequence drops the merged item. `CLAUDE.md` shrinks the BACPAC prerequisite list to the remaining items (xml / spatial / full-text) and gains two new Feature-reference index entries for DDL trigger and the GRANT/REVOKE/DENY+principal-DDL surface.
**Deferred follow-ups** documented in `bacpac-prerequisites.md`: DDL trigger firing (the simulator doesn't dispatch DDL events — AW's trigger body is an audit-log writer, not load-bearing); `sys.sql_modules` to surface DDL trigger bodies (not modeled at all yet — would extend to procs / funcs / views / DML triggers too); `DISABLE TRIGGER … ON DATABASE` / `ENABLE TRIGGER … ON DATABASE`; the per-permission 4-char type code lookup table (currently first-letter heuristic); server-scope / schema-scope / column-scope grants; `CREATE LOGIN` / `ALTER LOGIN` / `DROP LOGIN` (server-scope); the full `CREATE USER … FROM EXTERNAL PROVIDER` / `WITH PASSWORD` semantics (currently parse-and-discard).ON DATABASE + permission statements (GRANT / REVOKE / DENY) + principal DDL (CREATE USER / CREATE ROLE / ALTER ROLE / DROP USER / DROP ROLE) + the three principal/permission catalog views — fifth bacpac prerequisite bundle. Combined parse-and-store-but-no-enforce surface. The simulator has no permission model and no DDL-event dispatch loop; both elements exist for AW round-trip (model.xml load + sys.* catalog visibility) and intentionally don't fire / don't gate any operation.1 parent f91a71c commit 8e1b7f3
19 files changed
Lines changed: 1369 additions & 24 deletions
File tree
- SqlServerSimulator.Tests
- SqlServerSimulator
- Errors
- Parser
- Simulation
- docs/claude
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
| 161 | + | |
| 162 | + | |
161 | 163 | | |
162 | | - | |
| 164 | + | |
163 | 165 | | |
164 | 166 | | |
165 | 167 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
0 commit comments