@@ -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
10851173internal 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
10961184internal 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+
10981194internal sealed record UserDefinedDataTypeDef ( string SchemaName , string TypeName , string BaseType , bool Nullable ) ;
10991195
11001196internal 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