From 9a540234af98efb2e81eaf357b952802221dbddd Mon Sep 17 00:00:00 2001 From: hmajerus Date: Fri, 5 May 2023 00:27:24 -0500 Subject: [PATCH 1/2] Add support for DATA_COMPRESSION and SORT_IN_TEMPDB index options Closes #30408 --- src/EFCore.SqlServer/DataCompressionType.cs | 29 +++ ...verCSharpRuntimeAnnotationCodeGenerator.cs | 4 + .../SqlServerIndexBuilderExtensions.cs | 172 +++++++++++++ .../Extensions/SqlServerIndexExtensions.cs | 132 ++++++++++ .../SqlServerRuntimeModelConvention.cs | 2 + .../Internal/SqlServerAnnotationNames.cs | 16 ++ .../Internal/SqlServerAnnotationProvider.cs | 10 + .../Internal/SqlServerIndexExtensions.cs | 34 +++ .../SqlServerMigrationsSqlGenerator.cs | 21 ++ .../Properties/SqlServerStrings.Designer.cs | 16 ++ .../Properties/SqlServerStrings.resx | 6 + .../CSharpRuntimeModelCodeGeneratorTest.cs | 12 +- .../Migrations/MigrationsSqlServerTest.cs | 99 ++++++++ .../SqlServerMigrationsSqlGeneratorTest.cs | 43 ++++ .../SqlServerModelValidatorTest.cs | 32 +++ .../SqlServerBuilderExtensionsTest.cs | 66 +++++ .../Migrations/SqlServerModelDifferTest.cs | 237 ++++++++++++++++++ 17 files changed, 930 insertions(+), 1 deletion(-) create mode 100644 src/EFCore.SqlServer/DataCompressionType.cs diff --git a/src/EFCore.SqlServer/DataCompressionType.cs b/src/EFCore.SqlServer/DataCompressionType.cs new file mode 100644 index 00000000000..058f8f0567d --- /dev/null +++ b/src/EFCore.SqlServer/DataCompressionType.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore; + +/// +/// Indicates type of data compression used on a index. +/// +/// +/// See Data Compression for more information on data compression. +/// +public enum DataCompressionType +{ + /// + /// Index is not compressed. + /// + None, + + /// + /// Index is compressed by using row compression. + /// + Row, + + /// + /// Index is compressed by using page compression. + /// + Page +} diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs index 793eb75ca00..7b1b13d36de 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs @@ -104,6 +104,8 @@ public override void Generate(IIndex index, CSharpRuntimeAnnotationCodeGenerator annotations.Remove(SqlServerAnnotationNames.CreatedOnline); annotations.Remove(SqlServerAnnotationNames.Include); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.SortedInTempDb); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } base.Generate(index, parameters); @@ -119,6 +121,8 @@ public override void Generate(ITableIndex index, CSharpRuntimeAnnotationCodeGene annotations.Remove(SqlServerAnnotationNames.CreatedOnline); annotations.Remove(SqlServerAnnotationNames.Include); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.SortedInTempDb); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } base.Generate(index, parameters); diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs index 4d5e3de65db..5362f8d3cb6 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs @@ -391,4 +391,176 @@ public static bool CanSetFillFactor( int? fillFactor, bool fromDataAnnotation = false) => indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.FillFactor, fillFactor, fromDataAnnotation); + + /// + /// Configures whether the index is created with sort in tempdb option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with sort in tempdb option. + /// A builder to further configure the index. + public static IndexBuilder IsSortedInTempDb(this IndexBuilder indexBuilder, bool sortedInTempDb = true) + { + indexBuilder.Metadata.SetIsSortedInTempDb(sortedInTempDb); + + return indexBuilder; + } + + /// + /// Configures whether the index is created with sort in tempdb option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with sort in tempdb option. + /// A builder to further configure the index. + public static IndexBuilder IsSortedInTempDb( + this IndexBuilder indexBuilder, + bool sortedInTempDb = true) + => (IndexBuilder)IsSortedInTempDb((IndexBuilder)indexBuilder, sortedInTempDb); + + /// + /// Configures whether the index is created with sort in tempdb option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with sort in tempdb option. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionIndexBuilder? IsSortedInTempDb( + this IConventionIndexBuilder indexBuilder, + bool? sortedInTempDb, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetIsSortedInTempDb(sortedInTempDb, fromDataAnnotation)) + { + indexBuilder.Metadata.SetIsSortedInTempDb(sortedInTempDb, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the index can be configured with sort in tempdb option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating whether the index is created with sort in tempdb option. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + /// if the index can be configured with sort in tempdb option when targeting SQL Server. + public static bool CanSetIsSortedInTempDb( + this IConventionIndexBuilder indexBuilder, + bool? sortedInTempDb, + bool fromDataAnnotation = false) + => indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.SortedInTempDb, sortedInTempDb, fromDataAnnotation); + + /// + /// Configures whether the index is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating the data compression option to be used. + /// A builder to further configure the index. + public static IndexBuilder UseDataCompression(this IndexBuilder indexBuilder, DataCompressionType dataCompressionType) + { + indexBuilder.Metadata.SetDataCompression(dataCompressionType); + + return indexBuilder; + } + + /// + /// Configures whether the index is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating the data compression option to be used. + /// A builder to further configure the index. + public static IndexBuilder UseDataCompression( + this IndexBuilder indexBuilder, + DataCompressionType dataCompressionType) + => (IndexBuilder)UseDataCompression((IndexBuilder)indexBuilder, dataCompressionType); + + /// + /// Configures whether the index is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating the data compression option to be used. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionIndexBuilder? UseDataCompression( + this IConventionIndexBuilder indexBuilder, + DataCompressionType? dataCompressionType, + bool fromDataAnnotation = false) + { + if (indexBuilder.CanSetDataCompression(dataCompressionType, fromDataAnnotation)) + { + indexBuilder.Metadata.SetDataCompression(dataCompressionType, fromDataAnnotation); + + return indexBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the index can be configured with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the index being configured. + /// A value indicating the data compression option to be used. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + /// if the index can be configured with data compression option when targeting SQL Server. + public static bool CanSetDataCompression( + this IConventionIndexBuilder indexBuilder, + DataCompressionType? dataCompressionType, + bool fromDataAnnotation = false) + => indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.DataCompression, dataCompressionType, fromDataAnnotation); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs index 7568b318f2b..63510e05e2c 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs @@ -297,4 +297,136 @@ public static void SetFillFactor(this IMutableIndex index, int? fillFactor) /// The for whether the index uses the fill factor. public static ConfigurationSource? GetFillFactorConfigurationSource(this IConventionIndex index) => index.FindAnnotation(SqlServerAnnotationNames.FillFactor)?.GetConfigurationSource(); + + /// + /// Returns a value indicating whether the index is sorted in tempdb. + /// + /// The index. + /// if the index is sorted in tempdb. + public static bool? GetIsSortedInTempDb(this IReadOnlyIndex index) + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (bool?)index[SqlServerAnnotationNames.SortedInTempDb]; + + /// + /// Returns a value indicating whether the index is sorted in tempdb. + /// + /// The index. + /// The identifier of the store object. + /// if the index is sorted in tempdb. + public static bool? GetIsSortedInTempDb(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject) + { + if (index is RuntimeIndex) + { + throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + } + + var annotation = index.FindAnnotation(SqlServerAnnotationNames.SortedInTempDb); + if (annotation != null) + { + return (bool?)annotation.Value; + } + + var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject); + return sharedTableRootIndex?.GetIsSortedInTempDb(storeObject); + } + + /// + /// Sets a value indicating whether the index is sorted in tempdb. + /// + /// The index. + /// The value to set. + public static void SetIsSortedInTempDb(this IMutableIndex index, bool? sortedInTempDb) + => index.SetAnnotation( + SqlServerAnnotationNames.SortedInTempDb, + sortedInTempDb); + + /// + /// Sets a value indicating whether the index is sorted in tempdb. + /// + /// The index. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsSortedInTempDb( + this IConventionIndex index, + bool? sortedInTempDb, + bool fromDataAnnotation = false) + => (bool?)index.SetAnnotation( + SqlServerAnnotationNames.SortedInTempDb, + sortedInTempDb, + fromDataAnnotation)?.Value; + + /// + /// Returns the for whether the index is sorted in tempdb. + /// + /// The index. + /// The for whether the index is sorted in tempdb. + public static ConfigurationSource? GetIsSortedInTempDbConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(SqlServerAnnotationNames.SortedInTempDb)?.GetConfigurationSource(); + + /// + /// Returns the data compression that the index uses. + /// + /// The index. + /// The data compression that the index uses + public static DataCompressionType? GetDataCompression(this IReadOnlyIndex index) + => (index is RuntimeIndex) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (DataCompressionType?)index[SqlServerAnnotationNames.DataCompression]; + + /// + /// Returns the data compression that the index uses. + /// + /// The index. + /// The identifier of the store object. + /// The data compression that the index uses + public static DataCompressionType? GetDataCompression(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject) + { + if (index is RuntimeIndex) + { + throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + } + + var annotation = index.FindAnnotation(SqlServerAnnotationNames.DataCompression); + if (annotation != null) + { + return (DataCompressionType?)annotation.Value; + } + + var sharedTableRootIndex = index.FindSharedObjectRootIndex(storeObject); + return sharedTableRootIndex?.GetDataCompression(storeObject); + } + + /// + /// Sets a value indicating the data compression the index uses. + /// + /// The index. + /// The value to set. + public static void SetDataCompression(this IMutableIndex index, DataCompressionType? dataCompression) => index.SetAnnotation( + SqlServerAnnotationNames.DataCompression, + dataCompression); + + /// + /// Sets a value indicating the data compression the index uses. + /// + /// The index. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static DataCompressionType? SetDataCompression( + this IConventionIndex index, + DataCompressionType? dataCompression, + bool fromDataAnnotation = false) => (DataCompressionType?)index.SetAnnotation( + SqlServerAnnotationNames.DataCompression, + dataCompression, + fromDataAnnotation)?.Value; + + /// + /// Returns the for the data compression the index uses. + /// + /// The index. + /// The for the data compression the index uses. + public static ConfigurationSource? GetDataCompressionConfigurationSource(this IConventionIndex index) + => index.FindAnnotation(SqlServerAnnotationNames.DataCompression)?.GetConfigurationSource(); } diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs index e9118e97711..eecf6562e9c 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs @@ -101,6 +101,8 @@ protected override void ProcessIndexAnnotations( annotations.Remove(SqlServerAnnotationNames.CreatedOnline); annotations.Remove(SqlServerAnnotationNames.Include); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.SortedInTempDb); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs index 8bf33226c28..4352a29ab49 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs @@ -51,6 +51,22 @@ public static class SqlServerAnnotationNames /// public const string FillFactor = Prefix + "FillFactor"; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public const string SortedInTempDb = Prefix + "SortInTempDb"; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public const string DataCompression = Prefix + "DataCompression"; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index 80b2364a49a..040ad163f9c 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -203,6 +203,16 @@ public override IEnumerable For(ITableIndex index, bool designTime) { yield return new Annotation(SqlServerAnnotationNames.FillFactor, fillFactor); } + + if (modelIndex.GetIsSortedInTempDb(table) is bool isSortedInTempDb) + { + yield return new Annotation(SqlServerAnnotationNames.SortedInTempDb, isSortedInTempDb); + } + + if (modelIndex.GetDataCompression(table) is DataCompressionType dataCompressionType) + { + yield return new Annotation(SqlServerAnnotationNames.DataCompression, dataCompressionType); + } } /// diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs index 49f21891482..369a26a0dbc 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs @@ -100,6 +100,40 @@ public static bool AreCompatibleForSqlServer( return false; } + if (index.GetIsSortedInTempDb() != duplicateIndex.GetIsSortedInTempDb()) + { + if (shouldThrow) + { + throw new InvalidOperationException( + SqlServerStrings.DuplicateIndexSortInTempDbMismatch( + index.DisplayName(), + index.DeclaringEntityType.DisplayName(), + duplicateIndex.DisplayName(), + duplicateIndex.DeclaringEntityType.DisplayName(), + index.DeclaringEntityType.GetSchemaQualifiedTableName(), + index.GetDatabaseName(storeObject))); + } + + return false; + } + + if (index.GetDataCompression() != duplicateIndex.GetDataCompression()) + { + if (shouldThrow) + { + throw new InvalidOperationException( + SqlServerStrings.DuplicateIndexDataCompressionMismatch( + index.DisplayName(), + index.DeclaringEntityType.DisplayName(), + duplicateIndex.DisplayName(), + duplicateIndex.DeclaringEntityType.DisplayName(), + index.DeclaringEntityType.GetSchemaQualifiedTableName(), + index.GetDatabaseName(storeObject))); + } + + return false; + } + return true; static bool SameColumnNames(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, StoreObjectIdentifier storeObject) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 30b6458ec2c..e5bee4cec81 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1852,6 +1852,27 @@ private static void IndexWithOptions(CreateIndexOperation operation, MigrationCo options.Add("ONLINE = ON"); } + if (operation[SqlServerAnnotationNames.SortedInTempDb] is bool isSortedInTempDb && isSortedInTempDb) + { + options.Add("SORT_IN_TEMPDB = ON"); + } + + if (operation[SqlServerAnnotationNames.DataCompression] is DataCompressionType dataCompressionType) + { + switch (dataCompressionType) + { + case DataCompressionType.None: + options.Add("DATA_COMPRESSION = NONE"); + break; + case DataCompressionType.Row: + options.Add("DATA_COMPRESSION = ROW"); + break; + case DataCompressionType.Page: + options.Add("DATA_COMPRESSION = PAGE"); + break; + } + } + if (options.Count > 0) { builder diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index ec120fdeef9..bf9b28eeb20 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -91,6 +91,14 @@ public static string DuplicateIndexClusteredMismatch(object? index1, object? ent GetString("DuplicateIndexClusteredMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), index1, entityType1, index2, entityType2, table, indexName); + /// + /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different data compression configurations. + /// + public static string DuplicateIndexDataCompressionMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) + => string.Format( + GetString("DuplicateIndexDataCompressionMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), + index1, entityType1, index2, entityType2, table, indexName); + /// /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different fill factor configurations. /// @@ -115,6 +123,14 @@ public static string DuplicateIndexOnlineMismatch(object? index1, object? entity GetString("DuplicateIndexOnlineMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), index1, entityType1, index2, entityType2, table, indexName); + /// + /// The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different sort in tempdb configurations. + /// + public static string DuplicateIndexSortInTempDbMismatch(object? index1, object? entityType1, object? index2, object? entityType2, object? table, object? indexName) + => string.Format( + GetString("DuplicateIndexSortInTempDbMismatch", nameof(index1), nameof(entityType1), nameof(index2), nameof(entityType2), nameof(table), nameof(indexName)), + index1, entityType1, index2, entityType2, table, indexName); + /// /// The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index be1db71e321..f7a64f369e2 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -153,6 +153,12 @@ The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different online configurations. + + The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different sort in tempdb configurations. + + + The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}', but have different data compression configurations. + The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations. diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 01d7dd474ec..1ae6ba65269 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -5539,6 +5539,14 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal( CoreStrings.RuntimeModelMissingData, Assert.Throws(() => alternateIndex.GetIncludeProperties()).Message); + Assert.Null(alternateIndex[SqlServerAnnotationNames.SortedInTempDb]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.GetIsSortedInTempDb()).Message); + Assert.Null(alternateIndex[SqlServerAnnotationNames.DataCompression]); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => alternateIndex.GetDataCompression()).Message); Assert.Equal(new[] { alternateIndex }, principalBaseId.GetContainingIndexes()); @@ -5728,7 +5736,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasFilter("AlternateId <> NULL") .IsCreatedOnline() .HasFillFactor(40) - .IncludeProperties(e => e.Id); + .IncludeProperties(e => e.Id) + .IsSortedInTempDb() + .UseDataCompression(DataCompressionType.Page); }); modelBuilder.Entity>>( diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index a3a39076926..093e0f1f09e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -2228,6 +2228,105 @@ FROM [sys].[default_constraints] [d] """); } + [ConditionalFact] + public virtual async Task Create_index_unique_with_include_fillfactor_and_sortintempdb() + { + await Test( + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.Property("FirstName"); + e.Property("LastName"); + e.Property("Name").IsRequired(); + }), + builder => { }, + builder => builder.Entity("People").HasIndex("Name") + .IsUnique() + .IncludeProperties("FirstName", "LastName") + .HasFillFactor(75) + .IsSortedInTempDb(), + model => + { + var table = Assert.Single(model.Tables); + var index = Assert.Single(table.Indexes); + Assert.True(index.IsUnique); + Assert.Null(index.Filter); + Assert.Equal(1, index.Columns.Count); + Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); + var includedColumns = (IReadOnlyList?)index[SqlServerAnnotationNames.Include]; + Assert.Null(includedColumns); + Assert.Equal(75, index[SqlServerAnnotationNames.FillFactor]); + Assert.Null(index[SqlServerAnnotationNames.SortedInTempDb]); + }); + + AssertSql( +""" +DECLARE @var0 sysname; +SELECT @var0 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [People] ALTER COLUMN [Name] nvarchar(450) NOT NULL; +""", +// +""" +CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WITH (FILLFACTOR = 75, SORT_IN_TEMPDB = ON); +"""); + } + + [ConditionalTheory] + [InlineData(DataCompressionType.None, "NONE")] + [InlineData(DataCompressionType.Row, "ROW")] + [InlineData(DataCompressionType.Page, "PAGE")] + public virtual async Task Create_index_unique_with_include_sortintempdb_and_datacompression(DataCompressionType dataCompression, string dataCompressionSql) + { + await Test( + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.Property("FirstName"); + e.Property("LastName"); + e.Property("Name").IsRequired(); + }), + builder => { }, + builder => builder.Entity("People").HasIndex("Name") + .IsUnique() + .IncludeProperties("FirstName", "LastName") + .IsSortedInTempDb() + .UseDataCompression(dataCompression), + model => + { + var table = Assert.Single(model.Tables); + var index = Assert.Single(table.Indexes); + Assert.True(index.IsUnique); + Assert.Null(index.Filter); + Assert.Equal(1, index.Columns.Count); + Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns); + var includedColumns = (IReadOnlyList?)index[SqlServerAnnotationNames.Include]; + Assert.Null(includedColumns); + Assert.Null(index[SqlServerAnnotationNames.SortedInTempDb]); + Assert.Null(index[SqlServerAnnotationNames.DataCompression]); + }); + + AssertSql( +""" +DECLARE @var0 sysname; +SELECT @var0 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [People] ALTER COLUMN [Name] nvarchar(450) NOT NULL; +""", +// +$""" +CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WITH (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = {dataCompressionSql}); +"""); + } + [ConditionalFact] [SqlServerCondition(SqlServerCondition.SupportsMemoryOptimized)] public virtual async Task Create_index_memoryOptimized_unique_nullable() diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs index e53c068bb06..856732091d4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs @@ -29,6 +29,49 @@ public void CreateIndexOperation_unique_online() """); } + [ConditionalFact] + public void CreateIndexOperation_unique_sortintempdb() + { + Generate( + new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName", "LastName" }, + IsUnique = true, + [SqlServerAnnotationNames.SortedInTempDb] = true + }); + + AssertSql( +""" +CREATE UNIQUE INDEX [IX_People_Name] ON [dbo].[People] ([FirstName], [LastName]) WHERE [FirstName] IS NOT NULL AND [LastName] IS NOT NULL WITH (SORT_IN_TEMPDB = ON); +"""); + } + + [ConditionalTheory] + [InlineData(DataCompressionType.None, "NONE")] + [InlineData(DataCompressionType.Row, "ROW")] + [InlineData(DataCompressionType.Page, "PAGE")] + public void CreateIndexOperation_unique_datacompression(DataCompressionType dataCompression, string dataCompressionSql) + { + Generate( + new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName", "LastName" }, + IsUnique = true, + [SqlServerAnnotationNames.DataCompression] = dataCompression + }); + + AssertSql( +$""" +CREATE UNIQUE INDEX [IX_People_Name] ON [dbo].[People] ([FirstName], [LastName]) WHERE [FirstName] IS NOT NULL AND [LastName] IS NOT NULL WITH (DATA_COMPRESSION = {dataCompressionSql}); +"""); + } + [ConditionalFact] public virtual void AddColumnOperation_identity_legacy() { diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 0e2c71e8591..eb028b7c292 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -402,6 +402,38 @@ public virtual void Detects_duplicate_index_names_within_hierarchy_differently_o modelBuilder); } + [ConditionalFact] + public virtual void Detects_duplicate_index_names_within_hierarchy_different_sort_in_tempdb() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(); + modelBuilder.Entity().HasIndex(c => c.Name).HasDatabaseName("IX_Animal_Name"); + modelBuilder.Entity().HasIndex(d => d.Name).HasDatabaseName("IX_Animal_Name").IsSortedInTempDb(); + + VerifyError( + SqlServerStrings.DuplicateIndexSortInTempDbMismatch( + "{'" + nameof(Dog.Name) + "'}", nameof(Dog), + "{'" + nameof(Cat.Name) + "'}", nameof(Cat), + nameof(Animal), "IX_Animal_Name"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_duplicate_index_names_within_hierarchy_different_data_compression() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(); + modelBuilder.Entity().HasIndex(c => c.Name).HasDatabaseName("IX_Animal_Name"); + modelBuilder.Entity().HasIndex(d => d.Name).HasDatabaseName("IX_Animal_Name").UseDataCompression(DataCompressionType.Page); + + VerifyError( + SqlServerStrings.DuplicateIndexDataCompressionMismatch( + "{'" + nameof(Dog.Name) + "'}", nameof(Dog), + "{'" + nameof(Cat.Name) + "'}", nameof(Cat), + nameof(Animal), "IX_Animal_Name"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_duplicate_index_names_within_hierarchy_with_different_different_include() { diff --git a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs index 1c338a09f9f..4d94b733803 100644 --- a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs +++ b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs @@ -1080,6 +1080,72 @@ public void Throws_if_attempt_to_set_fillfactor_with_argument_out_of_range(int f }); } + [ConditionalFact] + public void Can_set_index_with_sortintempdb() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .HasIndex(e => e.Name) + .IsSortedInTempDb(); + + var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + + Assert.True(index.GetIsSortedInTempDb()); + } + + [ConditionalFact] + public void Can_set_index_with_sortintempdb_non_generic() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity(typeof(Customer)) + .HasIndex("Name") + .IsSortedInTempDb(); + + var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + + Assert.True(index.GetIsSortedInTempDb()); + } + + [ConditionalTheory] + [InlineData(DataCompressionType.None)] + [InlineData(DataCompressionType.Row)] + [InlineData(DataCompressionType.Page)] + public void Can_set_index_with_datacompression(DataCompressionType dataCompression) + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .HasIndex(e => e.Name) + .UseDataCompression(dataCompression); + + var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + + Assert.Equal(dataCompression, index.GetDataCompression()); + } + + [ConditionalTheory] + [InlineData(DataCompressionType.None)] + [InlineData(DataCompressionType.Row)] + [InlineData(DataCompressionType.Page)] + public void Can_set_index_with_datacompression_non_generic(DataCompressionType dataCompression) + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity(typeof(Customer)) + .HasIndex("Name") + .UseDataCompression(dataCompression); + + var index = modelBuilder.Model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + + Assert.Equal(dataCompression, index.GetDataCompression()); + } + #region UseSqlOutputClause [ConditionalFact] diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs index b99f98fdc9c..4171dac604e 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs @@ -1390,4 +1390,241 @@ public void Rebuild_index_with_different_fillfactor_value() Assert.Equal(90, annotationValue); }); + + [ConditionalFact] + public void Dont_rebuild_index_with_unchanged_sortintempdb_option() + => Execute( + source => source + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.HasIndex("Zip") + .IsSortedInTempDb(); + }), + target => target + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.HasIndex("Zip") + .IsSortedInTempDb(); + }), + operations => Assert.Equal(0, operations.Count)); + + [ConditionalFact] + public void Rebuild_index_when_changing_sortintempdb_option() + => Execute( + _ => { }, + source => source + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip"); + }), + target => target + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip") + .IsSortedInTempDb(); + }), + upOps => + { + Assert.Equal(2, upOps.Count); + + var operation1 = Assert.IsType(upOps[0]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation1.GetAnnotations()); + + var operation2 = Assert.IsType(upOps[1]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + var annotation = operation2.GetAnnotation(SqlServerAnnotationNames.SortedInTempDb); + Assert.NotNull(annotation); + + var annotationValue = Assert.IsType(annotation.Value); + Assert.True(annotationValue); + }, + downOps => + { + Assert.Equal(2, downOps.Count); + + var operation1 = Assert.IsType(downOps[0]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation1.GetAnnotations()); + + var operation2 = Assert.IsType(downOps[1]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation2.GetAnnotations()); + }); + + [ConditionalTheory] + [InlineData(DataCompressionType.None)] + [InlineData(DataCompressionType.Row)] + [InlineData(DataCompressionType.Page)] + public void Dont_rebuild_index_with_unchanged_datacompression_option(DataCompressionType dataCompression) + => Execute( + source => source + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.HasIndex("Zip") + .UseDataCompression(dataCompression); + }), + target => target + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.HasIndex("Zip") + .UseDataCompression(dataCompression); + }), + operations => Assert.Equal(0, operations.Count)); + + [ConditionalTheory] + [InlineData(DataCompressionType.None)] + [InlineData(DataCompressionType.Row)] + [InlineData(DataCompressionType.Page)] + public void Rebuild_index_when_adding_datacompression_option(DataCompressionType dataCompression) + => Execute( + _ => { }, + source => source + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip"); + }), + target => target + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip") + .UseDataCompression(dataCompression); + }), + upOps => + { + Assert.Equal(2, upOps.Count); + + var operation1 = Assert.IsType(upOps[0]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation1.GetAnnotations()); + + var operation2 = Assert.IsType(upOps[1]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + var annotation = operation2.GetAnnotation(SqlServerAnnotationNames.DataCompression); + Assert.NotNull(annotation); + + var annotationValue = Assert.IsType(annotation.Value); + Assert.Equal(dataCompression, annotationValue); + }, + downOps => + { + Assert.Equal(2, downOps.Count); + + var operation1 = Assert.IsType(downOps[0]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation1.GetAnnotations()); + + var operation2 = Assert.IsType(downOps[1]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation2.GetAnnotations()); + }); + + [ConditionalFact] + public void Rebuild_index_with_different_datacompression_value() + => Execute( + source => source + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip") + .UseDataCompression(DataCompressionType.Row); + }), + target => target + .Entity( + "Address", + x => + { + x.Property("Id"); + x.Property("Zip"); + x.Property("City"); + x.Property("Street"); + x.HasIndex("Zip") + .UseDataCompression(DataCompressionType.Page); + }), + operations => + { + Assert.Equal(2, operations.Count); + + var operation1 = Assert.IsType(operations[0]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + Assert.Empty(operation1.GetAnnotations()); + + var operation2 = Assert.IsType(operations[1]); + Assert.Equal("Address", operation1.Table); + Assert.Equal("IX_Address_Zip", operation1.Name); + + var annotation = operation2.GetAnnotation(SqlServerAnnotationNames.DataCompression); + Assert.NotNull(annotation); + + var annotationValue = Assert.IsType(annotation.Value); + + Assert.Equal(DataCompressionType.Page, annotationValue); + }); } From 25f512b504008f90be04006706ed028c4add7a2c Mon Sep 17 00:00:00 2001 From: hmajerus Date: Wed, 13 Sep 2023 16:08:39 -0500 Subject: [PATCH 2/2] Add IsSortedInTempDb and UseDataCompression to snapshots with SqlServerIndexBuilderExtensions. Add tests to verify snapshot for IsSortedInTempDb, UseDataCompression, and FillFactor. --- .../SqlServerAnnotationCodeGenerator.cs | 10 ++ .../Migrations/ModelSnapshotSqlServerTest.cs | 111 ++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 78f93736fcf..a2360c095b7 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -79,6 +79,14 @@ private static readonly MethodInfo IndexHasFillFactorMethodInfo = typeof(SqlServerIndexBuilderExtensions).GetRuntimeMethod( nameof(SqlServerIndexBuilderExtensions.HasFillFactor), new[] { typeof(IndexBuilder), typeof(int) })!; + private static readonly MethodInfo IndexIsSortedInTempDbInfo + = typeof(SqlServerIndexBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerIndexBuilderExtensions.IsSortedInTempDb), new[] { typeof(IndexBuilder), typeof(bool) })!; + + private static readonly MethodInfo IndexUseDataCompressionInfo + = typeof(SqlServerIndexBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerIndexBuilderExtensions.UseDataCompression), new[] { typeof(IndexBuilder), typeof(DataCompressionType) })!; + private static readonly MethodInfo KeyIsClusteredMethodInfo = typeof(SqlServerKeyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerKeyBuilderExtensions.IsClustered), new[] { typeof(KeyBuilder), typeof(bool) })!; @@ -347,6 +355,8 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an SqlServerAnnotationNames.Include => new MethodCallCodeFragment(IndexIncludePropertiesMethodInfo, annotation.Value), SqlServerAnnotationNames.FillFactor => new MethodCallCodeFragment(IndexHasFillFactorMethodInfo, annotation.Value), + SqlServerAnnotationNames.SortedInTempDb => new MethodCallCodeFragment(IndexIsSortedInTempDbInfo, annotation.Value), + SqlServerAnnotationNames.DataCompression => new MethodCallCodeFragment(IndexUseDataCompressionInfo, annotation.Value), _ => null }; diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 86fec919a96..884c941520a 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -6010,6 +6010,117 @@ public virtual void IndexAttribute_IncludeProperties_generated_without_fluent_ap Assert.Equal("Name", Assert.Single(index.GetIncludeProperties())); }); + [ConditionalFact] + public virtual void IndexAttribute_HasFillFactor_is_stored_in_snapshot() + => Test( + builder => builder.Entity( + x => + { + x.HasIndex(e => e.Id).HasFillFactor(29); + }), + AddBoilerPlate( + GetHeading() + +""" + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + SqlServerIndexBuilderExtensions.HasFillFactor(b.HasIndex("Id"), 29); + + b.ToTable("EntityWithStringProperty", "DefaultSchema"); + }); +"""), + model => + { + var index = model.GetEntityTypes().First().GetIndexes().First(); + Assert.Equal(29, index.GetFillFactor()); + }); + + [ConditionalFact] + public virtual void IndexAttribute_UseDataCompression_is_stored_in_snapshot() + => Test( + builder => builder.Entity( + x => + { + x.HasIndex(e => e.Id).UseDataCompression(DataCompressionType.Row); + }), + AddBoilerPlate( + GetHeading() + +""" + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + SqlServerIndexBuilderExtensions.UseDataCompression(b.HasIndex("Id"), DataCompressionType.Row); + + b.ToTable("EntityWithStringProperty", "DefaultSchema"); + }); +"""), + model => + { + var index = model.GetEntityTypes().First().GetIndexes().First(); + Assert.Equal(DataCompressionType.Row, index.GetDataCompression()); + }); + + [ConditionalFact] + public virtual void IndexAttribute_IsSortedInTempDb_is_stored_in_snapshot() + => Test( + builder => builder.Entity( + x => + { + x.HasIndex(e => e.Id).IsSortedInTempDb(true); + }), + AddBoilerPlate( + GetHeading() + +""" + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + SqlServerIndexBuilderExtensions.IsSortedInTempDb(b.HasIndex("Id"), true); + + b.ToTable("EntityWithStringProperty", "DefaultSchema"); + }); +"""), + model => + { + var index = model.GetEntityTypes().First().GetIndexes().First(); + Assert.True(index.GetIsSortedInTempDb()); + }); + #endregion #region ForeignKey