Skip to content

Commit 6ac3196

Browse files
committed
BACPAC loader — SqlExtendedProperty host-routing for SqlIndexBase + SqlConstraint. WWI's last large remaining structural gap: 81 of 414 extended properties (76 hosted on SqlIndexBase, 5 on SqlConstraint) were landing on Skipped because EmitExtendedProperty's host-kind switch only handled SqlColumn / SqlTableBase / SqlSchema / SqlDatabaseOptions. Two-part fix wires CONSTRAINT and INDEX level2 acceptance through the existing sp_addextendedproperty surface; everything now round-trips through sys.extended_properties. WWI Skipped category count drops from 2 to 1; only the non-default-collation SqlDatabaseOptions entry remains (its own multi-area feature project).
**Simulator surface — `ResolveExtendedPropertyTarget`** (`SqlServerSimulator/Simulation/Simulation.ExtendedProperties.cs`). The level2 dispatch was hard-coded to COLUMN-only; now switches across CONSTRAINT / INDEX / COLUMN with Msg 15600 fall-through on anything else. CONSTRAINT-level2 walks `HeapTable.KeyConstraints` (PK + UQ), `CheckConstraints`, `OutgoingForeignKeys`, and `Columns[].DefaultConstraint`, returning a class=1 (OBJECT_OR_COLUMN) key with the constraint's own `ObjectId` as `major_id` and `minor_id=0` — matches real SQL Server, where constraints are first-class objects with their own object_id. INDEX-level2 uses class=7 with `(major_id=table.ObjectId, minor_id=index_id)`; the new `ComputeIndexId` helper mirrors the `sys.indexes` enumeration logic exactly (PK gets `index_id=1`, or HEAP-row 0 if no PK; other key constraints + indexes sequence by ObjectId order). `BuiltInResources.EnumerateSysExtendedProperties` adds the class=7 → `"INDEX"` mapping for the `class_desc` column. **Loader surface — `EmitExtendedProperty`** (`SqlServerSimulator/Storage/Bacpac/ModelXmlReader.cs`). Two new cases in the host-kind switch: - `SqlIndexBase` (3-part host `[schema].[table].[index]`) — straightforward translation: `@level0type=SCHEMA, @level1type=TABLE|VIEW, @level2type=INDEX`. Notably WWI's `FK_*`-named indexes (manually created to back FK columns) flow through this path — they're real `SqlIndex` elements that DACFx references through the `SqlIndexBase` host kind. - `SqlConstraint` (2-part host `[schema].[constraint_name]`) — needs the parent table name for `@level1name` but DACFx doesn't include it in the host reference. Added `LookupConstraintParentTable` which queries `sys.objects.parent_object_id` (constraint rows carry this; tables don't) joined with `sys.schemas`, returning the owning table name via `OBJECT_NAME(parent_object_id)`. The lookup runs once per SqlConstraint extended property — fine since WWI has only 5. **Test additions (8 new)**: - `SqlServerSimulator.Tests/ExtendedPropertyTests.cs` (+7): CHECK / PK / FK constraint host round-trip; INDEX host round-trip with class=7 + class_desc=INDEX + minor_id=2 assertion; PK index_id=1 case; missing-name error paths for both CONSTRAINT and INDEX raising Msg 15135. - `SqlServerSimulator.Tests.Internal/Storage/BacpacLoaderTests.cs` (+1): `Load_WWI_Extended_Properties_Cover_Index_And_Constraint_Hosts` exercises the round-trip on representative WWI samples — `Application.Cities.FK_Application_Cities_StateProvinceID` (INDEX host, class=7), `Sales.CK_Sales_Invoices_ReturnedDeliveryData_Must_Be_Valid_JSON` (CONSTRAINT host, class=1), plus an aggregate count check (`>= 410` of WWI's 414 total Description-named properties). **Test expectation + doc updates**: - `Load_WWI_Known_Gaps_Recorded_In_Skipped` swaps the SqlExtendedProperty count assertion for an `IsFalse(grouped.ContainsKey("SqlExtendedProperty"))` — all 414 WWI extended properties land. The only remaining category assertion is `AreEqual(1, grouped["SqlDatabaseOptions"])`. - `docs/claude/bacpac-prerequisites.md` step 10 captures the bundle; the WWI gap inventory in step 13 strikes through the SqlExtendedProperty entry and adds the SqlDatabaseOptions collation entry as the named final gap. - `CLAUDE.md`'s BACPAC import deep-dive pointer drops the SqlExtendedProperty entry from the WWI Skipped tally.
1 parent f89e164 commit 6ac3196

