Skip to content

Commit 44c330d

Browse files
committed
Bacpac loader dispatches SqlXmlIndex / SqlFullTextCatalog / SqlFullTextIndex / SqlFilegroup / DDL-trigger + filegroup extended properties, primary XML indexes allocate their internal node table (sys.objects IT row + sys.stats + sys.index_columns) that DacFx's export topology joins through, sys.fulltext_languages gains the 59-language catalog with fulltext_indexes.data_space_id/stoplist_id made non-NULL, filegroups get a Database registry behind sys.filegroups/sys.data_spaces, and extended properties learn TRIGGER (class 1) and FILEGROUP (class 20 DATASPACE) hosts.
1 parent 7149e68 commit 44c330d

18 files changed

Lines changed: 861 additions & 84 deletions

SqlServerSimulator.Tests/Bacpac/BacpacBuilder.cs

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public sealed partial class BacpacBuilder
4242
private readonly List<ViewIndexDef> _viewIndexes = [];
4343
private readonly List<UserDefinedDataTypeDef> _uddts = [];
4444
private readonly List<XmlSchemaCollectionDef> _xmlSchemaCollections = [];
45+
private readonly List<XmlIndexDef> _xmlIndexes = [];
46+
private readonly List<FullTextCatalogDef> _fullTextCatalogs = [];
47+
private readonly List<FullTextIndexDef> _fullTextIndexes = [];
4548
private readonly List<(string ElementType, string Name)> _silentlySkipped = [];
4649
private readonly List<(string ElementType, string Name)> _unknownElements = [];
4750
private string? _dspName;
@@ -154,6 +157,30 @@ public BacpacBuilder ConstraintExtendedProperty(string schemaName, string constr
154157
return this;
155158
}
156159

160+
/// <summary>
161+
/// Adds an extended property bound to a database DDL trigger host
162+
/// (<c>SqlDatabaseDdlTrigger</c>, addressed via <c>@level0type=N'TRIGGER'</c>).
163+
/// The <paramref name="triggerName"/> must match a
164+
/// <see cref="DatabaseDdlTrigger"/> emitted in the same model.
165+
/// </summary>
166+
public BacpacBuilder DdlTriggerExtendedProperty(string triggerName, string propertyName, string value)
167+
{
168+
_extendedProperties.Add(new ExtendedPropertyDef(triggerName, null, null, propertyName, value, ExtendedPropertyHost.DdlTrigger));
169+
return this;
170+
}
171+
172+
/// <summary>
173+
/// Adds an extended property bound to a filegroup host
174+
/// (<c>SqlFilegroup</c>, addressed via <c>@level0type=N'FILEGROUP'</c>).
175+
/// The <paramref name="filegroupName"/> must match a <see cref="Filegroup"/>
176+
/// registered in the same model (or the built-in <c>PRIMARY</c>).
177+
/// </summary>
178+
public BacpacBuilder FilegroupExtendedProperty(string filegroupName, string propertyName, string value)
179+
{
180+
_extendedProperties.Add(new ExtendedPropertyDef(filegroupName, null, null, propertyName, value, ExtendedPropertyHost.Filegroup));
181+
return this;
182+
}
183+
157184
/// <summary>
158185
/// Adds an extended property whose host kind is one the loader doesn't
159186
/// model (e.g. <c>SqlFilegroup</c> / <c>SqlDatabaseDdlTrigger</c>). Lands
@@ -260,6 +287,53 @@ public BacpacBuilder XmlSchemaCollection(string schemaName, string collectionNam
260287
return this;
261288
}
262289

