Skip to content

Commit 4eb35a4

Browse files
committed
Extended properties (sp_addextendedproperty + sp_updateextendedproperty + sp_dropextendedproperty + sys.extended_properties + fn_listextendedproperty) — third bacpac prerequisite. Pure-metadata surface, no semantic effect on queries; covers AW's 538 <SqlExtendedProperty> elements (461 column + 69 table + 5 schema + 1 DB + 2 filegroup/DDL-trigger which remain out of scope per the simulator's broader feature roster).
Storage lives on a new `Database.ExtendedProperties` (`ConcurrentDictionary<ExtendedPropertyKey, SqlValue>`) keyed by `(byte class, int major_id, int minor_id, string name)`. `ExtendedPropertyKey` is a readonly-struct with explicit `Equals` / `GetHashCode` overrides that route the `Name` comparison through `Collation.Default` (case-insensitive per real SQL Server). Per-DB flat dict mirrors `sys.extended_properties`'s catalog shape (not per-schema). Sproc trio in new `Simulation.ExtendedProperties.cs` partial — three branches in `Simulation.Exec.cs` dispatch to a shared `InvokeSpExtendedProperty(batch, ExtendedPropertyOp)` body that parses 8 named args (`@name`, `@value`, `@level0type` / `@level0name` / `@level1type` / `@level1name` / `@level2type` / `@level2name` — the `AtPrefixedString` token's `Value` is already `@`-stripped, so the arg-name comparison uses bare names). `ResolveExtendedPropertyTarget` walks the level0..2 chain, accepting a closed set (level0 = SCHEMA; level1 = TABLE / VIEW / PROCEDURE / FUNCTION / TYPE; level2 = COLUMN) and returning the `(ExtendedPropertyKey, targetLabel)` pair the error wording needs. Four new error factories with probe-confirmed verbatim wording against SQL Server 2025 (2026-05-14): - Msg 15233 (`ExtendedPropertyAlreadyExists`): `"Property cannot be added. Property '<name>' already exists for '<target>'."` — target label is `'object specified'` for DB-level, `'<schema>'` for schema, `'<schema>.<name>'` for table/view/proc/func, `'<schema>.<table>.<col>'` for column. - Msg 15217 (`ExtendedPropertyDoesNotExist`): same shape for update/drop on missing. - Msg 15135 (`ExtendedPropertyTargetMissing`): `"Object is invalid. Extended properties are not permitted on '<target>', or the object does not exist."` — target label uses the failing-level's value (`'no_such_schema'` / `'dbo.no_such_table'` / `'dbo.t1.no_such_col'`). - Msg 15600 (`InvalidExtendedPropertyParameter`): `"An invalid parameter or option was specified for procedure '<proc>'."` — positional arg, unknown @-name, missing required arg, unknown level type. `sys.extended_properties` catalog view in `BuiltInResources.cs` enumerates the dict with the shipped 6-column subset: `class` (tinyint) / `class_desc` (sysname — 0→DATABASE, 1→OBJECT_OR_COLUMN, 3→SCHEMA) / `major_id` (int) / `minor_id` (int — 0 for table-level, 1-based column ordinal for column-level) / `name` (sysname) / `value` (nvarchar(MAX), since `sql_variant` isn't modeled — AW's 538 properties are all nvarchar values so lossless for the bacpac workload). `fn_listextendedproperty` in new `Selection.ListExtendedProperty.cs` partial — built-in system TVF dispatched alongside `OPENJSON` / `STRING_SPLIT` in `ParseSingleFromSource`. 7 args (each may be NULL), returns 4 columns (`objtype` / `objname` / `name` / `value`). Filter pipeline: parse args as expressions, eval per-row to nullable strings, build `ExtendedPropertyListFilter` from the resolved target (class + major_id + minor_id + name + minor_id-must-be-nonzero gate for the "all columns of a table" shape), walk the dict and project matches. `'default'` wildcard at level1name / level2name fans out across every object of that level-type under the parent (probe-confirmed). Missing target returns zero rows (distinct from the sproc path's Msg 15135). Unknown level type (not SCHEMA / TABLE / VIEW / PROCEDURE / FUNCTION / TYPE / COLUMN) raises `NotSupportedException`. 21 new tests in `ExtendedPropertyTests.cs` cover: schema / table / column / DB-level read-back via `sys.extended_properties` (class + class_desc + minor_id correctness), all four duplicate-add target-label variants for Msg 15233, update + drop happy paths, all three missing-target variants for Msg 15135, Msg 15217 / 15600 paths, plus 5 `fn_listextendedproperty` cases (table-level scalar read, name filter across levels, column-level filter by ordinal, missing-target zero rows, `'default'` wildcard fanout). Full suite 4640 → 4661, all green Debug + Release. `docs/claude/bacpac-prerequisites.md` flips the Extended-properties bullet from `[ ]` to `[x]` with the full implementation walkthrough and renumbers the order-of-operations sequence (`hierarchyid` is now next). CLAUDE.md gains a sibling Feature-reference index entry next to the existing UDDT one. Two known gaps documented as deferred: PARAMETER / INDEX / TRIGGER / CONSTRAINT level types (NotSupportedException — AW doesn't exercise them, bacpac-loader baseline doesn't need them), and non-nvarchar `sql_variant` values lossy-coerce on read-back (invisible in practice since AW's properties are all nvarchar).
1 parent 73c1420 commit 4eb35a4

10 files changed

Lines changed: 1080 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
148148
- **Touching `DECLARE @t TABLE`, table-variable DML routing, `OUTPUT … INTO <target>` (`@t` or regular)**[`docs/claude/table-variables.md`](docs/claude/table-variables.md).
149149
- **Touching `CREATE TYPE … AS TABLE`, `DECLARE @t MyType`, TVP procedure parameters / `READONLY`, or the ADO.NET `DbParameter.TypeName` extension (DataTable / IDataReader as a TVP value source)**[`docs/claude/table-valued-parameters.md`](docs/claude/table-valued-parameters.md).
150150
- **Touching `CREATE TYPE name FROM <builtin> [NULL|NOT NULL]` (scalar alias types / UDDTs), `Schema.AliasTypes` dict, multi-part type-reference parsing (`[schema].[typename]` in column / parameter / `DECLARE @v` / `ALTER COLUMN` / OPENJSON / sp_executesql positions), the `Simulation.ResolveTypeReference` helper, nullability inheritance from the alias default, Msg 222 / 2716 St 3 / 219 enforcement, or `sys.types` alias-type rows**[`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the section "UDDTs / alias types" carries the implementation detail; this is one of the bacpac-loader prerequisite bundles).
151+
- **Touching `sp_addextendedproperty` / `sp_updateextendedproperty` / `sp_dropextendedproperty` system sprocs, the `Database.ExtendedProperties` per-database dict + `ExtendedPropertyKey` value-type, the named-arg parsing in `Simulation.ExtendedProperties.cs`, the level-type resolver (SCHEMA / TABLE / VIEW / PROCEDURE / FUNCTION / TYPE / COLUMN), Msg 15233 / 15217 / 15135 / 15600 enforcement, `sys.extended_properties` catalog view, or `fn_listextendedproperty` system TVF (dispatched alongside OPENJSON / STRING_SPLIT in `ParseSingleFromSource`)**[`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the section "Extended properties" carries the implementation detail; another bacpac-loader prerequisite bundle).
151152
- **Touching `CREATE SEQUENCE` / `ALTER SEQUENCE` / `DROP SEQUENCE`, `NEXT VALUE FOR`, the per-row dedup mechanism (`BatchContext.CurrentRowStamp` / `SequenceRowCache`), or `sys.sequences`**[`docs/claude/sequences.md`](docs/claude/sequences.md).
152153
- **Touching `CREATE TRIGGER` / `ALTER TRIGGER` / `DROP TRIGGER` / `DISABLE`/`ENABLE TRIGGER`, the `INSERTED` / `DELETED` pseudo-table materialization, the `TriggerFrame` / `FiringTriggerIds` recursion guard, `TRIGGER_NESTLEVEL()`, or `sys.triggers`**[`docs/claude/triggers.md`](docs/claude/triggers.md).
153154
- **Touching `FOREIGN KEY` parsing (`REFERENCES` inline / `FOREIGN KEY (cols) REFERENCES other(cols)` table-level), referential-action wiring (`ON DELETE` / `ON UPDATE` × `NO ACTION` / `CASCADE` / `SET NULL` / `SET DEFAULT`), the FK enforcement loop in `Simulation.ForeignKeys.cs`, the `HeapTable.OutgoingForeignKeys` / `IncomingForeignKeys` lists, cascade-cycle detection (Msg 1785), DROP-TABLE protection (Msg 3726), or `sys.foreign_keys` / `sys.foreign_key_columns`**[`docs/claude/foreign-keys.md`](docs/claude/foreign-keys.md).
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Exercises the extended-properties surface — <c>sp_addextendedproperty</c>,
7+
/// <c>sp_updateextendedproperty</c>, <c>sp_dropextendedproperty</c>,
8+
/// <c>sys.extended_properties</c>, and <c>fn_listextendedproperty</c>. Third
9+
/// bacpac prerequisite (after database-options expansion + UDDTs);
10+
/// AdventureWorks2025 carries 538 <c>SqlExtendedProperty</c> elements that
11+
/// the loader translates to <c>EXEC sp_addextendedproperty</c> calls.
12+
/// Behavior probed against SQL Server 2025 (2026-05-14).
13+
/// </summary>
14+
[TestClass]
15+
public class ExtendedPropertyTests
16+
{
17+
[TestMethod]
18+
public void AddOnSchema_ThenReadFromSysExtendedProperties()
19+
=> AreEqual("dbo schema notes", new Simulation().ExecuteScalar("""
20+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'dbo schema notes',
21+
@level0type=N'SCHEMA', @level0name=N'dbo';
22+
SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'MS_Description' AND class = 3
23+
"""));
24+
25+
[TestMethod]
26+
public void AddOnTable_RowShapeMatchesProbe()
27+
{
28+
var sim = new Simulation();
29+
_ = sim.ExecuteNonQuery("""
30+
CREATE TABLE dbo.t1 (id int);
31+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'table notes',
32+
@level0type=N'SCHEMA', @level0name=N'dbo',
33+
@level1type=N'TABLE', @level1name=N't1';
34+
""");
35+
AreEqual((byte)1, sim.ExecuteScalar(
36+
"SELECT class FROM sys.extended_properties WHERE name = 'MS_Description'"));
37+
AreEqual("OBJECT_OR_COLUMN", sim.ExecuteScalar(
38+
"SELECT class_desc FROM sys.extended_properties WHERE name = 'MS_Description'"));
39+
AreEqual(0, sim.ExecuteScalar(
40+
"SELECT minor_id FROM sys.extended_properties WHERE name = 'MS_Description'"));
41+
}
42+
43+
[TestMethod]
44+
public void AddOnColumn_MinorIdIsColumnOrdinal()
45+
{
46+
var sim = new Simulation();
47+
_ = sim.ExecuteNonQuery("""
48+
CREATE TABLE dbo.t1 (id int, name nvarchar(50));
49+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'col notes',
50+
@level0type=N'SCHEMA', @level0name=N'dbo',
51+
@level1type=N'TABLE', @level1name=N't1',
52+
@level2type=N'COLUMN', @level2name=N'name';
53+
""");
54+
// `name` is the 2nd column, minor_id = 2 (1-based ordinal per probe).
55+
AreEqual(2, sim.ExecuteScalar(
56+
"SELECT minor_id FROM sys.extended_properties WHERE name = 'MS_Description'"));
57+
}
58+
59+
[TestMethod]
60+
public void DatabaseLevel_ClassIs0()
61+
{
62+
var sim = new Simulation();
63+
_ = sim.ExecuteNonQuery(
64+
"EXEC sp_addextendedproperty @name=N'DBDesc', @value=N'whole-db notes'");
65+
AreEqual((byte)0, sim.ExecuteScalar(
66+
"SELECT class FROM sys.extended_properties WHERE name = 'DBDesc'"));
67+
AreEqual("DATABASE", sim.ExecuteScalar(
68+
"SELECT class_desc FROM sys.extended_properties WHERE name = 'DBDesc'"));
69+
}
70+
71+
[TestMethod]
72+
public void DuplicateAdd_Schema_RaisesMsg15233()
73+
{
74+
var ex = new Simulation().AssertSqlError("""
75+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
76+
@level0type=N'SCHEMA', @level0name=N'dbo';
77+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
78+
@level0type=N'SCHEMA', @level0name=N'dbo';
79+
""", 15233);
80+
Contains("'X'", ex.Message);
81+
Contains("'dbo'", ex.Message);
82+
}
83+
84+
[TestMethod]
85+
public void DuplicateAdd_Database_TargetLabelIsObjectSpecified()
86+
{
87+
// Probe-confirmed: DB-level dup uses the literal `'object specified'`
88+
// token in the Msg 15233 wording.
89+
var ex = new Simulation().AssertSqlError("""
90+
EXEC sp_addextendedproperty @name=N'X', @value=N'v';
91+
EXEC sp_addextendedproperty @name=N'X', @value=N'v';
92+
""", 15233);
93+
Contains("'object specified'", ex.Message);
94+
}
95+
96+
[TestMethod]
97+
public void DuplicateAdd_Table_TargetLabelIsSchemaDotTable()
98+
{
99+
var ex = new Simulation().AssertSqlError("""
100+
CREATE TABLE dbo.t1 (id int);
101+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
102+
@level0type=N'SCHEMA', @level0name=N'dbo',
103+
@level1type=N'TABLE', @level1name=N't1';
104+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
105+
@level0type=N'SCHEMA', @level0name=N'dbo',
106+
@level1type=N'TABLE', @level1name=N't1';
107+
""", 15233);
108+
Contains("'dbo.t1'", ex.Message);
109+
}
110+
111+
[TestMethod]
112+
public void DuplicateAdd_Column_TargetLabelIsSchemaDotTableDotColumn()
113+
{
114+
var ex = new Simulation().AssertSqlError("""
115+
CREATE TABLE dbo.t1 (id int);
116+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
117+
@level0type=N'SCHEMA', @level0name=N'dbo',
118+
@level1type=N'TABLE', @level1name=N't1',
119+
@level2type=N'COLUMN', @level2name=N'id';
120+
EXEC sp_addextendedproperty @name=N'X', @value=N'v',
121+
@level0type=N'SCHEMA', @level0name=N'dbo',
122+
@level1type=N'TABLE', @level1name=N't1',
123+
@level2type=N'COLUMN', @level2name=N'id';
124+
""", 15233);
125+
Contains("'dbo.t1.id'", ex.Message);
126+
}
127+
128+
[TestMethod]
129+
public void Update_ChangesValue()
130+
{
131+
var sim = new Simulation();
132+
_ = sim.ExecuteNonQuery("""
133+
EXEC sp_addextendedproperty @name=N'X', @value=N'old',
134+
@level0type=N'SCHEMA', @level0name=N'dbo';
135+
EXEC sp_updateextendedproperty @name=N'X', @value=N'new',
136+
@level0type=N'SCHEMA', @level0name=N'dbo';
137+
""");
138+
AreEqual("new", sim.ExecuteScalar(
139+
"SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'X'"));
140+
}
141+
142+
[TestMethod]
143+
public void Update_OnMissing_RaisesMsg15217()
144+
{
145+
var ex = new Simulation().AssertSqlError(
146+
"EXEC sp_updateextendedproperty @name=N'NoSuch', @value=N'x', @level0type=N'SCHEMA', @level0name=N'dbo'",
147+
15217);
148+
Contains("'NoSuch'", ex.Message);
149+
}
150+
151+
[TestMethod]
152+
public void Drop_RemovesEntry()
153+
{
154+
var sim = new Simulation();
155+
_ = sim.ExecuteNonQuery("""
156+
EXEC sp_addextendedproperty @name=N'X', @value=N'v', @level0type=N'SCHEMA', @level0name=N'dbo';
157+
EXEC sp_dropextendedproperty @name=N'X', @level0type=N'SCHEMA', @level0name=N'dbo';
158+
""");
159+
AreEqual(0, sim.ExecuteScalar(
160+
"SELECT COUNT(*) FROM sys.extended_properties WHERE name = 'X'"));
161+
}
162+
163+
[TestMethod]
164+
public void Drop_OnMissing_RaisesMsg15217()
165+
=> new Simulation().AssertSqlError(
166+
"EXEC sp_dropextendedproperty @name=N'NoSuch', @level0type=N'SCHEMA', @level0name=N'dbo'",
167+
15217);
168+
169+
[TestMethod]
170+
public void BadSchemaName_RaisesMsg15135()
171+
{
172+
var ex = new Simulation().AssertSqlError(
173+
"EXEC sp_addextendedproperty @name=N'X', @value=N'x', @level0type=N'SCHEMA', @level0name=N'no_such_schema'",
174+
15135);
175+
Contains("'no_such_schema'", ex.Message);
176+
}
177+
178+
[TestMethod]
179+
public void BadTableName_RaisesMsg15135()
180+
{
181+
var ex = new Simulation().AssertSqlError(
182+
"EXEC sp_addextendedproperty @name=N'X', @value=N'x', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'no_such_table'",
183+
15135);
184+
Contains("'dbo.no_such_table'", ex.Message);
185+
}
186+
187+
[TestMethod]
188+
public void BadColumnName_RaisesMsg15135()
189+
{
190+
var ex = new Simulation().AssertSqlError("""
191+
CREATE TABLE dbo.t1 (id int);
192+
EXEC sp_addextendedproperty @name=N'X', @value=N'x',
193+
@level0type=N'SCHEMA', @level0name=N'dbo',
194+
@level1type=N'TABLE', @level1name=N't1',
195+
@level2type=N'COLUMN', @level2name=N'no_such_col';
196+
""", 15135);
197+
Contains("'dbo.t1.no_such_col'", ex.Message);
198+
}
199+
200+
[TestMethod]
201+
public void BadLevel0Type_RaisesMsg15600()
202+
=> new Simulation().AssertSqlError(
203+
"EXEC sp_addextendedproperty @name=N'X', @value=N'x', @level0type=N'BOGUS', @level0name=N'x'",
204+
15600);
205+
206+
[TestMethod]
207+
public void FnListExtendedProperty_TableLevel_ReturnsOneRow()
208+
{
209+
var sim = new Simulation();
210+
_ = sim.ExecuteNonQuery("""
211+
CREATE TABLE dbo.t1 (id int);
212+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'table notes',
213+
@level0type=N'SCHEMA', @level0name=N'dbo',
214+
@level1type=N'TABLE', @level1name=N't1';
215+
""");
216+
using var conn = sim.CreateOpenConnection();
217+
using var cmd = conn.CreateCommand(
218+
"SELECT objtype, objname, name FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 't1', NULL, NULL)");
219+
using var r = cmd.ExecuteReader();
220+
IsTrue(r.Read());
221+
AreEqual("TABLE", r.GetString(0));
222+
AreEqual("t1", r.GetString(1));
223+
AreEqual("MS_Description", r.GetString(2));
224+
IsFalse(r.Read());
225+
}
226+
227+
[TestMethod]
228+
public void FnListExtendedProperty_NameFilter_AppliesAcrossLevels()
229+
{
230+
var sim = new Simulation();
231+
_ = sim.ExecuteNonQuery("""
232+
CREATE TABLE dbo.t1 (id int);
233+
EXEC sp_addextendedproperty @name=N'Author', @value=N'alice',
234+
@level0type=N'SCHEMA', @level0name=N'dbo',
235+
@level1type=N'TABLE', @level1name=N't1';
236+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'desc',
237+
@level0type=N'SCHEMA', @level0name=N'dbo',
238+
@level1type=N'TABLE', @level1name=N't1';
239+
""");
240+
AreEqual(1, sim.ExecuteScalar(
241+
"SELECT COUNT(*) FROM fn_listextendedproperty('Author', 'SCHEMA', 'dbo', 'TABLE', 't1', NULL, NULL)"));
242+
AreEqual(2, sim.ExecuteScalar(
243+
"SELECT COUNT(*) FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 't1', NULL, NULL)"));
244+
}
245+
246+
[TestMethod]
247+
public void FnListExtendedProperty_ColumnLevel_FindsByOrdinal()
248+
{
249+
var sim = new Simulation();
250+
_ = sim.ExecuteNonQuery("""
251+
CREATE TABLE dbo.t1 (id int, name nvarchar(50));
252+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'name col notes',
253+
@level0type=N'SCHEMA', @level0name=N'dbo',
254+
@level1type=N'TABLE', @level1name=N't1',
255+
@level2type=N'COLUMN', @level2name=N'name';
256+
""");
257+
using var conn = sim.CreateOpenConnection();
258+
using var cmd = conn.CreateCommand(
259+
"SELECT objtype, objname FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 't1', 'COLUMN', 'name')");
260+
using var r = cmd.ExecuteReader();
261+
IsTrue(r.Read());
262+
AreEqual("COLUMN", r.GetString(0));
263+
AreEqual("name", r.GetString(1));
264+
}
265+
266+
/// <summary>
267+
/// Probe-confirmed: fn_listextendedproperty returns zero rows on a
268+
/// missing object (NOT Msg 15135 — that's the sproc path).
269+
/// </summary>
270+
[TestMethod]
271+
public void FnListExtendedProperty_MissingTarget_ReturnsZeroRows()
272+
=> AreEqual(0, new Simulation().ExecuteScalar(
273+
"SELECT COUNT(*) FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 'no_such_table', NULL, NULL)"));
274+
275+
[TestMethod]
276+
public void FnListExtendedProperty_DefaultWildcard_FansOutAcrossSchemaTables()
277+
{
278+
var sim = new Simulation();
279+
_ = sim.ExecuteNonQuery("""
280+
CREATE TABLE dbo.t1 (id int);
281+
CREATE TABLE dbo.t2 (id int);
282+
EXEC sp_addextendedproperty @name=N'Desc', @value=N't1', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N't1';
283+
EXEC sp_addextendedproperty @name=N'Desc', @value=N't2', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N't2';
284+
""");
285+
// 'default' wildcard at level1name expands to every table in the schema.
286+
AreEqual(2, sim.ExecuteScalar(
287+
"SELECT COUNT(*) FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 'default', NULL, NULL)"));
288+
}
289+
}

SqlServerSimulator/BuiltInResources.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,24 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
786786
};
787787
var dmTranActiveSnapshotDbTxView = new CatalogView("dm_tran_active_snapshot_database_transactions", dmTranActiveSnapshotDbTxColumns, VersionStoreDmvs.EnumerateDmTranActiveSnapshotDatabaseTransactions);
788788