7 files changed

Lines changed: 350 additions & 17 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
164164
- **Touching `XmlSqlType` (singleton in `Storage/XmlType.cs`, SystemTypeId=241, IsLob=true), `XmlSchemaCollection` storage class + `Schema.XmlSchemaCollections` dict + `Database.AllocateXmlCollectionId` (seeds at 65536 per probe), `HeapColumn.XmlSchemaCollection` per-column binding, the `xml`-recognizing arms of `SqlType.ResolveSimpleKeyword` / `Precedence` / `SystemTypeId`, the `xml(name)` / `xml(CONTENT name)` / `xml(DOCUMENT name)` column-type parsing in `ParseOneColumnIntoLists` (via `PeekIsXmlSchemaArgument` / `ParseXmlSchemaCollectionArgument`), `Simulation.Xml.cs` partial (CREATE XML SCHEMA COLLECTION / DROP XML SCHEMA COLLECTION / CREATE [PRIMARY] XML INDEX / FOR PATH|VALUE|PROPERTY secondary form), the `Xml` contextual keyword + `Keyword.Primary` dispatch in `TryParseCreate`, `XmlIndex` / `XmlSecondaryIndexType` storage on `HeapTable.XmlIndexes`, the `XmlMethodCall` expression in `Parser/Expressions/XmlMethodCall.cs` (closed accept-list of `value` / `nodes` / `query` / `exist` / `modify`, parses cleanly, throws `NotSupportedException` at `Run`), or the `sys.xml_schema_collections` / `sys.xml_indexes` catalog views** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the "`xml` data type + XML schema collections + XML methods + XML indexes" section carries the implementation detail).
165165
- **Touching `GeographySqlType` / `GeometrySqlType` (singletons in `Storage/SpatialType.cs`, both inherit `SpatialSqlType : SqlType(SqlTypeCategory.String)`, SystemTypeId=240, UserTypeId=130/129, IsLob=true), `SqlValue.FromGeography` / `FromGeometry` factories + the spatial branches in `FromString`, the `geography` / `geometry`-recognizing arms of `SqlType.ResolveSimpleKeyword` / `Precedence` / `SystemTypeId` / `UserTypeId`, the `SpatialMethodCall` expression in `Parser/Expressions/SpatialMethodCall.cs` (broad ~70-name accept-list of OGC + Microsoft-extension methods; parses cleanly + throws `NotSupportedException` at `Run` except `.ToString()` returns the stored WKT), the `SpatialStaticCall` expression in `Parser/Expressions/SpatialStaticCall.cs` (`geography::Parse(wkt)` / `geography::STGeomFromText(wkt, srid)` / `geometry::Point(x, y, srid)` work; other static methods throw), the `::` dispatch for `geography`/`geometry` in `Expression.cs` alongside `hierarchyid::`, the `.ToString()` polymorphic delegation in `HierarchyIdMethodCall.Run` (spatial receivers return WKT through the spatial path), the `SpatialIndex` storage class + `SpatialIndexKind` enum on `HeapTable.SpatialIndexes`, the `Simulation.Spatial.cs` partial (CREATE SPATIAL INDEX with USING / BOUNDING_BOX / GRIDS / CELLS_PER_OBJECT parsing + default tessellation_scheme), the `Spatial` contextual keyword routing in `TryParseCreate`, or the `sys.spatial_indexes` (23-col probe-confirmed) / `sys.spatial_index_tessellations` (16-col probe-confirmed) / `sys.spatial_reference_systems` (6-col, empty seed) catalog views** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the "`geography` / `geometry` data types" section carries the implementation detail).
166166
- **Adding a new top-level statement parser or changing the dispatch loop's statement-separator rules**[`docs/claude/grammar.md`](docs/claude/grammar.md) + [`docs/claude/control-flow.md`](docs/claude/control-flow.md).
167-
- **Working on BACPAC import (`Simulation.FromBacpac(string, out BacpacLoadResult)` + Stream overload, internal until externally consumed) — the model.xml dispatcher in `SqlServerSimulator/Storage/Bacpac/ModelXmlReader.cs` (9 phases: DB options + schemas + UDDTs + **sequences + roles + table types + filegroups (skip)** → tables → constraints → FKs → (unused) → views → programmable objects → **deferred computed columns + indexes** → extended properties → BCP data), the `BacpacReader` + `BcpRowReader` + `HierarchyIdWireDecoder` data path, the `BacpacLoadResult.Skipped` + `ElementCounts` + `Warnings` + `TableColumnIsAlias` diagnostics carriers, the resilient-loader contract (per-element exceptions land on Skipped with `"Load failed: …"` prefix and don't abort the load; computed-column failures use `"Deferred: …"` prefix so the AW guard stays meaningful), the **BCP-filters-computed-columns contract** (`BacpacReader.LoadRowsFromBcp` strips columns with `HeapColumn.Computed != null` before passing to `BcpRowReader`/`RowEncoder` — DACFx-emitted BCP files exclude computed columns from the wire layout regardless of PERSISTED; the loader emits computed columns *without* the PERSISTED qualifier so the simulator recomputes on every read), the wire-format matrix (fixed-raw / 1-byte-prefix / 2-byte-prefix / 8-byte-prefix tiers — see [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) "BCP wire format" section)** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md). Loader baseline ships (2026-05-15) with AdventureWorks2025 at **100% row coverage** (760,167 / 760,167; zero BCP-file failures); WideWorldImporters-Standard loads end-to-end with **48/48 tables + 26/26 sequences + 9/9 roles + 4/4 table types + 8/8 computed columns + 42/42 procedures + 94/94 indexes + 3/3 views + 1/1 scalar function + 4.7M rows**, remaining Skipped categories cataloged in `Load_WWI_Known_Gaps_Recorded_In_Skipped` (81 SqlExtendedProperty, 1 SqlDatabaseOptions-Collation). MAX types (varchar(MAX) / nvarchar(MAX) / varbinary(MAX)) + xml + CLR-UDT family all use the simple inline 8-byte-prefix wire shape (NOT TDS-PLP chunked form); plain `bit` uses 1-byte length prefix regardless of nullability (probe-confirmed via Production.Document.FolderFlag, the sole non-UDDT bit column in AW). Hierarchyid wire decoder covers AW's [0..79] positive-ordinal envelope via a 4-prefix order-preserving prefix code (`01` / `100` / `101` / `110`); negative ordinals + ordinals ≥ 80 + dotted sub-ordinals raise `NotSupportedException` for a follow-up bundle to extend. Geography rows load with `SpatialLocation = NULL` (WKB→WKT decoding deferred).
167+
- **Working on BACPAC import (`Simulation.FromBacpac(string, out BacpacLoadResult)` + Stream overload, internal until externally consumed) — the model.xml dispatcher in `SqlServerSimulator/Storage/Bacpac/ModelXmlReader.cs` (9 phases: DB options + schemas + UDDTs + **sequences + roles + table types + filegroups (skip)** → tables → constraints → FKs → (unused) → views → programmable objects → **deferred computed columns + indexes** → extended properties → BCP data), the `BacpacReader` + `BcpRowReader` + `HierarchyIdWireDecoder` data path, the `BacpacLoadResult.Skipped` + `ElementCounts` + `Warnings` + `TableColumnIsAlias` diagnostics carriers, the resilient-loader contract (per-element exceptions land on Skipped with `"Load failed: …"` prefix and don't abort the load; computed-column failures use `"Deferred: …"` prefix so the AW guard stays meaningful), the **BCP-filters-computed-columns contract** (`BacpacReader.LoadRowsFromBcp` strips columns with `HeapColumn.Computed != null` before passing to `BcpRowReader`/`RowEncoder` — DACFx-emitted BCP files exclude computed columns from the wire layout regardless of PERSISTED; the loader emits computed columns *without* the PERSISTED qualifier so the simulator recomputes on every read), the wire-format matrix (fixed-raw / 1-byte-prefix / 2-byte-prefix / 8-byte-prefix tiers — see [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) "BCP wire format" section)** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md). Loader baseline ships (2026-05-15) with AdventureWorks2025 at **100% row coverage** (760,167 / 760,167; zero BCP-file failures); WideWorldImporters-Standard loads end-to-end with **48/48 tables + 26/26 sequences + 9/9 roles + 4/4 table types + 8/8 computed columns + 42/42 procedures + 94/94 indexes + 3/3 views + 1/1 scalar function + 4.7M rows**, remaining Skipped categories cataloged in `Load_WWI_Known_Gaps_Recorded_In_Skipped` (1 SqlDatabaseOptions-Collation — the last category not feature-modeled). MAX types (varchar(MAX) / nvarchar(MAX) / varbinary(MAX)) + xml + CLR-UDT family all use the simple inline 8-byte-prefix wire shape (NOT TDS-PLP chunked form); plain `bit` uses 1-byte length prefix regardless of nullability (probe-confirmed via Production.Document.FolderFlag, the sole non-UDDT bit column in AW). Hierarchyid wire decoder covers AW's [0..79] positive-ordinal envelope via a 4-prefix order-preserving prefix code (`01` / `100` / `101` / `110`); negative ordinals + ordinals ≥ 80 + dotted sub-ordinals raise `NotSupportedException` for a follow-up bundle to extend. Geography rows load with `SpatialLocation = NULL` (WKB→WKT decoding deferred).
168168

169169
## Not modeled
170170

SqlServerSimulator.Tests.Internal/Storage/BacpacLoaderTests.cs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ public void Load_WWI_Known_Gaps_Recorded_In_Skipped()
615615
IsFalse(grouped.ContainsKey("SqlFilegroup"), "Filegroup skip-with-diagnostic.");
616616
IsFalse(grouped.ContainsKey("SqlCheckConstraint"), "Paren-wrapped value LHS in boolean parser landed; WWI's CK_Sales_SpecialDeals_Exactly_One_NOT_NULL_Pricing_Option_Is_Required loads.");
617617
IsFalse(grouped.ContainsKey("SqlPermissionStatement"), "SqlPermissionStatement dispatcher entry landed; the 2 encryption-key VIEW grants emit through the GRANT parser.");
618-
AreEqual(81, grouped["SqlExtendedProperty"], "Extended properties on table-type columns + a handful of computed-col-adjacent columns the loader doesn't yet host-route.");
618+
IsFalse(grouped.ContainsKey("SqlExtendedProperty"), "Extended-property host-routing landed for SqlIndexBase + SqlConstraint; all 414 WWI extended properties load.");
619619
AreEqual(1, grouped["SqlDatabaseOptions"], "Non-default collation deferred.");
620620
}
621621

@@ -635,6 +635,68 @@ public void Load_WWI_Computed_Columns_Land_With_is_computed_Set()
635635
AreEqual(8, reader.GetInt32(0));
636636
}
637637