290+
/// <summary>
291+
/// Emits a primary <c>SqlXmlIndex</c> element
292+
/// (<c>CREATE PRIMARY XML INDEX name ON table(col)</c>). The table must
293+
/// carry a clustered PK and an xml <paramref name="column"/>.
294+
/// </summary>
295+
public BacpacBuilder PrimaryXmlIndex(string schemaName, string tableName, string indexName, string column)
296+
{
297+
_xmlIndexes.Add(new XmlIndexDef(schemaName, tableName, indexName, column, IsPrimary: true, null, null));
298+
return this;
299+
}
300+
301+
/// <summary>
302+
/// Emits a secondary <c>SqlXmlIndex</c> element
303+
/// (<c>CREATE XML INDEX name ON table(col) USING XML INDEX primary FOR
304+
/// PATH|PROPERTY|VALUE</c>). <paramref name="usage"/> is DACFx's enum:
305+
/// 1 = PATH, 2 = PROPERTY, 3 = VALUE.
306+
/// </summary>
307+
public BacpacBuilder SecondaryXmlIndex(string schemaName, string tableName, string indexName, string column, string usingPrimaryIndexName, int usage)
308+
{
309+
_xmlIndexes.Add(new XmlIndexDef(schemaName, tableName, indexName, column, IsPrimary: false, usingPrimaryIndexName, usage));
310+
return this;
311+
}
312+
313+
/// <summary>
314+
/// Emits a <c>SqlFullTextCatalog</c> element
315+
/// (<c>CREATE FULLTEXT CATALOG name WITH ACCENT_SENSITIVITY = … [AS DEFAULT]
316+
/// AUTHORIZATION owner</c>).
317+
/// </summary>
318+
public BacpacBuilder FullTextCatalog(string name, bool accentSensitive = true, bool isDefault = true, string owner = "dbo")
319+
{
320+
_fullTextCatalogs.Add(new FullTextCatalogDef(name, accentSensitive, isDefault, owner));
321+
return this;
322+
}
323+
324+
/// <summary>
325+
/// Emits a <c>SqlFullTextIndex</c> element
326+
/// (<c>CREATE FULLTEXT INDEX ON table (col [TYPE COLUMN t] LANGUAGE n, …)
327+
/// KEY INDEX keyName ON catalog</c>). Each column is
328+
/// <c>(column, languageId, typeColumn?)</c>.
329+
/// </summary>
330+
public BacpacBuilder FullTextIndex(string schemaName, string tableName, string catalogName, string keyIndexName, params (string Column, int LanguageId, string? TypeColumn)[] columns)
331+
{
332+
var cols = columns.Select(c => new FullTextIndexColumnDef(c.Column, c.LanguageId, c.TypeColumn)).ToArray();
333+
_fullTextIndexes.Add(new FullTextIndexDef(schemaName, tableName, catalogName, keyIndexName, cols));
334+
return this;
335+
}
336+
263337
/// <summary>
264338
/// Emits a <c>SqlPartitionFunction</c> element. The loader treats this
265339
/// as a silent no-op (filegroup-mapping metadata with no semantic effect
@@ -307,9 +381,10 @@ public BacpacBuilder UnknownTopLevelElement(string elementType, string elementNa
307381
}
308382