789+
// sys.extended_properties: per-database user-defined annotations
790+
// attached to schemas / tables / columns / etc. via the
791+
// sp_addextendedproperty / sp_updateextendedproperty /
792+
// sp_dropextendedproperty trio. Real SQL Server's `value` column is
793+
// typed `sql_variant` — the simulator surfaces it as `nvarchar(MAX)`
794+
// since sql_variant isn't modeled; AW's 538 properties are all
795+
// nvarchar values so functional fidelity is preserved.
796+
var extendedPropertiesColumns = new HeapColumn[]
797+
{
798+
new("class", SqlType.TinyInt, null, false),
799+
new("class_desc", SqlType.SystemName, 60, true),
800+
new("major_id", SqlType.Int32, null, false),
801+
new("minor_id", SqlType.Int32, null, false),
802+
new("name", SqlType.SystemName, 128, false),
803+
new("value", NVarcharSqlType.MaxForm, SqlType.MaxLengthSentinel, true),
804+
};
805+
var extendedPropertiesView = new CatalogView("extended_properties", extendedPropertiesColumns, EnumerateSysExtendedProperties);
806+
789807
return new Dictionary<string, CatalogView>(Collation.Default)
790808
{
791809
["sys.schemas"] = schemasView,
@@ -811,6 +829,7 @@ private static Dictionary<string, CatalogView> BuildCatalogViews()
811829
["sys.dm_tran_version_store"] = dmTranVersionStoreView,
812830
["sys.dm_tran_version_store_space_usage"] = dmTranVersionStoreSpaceUsageView,
813831
["sys.dm_tran_active_snapshot_database_transactions"] = dmTranActiveSnapshotDbTxView,
832+
["sys.extended_properties"] = extendedPropertiesView,
814833
["INFORMATION_SCHEMA.TABLES"] = isTablesView,
815834
["INFORMATION_SCHEMA.COLUMNS"] = isColumnsView,
816835
["INFORMATION_SCHEMA.SCHEMATA"] = isSchemataView,
@@ -893,6 +912,41 @@ private static IEnumerable<SqlValue[]> EnumerateSysTypes(Parser.BatchContext bat
893912
}
894913
}
895914