638+
[TestMethod]
639+
public void Load_WWI_Extended_Properties_Cover_Index_And_Constraint_Hosts()
640+
{
641+
// WWI carries 76 SqlIndexBase + 5 SqlConstraint extended properties
642+
// that the 2026-05-16 loader-bundle wired through sp_addextendedproperty.
643+
// Verify the catalog round-trips a known sample of each:
644+
// INDEX: Application.Cities.FK_Application_Cities_StateProvinceID
645+
// CONSTRAINT: Sales.CK_Sales_Invoices_ReturnedDeliveryData_Must_Be_Valid_JSON
646+
var simulation = LoadWideWorldImporters(out _);
647+
using var connection = (SimulatedDbConnection)simulation.CreateDbConnection();
648+
connection.Open();
649+
650+
// Index host: class=7 (INDEX), value should land on the named index.
651+
using (var command = connection.CreateCommand())
652+
{
653+
command.CommandText = """
654+
SELECT CAST(ep.value AS nvarchar(MAX))
655+
FROM sys.extended_properties ep
656+
JOIN sys.indexes i ON ep.major_id = i.object_id AND ep.minor_id = i.index_id
657+
JOIN sys.objects o ON i.object_id = o.object_id
658+
JOIN sys.schemas s ON o.schema_id = s.schema_id
659+
WHERE ep.class = 7
660+
AND s.name = 'Application'
661+
AND o.name = 'Cities'
662+
AND i.name = 'FK_Application_Cities_StateProvinceID'
663+
AND ep.name = 'Description';
664+
""";
665+
using var reader = command.ExecuteReader();
666+
IsTrue(reader.Read(), "Expected SqlIndexBase host extended property on FK-named index.");
667+
AreEqual("Auto-created to support a foreign key", reader.GetString(0));
668+
}
669+
670+
// Constraint host: class=1 (OBJECT_OR_COLUMN), major_id = the
671+
// constraint's own object_id (not the table's).
672+
using (var command = connection.CreateCommand())
673+
{
674+
command.CommandText = """
675+
SELECT CAST(ep.value AS nvarchar(MAX))
676+
FROM sys.extended_properties ep
677+
JOIN sys.check_constraints c ON ep.major_id = c.object_id
678+
JOIN sys.schemas s ON c.schema_id = s.schema_id
679+
WHERE ep.class = 1
680+
AND s.name = 'Sales'
681+
AND c.name = 'CK_Sales_Invoices_ReturnedDeliveryData_Must_Be_Valid_JSON'
682+
AND ep.name = 'Description';
683+
""";
684+
using var reader = command.ExecuteReader();
685+
IsTrue(reader.Read(), "Expected SqlConstraint host extended property on CK constraint.");
686+
AreEqual("Ensures that if returned delivery data is present that it is valid JSON", reader.GetString(0));
687+
}
688+
689+
// Aggregate count check — all 414 WWI extended properties landed.
690+
using (var command = connection.CreateCommand())
691+
{
692+
command.CommandText = "SELECT COUNT(*) FROM sys.extended_properties WHERE name = 'Description';";
693+
using var reader = command.ExecuteReader();
694+
IsTrue(reader.Read());
695+
// 292 SqlColumn + 76 SqlIndexBase + 31 SqlTableBase + 10 SqlSchema + 5 SqlConstraint = 414.
696+
IsGreaterThanOrEqualTo(410, reader.GetInt32(0));
697+
}
698+
}
699+
638700
[TestMethod]
639701
public void Load_WWI_Encryption_Key_Grants_Land_In_sys_database_permissions()
640702
{

SqlServerSimulator.Tests/ExtendedPropertyTests.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,119 @@ public void FnListExtendedProperty_DefaultWildcard_FansOutAcrossSchemaTables()
286286
AreEqual(2, sim.ExecuteScalar(
287287
"SELECT COUNT(*) FROM fn_listextendedproperty(NULL, 'SCHEMA', 'dbo', 'TABLE', 'default', NULL, NULL)"));
288288
}
289+
290+
[TestMethod]
291+
public void AddOnCheckConstraint_RoundTrips()
292+
{
293+
var sim = new Simulation();
294+
_ = sim.ExecuteNonQuery("""
295+
CREATE TABLE dbo.t1 (id int, qty int, CONSTRAINT ck_qty_pos CHECK (qty > 0));
296+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'positive qty rule',
297+
@level0type=N'SCHEMA', @level0name=N'dbo',
298+
@level1type=N'TABLE', @level1name=N't1',
299+
@level2type=N'CONSTRAINT', @level2name=N'ck_qty_pos';
300+
""");
301+
AreEqual("positive qty rule", sim.ExecuteScalar(
302+
"SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'MS_Description'"));
303+
// CHECK constraint reuses class=1 (OBJECT_OR_COLUMN); major_id is the
304+
// constraint's own object_id (not the table's).
305+
AreEqual((byte)1, sim.ExecuteScalar(
306+
"SELECT class FROM sys.extended_properties WHERE name = 'MS_Description'"));
307+
}
308+
309+
[TestMethod]
310+
public void AddOnPrimaryKeyConstraint_RoundTrips()
311+
{
312+
var sim = new Simulation();
313+
_ = sim.ExecuteNonQuery("""
314+
CREATE TABLE dbo.t1 (id int CONSTRAINT pk_t1 PRIMARY KEY);
315+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'primary key',
316+
@level0type=N'SCHEMA', @level0name=N'dbo',
317+
@level1type=N'TABLE', @level1name=N't1',
318+
@level2type=N'CONSTRAINT', @level2name=N'pk_t1';
319+
""");
320+
AreEqual("primary key", sim.ExecuteScalar(
321+
"SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'MS_Description'"));
322+
}
323+
324+
[TestMethod]
325+
public void AddOnForeignKey_RoundTrips()
326+
{
327+
var sim = new Simulation();
328+
_ = sim.ExecuteNonQuery("""
329+
CREATE TABLE dbo.p (id int PRIMARY KEY);
330+
CREATE TABLE dbo.c (id int PRIMARY KEY, p_id int CONSTRAINT fk_c_p REFERENCES p(id));
331+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'fk to p',
332+
@level0type=N'SCHEMA', @level0name=N'dbo',
333+
@level1type=N'TABLE', @level1name=N'c',
334+
@level2type=N'CONSTRAINT', @level2name=N'fk_c_p';
335+
""");
336+
AreEqual("fk to p", sim.ExecuteScalar(
337+
"SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'MS_Description'"));
338+
}
339+
340+
[TestMethod]
341+
public void AddOnConstraint_MissingName_RaisesTargetMissing()
342+
{
343+
var sim = new Simulation();
344+
_ = sim.ExecuteNonQuery("CREATE TABLE dbo.t1 (id int);");
345+
_ = sim.AssertSqlError("""
346+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'x',
347+
@level0type=N'SCHEMA', @level0name=N'dbo',
348+
@level1type=N'TABLE', @level1name=N't1',
349+
@level2type=N'CONSTRAINT', @level2name=N'no_such_constraint';
350+
""", 15135);
351+
}
352+
353+
[TestMethod]
354+
public void AddOnIndex_RoundTrips_WithClass7()
355+
{
356+
var sim = new Simulation();
357+
_ = sim.ExecuteNonQuery("""
358+
CREATE TABLE dbo.t1 (id int PRIMARY KEY, name nvarchar(50));
359+
CREATE INDEX ix_name ON dbo.t1 (name);
360+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'name lookup',
361+
@level0type=N'SCHEMA', @level0name=N'dbo',
362+
@level1type=N'TABLE', @level1name=N't1',
363+
@level2type=N'INDEX', @level2name=N'ix_name';
364+
""");
365+
AreEqual("name lookup", sim.ExecuteScalar(
366+
"SELECT CAST(value AS nvarchar(MAX)) FROM sys.extended_properties WHERE name = 'MS_Description'"));
367+
AreEqual((byte)7, sim.ExecuteScalar(
368+
"SELECT class FROM sys.extended_properties WHERE name = 'MS_Description'"));
369+
AreEqual("INDEX", sim.ExecuteScalar(
370+
"SELECT class_desc FROM sys.extended_properties WHERE name = 'MS_Description'"));
371+
// minor_id = index_id = 2 (PK is 1, ix_name is the next index in
372+
// ObjectId order — matches sys.indexes enumeration).
373+
AreEqual(2, sim.ExecuteScalar(
374+
"SELECT minor_id FROM sys.extended_properties WHERE name = 'MS_Description'"));
375+
}
376+
377+
[TestMethod]
378+
public void AddOnIndex_PrimaryKey_IndexIdIs1()
379+
{
380+
var sim = new Simulation();
381+
_ = sim.ExecuteNonQuery("""
382+
CREATE TABLE dbo.t1 (id int CONSTRAINT pk_t1 PRIMARY KEY);
383+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'pk index',
384+
@level0type=N'SCHEMA', @level0name=N'dbo',
385+
@level1type=N'TABLE', @level1name=N't1',
386+
@level2type=N'INDEX', @level2name=N'pk_t1';
387+
""");
388+
AreEqual(1, sim.ExecuteScalar(
389+
"SELECT minor_id FROM sys.extended_properties WHERE name = 'MS_Description'"));
390+
}
391+
392+
[TestMethod]
393+
public void AddOnIndex_MissingName_RaisesTargetMissing()
394+
{
395+
var sim = new Simulation();
396+
_ = sim.ExecuteNonQuery("CREATE TABLE dbo.t1 (id int);");
397+
_ = sim.AssertSqlError("""
398+
EXEC sp_addextendedproperty @name=N'MS_Description', @value=N'x',
399+
@level0type=N'SCHEMA', @level0name=N'dbo',
400+
@level1type=N'TABLE', @level1name=N't1',
401+
@level2type=N'INDEX', @level2name=N'no_such_index';
402+
""", 15135);
403+
}
289404
}

SqlServerSimulator/BuiltInResources.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ private static IEnumerable<SqlValue[]> EnumerateSysExtendedProperties(Parser.Bat
11611161
0 => "DATABASE",
11621162
1 => "OBJECT_OR_COLUMN",
11631163
3 => "SCHEMA",
1164+
7 => "INDEX",
11641165
_ => key.Class.ToString(CultureInfo.InvariantCulture),
11651166
};
11661167
yield return [

0 commit comments

Comments
 (0)