309383
/// <summary>
310-
/// Emits a <c>SqlFilegroup</c> element. Silent no-op in the loader
311-
/// (matches partition/columnstore — physical-storage decoration with
312-
/// no semantic effect on row-store query results).
384+
/// Emits a <c>SqlFilegroup</c> element. The loader registers the filegroup
385+
/// on the target database (no Skipped entry) so
386+
/// <c>sys.filegroups</c> / <c>sys.data_spaces</c> surface it; there's no
387+
/// physical file model, so table / index placement is unaffected.
313388
/// </summary>
314389
public BacpacBuilder Filegroup(string name)
315390
{
@@ -435,6 +510,19 @@ private void WriteModelXml(ZipArchive archive)
435510
foreach (var xsc in _xmlSchemaCollections)
436511
model.Add(BuildXmlSchemaCollectionElement(ns, xsc));
437512

513+
foreach (var cat in _fullTextCatalogs)
514+
model.Add(BuildFullTextCatalogElement(ns, cat));
515+
516+
foreach (var fti in _fullTextIndexes)
517+
model.Add(BuildFullTextIndexElement(ns, fti));
518+
519+
// Primary XML indexes before secondary ones (a secondary references
520+
// its primary), mirroring DACFx's name-sorted document order.
521+
foreach (var xi in _xmlIndexes.Where(x => x.IsPrimary))
522+
model.Add(BuildXmlIndexElement(ns, xi));
523+
foreach (var xi in _xmlIndexes.Where(x => !x.IsPrimary))
524+
model.Add(BuildXmlIndexElement(ns, xi));
525+
438526
foreach (var seq in _sequences)
439527
model.Add(BuildSequenceElement(ns, seq));
440528

@@ -1080,7 +1168,7 @@ internal sealed record ProgrammableObjectDef(
10801168
bool FunctionBodyHost,
10811169
string? ParentTable = null);
10821170

1083-
internal enum ExtendedPropertyHost { AutoDetect, Index, Constraint, Unknown }
1171+
internal enum ExtendedPropertyHost { AutoDetect, Index, Constraint, Unknown, DdlTrigger, Filegroup }
10841172

10851173
internal sealed record ExtendedPropertyDef(string? SchemaName, string? TableName, string? ColumnName, string PropertyName, string Value, ExtendedPropertyHost Host = ExtendedPropertyHost.AutoDetect)
10861174
{
@@ -1095,6 +1183,14 @@ internal sealed record PermissionDef(string Action, string Permission, string Gr
10951183

10961184
internal sealed record ViewIndexDef(string ViewSchema, string ViewName, string IndexName, string[] KeyColumns);
10971185

1186+
internal sealed record XmlIndexDef(string SchemaName, string TableName, string IndexName, string Column, bool IsPrimary, string? UsingPrimaryIndexName, int? PrimaryXmlIndexUsage);
1187+
1188+
internal sealed record FullTextCatalogDef(string Name, bool AccentSensitive, bool IsDefault, string Owner);
1189+
1190+
internal sealed record FullTextIndexColumnDef(string Column, int LanguageId, string? TypeColumn);
1191+
1192+
internal sealed record FullTextIndexDef(string SchemaName, string TableName, string CatalogName, string KeyIndexName, FullTextIndexColumnDef[] Columns);
1193+
10981194
internal sealed record UserDefinedDataTypeDef(string SchemaName, string TypeName, string BaseType, bool Nullable);
10991195

11001196
internal sealed record XmlSchemaCollectionDef(string SchemaName, string CollectionName, string XsdText);
@@ -1283,6 +1379,83 @@ private static XElement BuildViewIndexElement(XNamespace ns, ViewIndexDef vi)
12831379
return element;
12841380
}
12851381

1382+
private static XElement Relationship(XNamespace ns, string name, string reference) =>
1383+
new(ns + "Relationship",
1384+
new XAttribute("Name", name),
1385+
new XElement(ns + "Entry",
1386+
new XElement(ns + "References",
1387+
new XAttribute("Name", reference))));
1388+
1389+
private static XElement BuildXmlIndexElement(XNamespace ns, XmlIndexDef xi)
1390+
{
1391+
var element = new XElement(ns + "Element",
1392+
new XAttribute("Type", "SqlXmlIndex"),
1393+
new XAttribute("Name", $"[{xi.SchemaName}].[{xi.TableName}].[{xi.IndexName}]"));
1394+
if (xi.IsPrimary)
1395+
{
1396+
element.Add(new XElement(ns + "Property",
1397+
new XAttribute("Name", "IsPrimary"), new XAttribute("Value", "True")));
1398+
}
1399+
else
1400+
{
1401+
element.Add(new XElement(ns + "Property",
1402+
new XAttribute("Name", "PrimaryXmlIndexUsage"),
1403+
new XAttribute("Value", xi.PrimaryXmlIndexUsage!.Value.ToString(System.Globalization.CultureInfo.InvariantCulture))));
1404+
}
1405+
element.Add(Relationship(ns, "Column", $"[{xi.SchemaName}].[{xi.TableName}].[{xi.Column}]"));
1406+
element.Add(Relationship(ns, "IndexedObject", $"[{xi.SchemaName}].[{xi.TableName}]"));
1407+
if (!xi.IsPrimary)
1408+
element.Add(Relationship(ns, "UsingPrimaryXmlIndex", $"[{xi.SchemaName}].[{xi.TableName}].[{xi.UsingPrimaryIndexName}]"));
1409+
return element;
1410+
}
1411+
1412+
private static XElement BuildFullTextCatalogElement(XNamespace ns, FullTextCatalogDef cat)
1413+
{
1414+
var element = new XElement(ns + "Element",
1415+
new XAttribute("Type", "SqlFullTextCatalog"),
1416+
new XAttribute("Name", $"[{cat.Name}]"),
1417+
new XElement(ns + "Property",
1418+
new XAttribute("Name", "IsAccentSensitive"), new XAttribute("Value", cat.AccentSensitive ? "True" : "False")));
1419+
if (cat.IsDefault)
1420+
{
1421+
element.Add(new XElement(ns + "Property",
1422+
new XAttribute("Name", "IsDefault"), new XAttribute("Value", "True")));
1423+
}
1424+
element.Add(new XElement(ns + "Relationship",
1425+
new XAttribute("Name", "Authorizer"),
1426+
new XElement(ns + "Entry",
1427+
new XElement(ns + "References",
1428+
new XAttribute("ExternalSource", "BuiltIns"),
1429+
new XAttribute("Name", $"[{cat.Owner}]")))));
1430+
return element;
1431+
}
1432+
1433+
private static XElement BuildFullTextIndexElement(XNamespace ns, FullTextIndexDef fti)
1434+
{
1435+
var element = new XElement(ns + "Element",
1436+
new XAttribute("Type", "SqlFullTextIndex"),
1437+
new XAttribute("Name", $"[{fti.SchemaName}].[{fti.TableName}]"));
1438+
element.Add(Relationship(ns, "Catalog", $"[{fti.CatalogName}]"));
1439+
1440+
var columns = new XElement(ns + "Relationship", new XAttribute("Name", "Columns"));
1441+
foreach (var col in fti.Columns)
1442+
{
1443+
var spec = new XElement(ns + "Element",
1444+
new XAttribute("Type", "SqlFullTextIndexColumnSpecifier"),
1445+
new XElement(ns + "Property",
1446+
new XAttribute("Name", "LanguageId"),
1447+
new XAttribute("Value", col.LanguageId.ToString(System.Globalization.CultureInfo.InvariantCulture))),
1448+
Relationship(ns, "Column", $"[{fti.SchemaName}].[{fti.TableName}].[{col.Column}]"));
1449+
if (col.TypeColumn is not null)
1450+
spec.Add(Relationship(ns, "TypeColumn", $"[{fti.SchemaName}].[{fti.TableName}].[{col.TypeColumn}]"));
1451+
columns.Add(new XElement(ns + "Entry", spec));
1452+
}
1453+
element.Add(columns);
1454+
element.Add(Relationship(ns, "IndexedObject", $"[{fti.SchemaName}].[{fti.TableName}]"));
1455+
element.Add(Relationship(ns, "KeyName", $"[{fti.SchemaName}].[{fti.KeyIndexName}]"));
1456+
return element;
1457+
}
1458+
12861459
private static XElement BuildPermissionElement(XNamespace ns, PermissionDef perm)
12871460
{
12881461
// Name shape: `[Action.PermissionCamelCase.Database].[grantee].[grantor]`.
@@ -1314,6 +1487,14 @@ private static XElement BuildExtendedPropertyElement(XNamespace ns, ExtendedProp
13141487
("SqlConstraint",
13151488
$"[SqlConstraint].[{ep.SchemaName}].[{ep.TableName}].[{ep.PropertyName}]",
13161489
$"[{ep.SchemaName}].[{ep.TableName}]"),
1490+
ExtendedPropertyHost.DdlTrigger =>
1491+
("SqlDatabaseDdlTrigger",
1492+
$"[SqlDatabaseDdlTrigger].[{ep.SchemaName}].[{ep.PropertyName}]",
1493+
$"[{ep.SchemaName}]"),
1494+
ExtendedPropertyHost.Filegroup =>
1495+
("SqlFilegroup",
1496+
$"[SqlFilegroup].[{ep.SchemaName}].[{ep.PropertyName}]",
1497+
$"[{ep.SchemaName}]"),
13171498
ExtendedPropertyHost.Unknown =>
13181499
(ep.UnknownHostKind ?? "Unknown",
13191500
$"[{ep.UnknownHostKind}].[{ep.SchemaName}].[{ep.PropertyName}]",

0 commit comments

Comments
 (0)