915+
/// <summary>
916+
/// Rows for <c>sys.extended_properties</c>. Walks every entry in
917+
/// <see cref="Database.ExtendedProperties"/> (per-database flat dict)
918+
/// and projects the 6-column shape. The <c>class_desc</c> string is
919+
/// derived from the class number per real SQL Server's enum (0 =
920+
/// DATABASE, 1 = OBJECT_OR_COLUMN, 3 = SCHEMA — the only classes the
921+
/// simulator currently emits; others fall through as the string form
922+
/// of the class number for forward compat). Value is coerced to
923+
/// <c>nvarchar(MAX)</c> since the simulator doesn't model
924+
/// <c>sql_variant</c>; for AW's all-nvarchar workload, this is a
925+
/// lossless surfacing.
926+
/// </summary>
927+
private static IEnumerable<SqlValue[]> EnumerateSysExtendedProperties(Parser.BatchContext batch)
928+
{
929+
foreach (var kvp in batch.CurrentDatabase.ExtendedProperties)
930+
{
931+
var key = kvp.Key;
932+
var classDesc = key.Class switch
933+
{
934+
0 => "DATABASE",
935+
1 => "OBJECT_OR_COLUMN",
936+
3 => "SCHEMA",
937+
_ => key.Class.ToString(CultureInfo.InvariantCulture),
938+
};
939+
yield return [
940+
SqlValue.FromByte(key.Class),
941+
SqlValue.FromSystemName(classDesc),
942+
SqlValue.FromInt32(key.MajorId),
943+
SqlValue.FromInt32(key.MinorId),
944+
SqlValue.FromSystemName(key.Name),
945+
kvp.Value.IsNull ? SqlValue.Null(NVarcharSqlType.MaxForm) : kvp.Value.CoerceTo(NVarcharSqlType.MaxForm),
946+
];
947+
}
948+
}
949+
896950
private static IEnumerable<SqlValue[]> EnumerateSysTableTypes(Parser.BatchContext batch)
897951
{
898952
var trueBit = SqlValue.FromBoolean(true);

0 commit comments

Comments
 (0)