From 19bf67cada8d3a6e228dc6f545d903286b68a93b Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 10 Jun 2026 08:00:48 +0500 Subject: [PATCH] Add DATA_COMPRESSION support for primary keys and unique constraints Fixes #33145. SQL Server PRIMARY KEY / UNIQUE constraints accept WITH (DATA_COMPRESSION = ...) just like indexes do. This adds first-class support, mirroring the existing index DATA_COMPRESSION and key FILLFACTOR features: - UseDataCompression on KeyBuilder / KeyBuilder / IConventionKeyBuilder (plus CanSetDataCompression), and Get/SetDataCompression on the key metadata. - SqlServerAnnotationProvider attaches the annotation to the unique constraint. The shared IndexOptions already emits DATA_COMPRESSION for AddPrimaryKey / AddUniqueConstraint operations, so the migrations generator needs no change. - Fluent-API, runtime-annotation, and runtime-model code generation handling. As with the index case, DATA_COMPRESSION is not reverse-engineered. --- .../SqlServerAnnotationCodeGenerator.cs | 6 ++ ...verCSharpRuntimeAnnotationCodeGenerator.cs | 2 + .../EFCore.SqlServer.baseline.json | 27 ++++++ .../SqlServerKeyBuilderExtensions.cs | 82 +++++++++++++++++++ .../Extensions/SqlServerKeyExtensions.cs | 67 +++++++++++++++ .../SqlServerRuntimeModelConvention.cs | 1 + .../Internal/SqlServerAnnotationProvider.cs | 5 ++ .../Migrations/MigrationsSqlServerTest.cs | 38 +++++++++ .../SqlServerBuilderExtensionsTest.cs | 30 +++++++ 9 files changed, 258 insertions(+) diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 6a7713f503d..c175dfc36d2 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -124,6 +124,10 @@ private static readonly MethodInfo KeyHasFillFactorMethodInfo = typeof(SqlServerKeyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerKeyBuilderExtensions.HasFillFactor), [typeof(KeyBuilder), typeof(int)])!; + private static readonly MethodInfo KeyUseDataCompressionMethodInfo + = typeof(SqlServerKeyBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerKeyBuilderExtensions.UseDataCompression), [typeof(KeyBuilder), typeof(DataCompressionType)])!; + private static readonly MethodInfo TableIsTemporalMethodInfo = typeof(SqlServerTableBuilderExtensions).GetRuntimeMethod( nameof(SqlServerTableBuilderExtensions.IsTemporal), [typeof(TableBuilder), typeof(bool)])!; @@ -456,6 +460,8 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an SqlServerAnnotationNames.FillFactor => new MethodCallCodeFragment(KeyHasFillFactorMethodInfo, annotation.Value), + SqlServerAnnotationNames.DataCompression => new MethodCallCodeFragment(KeyUseDataCompressionMethodInfo, annotation.Value), + _ => null }; diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs index d6244c596a6..2bc9905d8b3 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs @@ -143,6 +143,7 @@ public override void Generate(IKey key, CSharpRuntimeAnnotationCodeGeneratorPara var annotations = parameters.Annotations; annotations.Remove(SqlServerAnnotationNames.Clustered); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } base.Generate(key, parameters); @@ -156,6 +157,7 @@ public override void Generate(IUniqueConstraint uniqueConstraint, CSharpRuntimeA var annotations = parameters.Annotations; annotations.Remove(SqlServerAnnotationNames.Clustered); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } base.Generate(uniqueConstraint, parameters); diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json index 8de92252d64..535722e3967 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json +++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json @@ -1751,6 +1751,9 @@ { "Type": "static class Microsoft.EntityFrameworkCore.SqlServerKeyBuilderExtensions", "Methods": [ + { + "Member": "static bool CanSetDataCompression(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder keyBuilder, Microsoft.EntityFrameworkCore.DataCompressionType? dataCompressionType, bool fromDataAnnotation = false);" + }, { "Member": "static bool CanSetFillFactor(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder keyBuilder, int? fillFactor, bool fromDataAnnotation = false);" }, @@ -1774,12 +1777,30 @@ }, { "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder? IsClustered(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder keyBuilder, bool? clustered, bool fromDataAnnotation = false);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.KeyBuilder UseDataCompression(this Microsoft.EntityFrameworkCore.Metadata.Builders.KeyBuilder keyBuilder, Microsoft.EntityFrameworkCore.DataCompressionType dataCompressionType);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.KeyBuilder UseDataCompression(this Microsoft.EntityFrameworkCore.Metadata.Builders.KeyBuilder keyBuilder, Microsoft.EntityFrameworkCore.DataCompressionType dataCompressionType);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder? UseDataCompression(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder keyBuilder, Microsoft.EntityFrameworkCore.DataCompressionType? dataCompressionType, bool fromDataAnnotation = false);" } ] }, { "Type": "static class Microsoft.EntityFrameworkCore.SqlServerKeyExtensions", "Methods": [ + { + "Member": "static Microsoft.EntityFrameworkCore.DataCompressionType? GetDataCompression(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey key);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.DataCompressionType? GetDataCompression(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey key, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetDataCompressionConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionKey key);" + }, { "Member": "static int? GetFillFactor(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey key);" }, @@ -1798,6 +1819,12 @@ { "Member": "static bool? IsClustered(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey key, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject);" }, + { + "Member": "static void SetDataCompression(this Microsoft.EntityFrameworkCore.Metadata.IMutableKey key, Microsoft.EntityFrameworkCore.DataCompressionType? dataCompression);" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.DataCompressionType? SetDataCompression(this Microsoft.EntityFrameworkCore.Metadata.IConventionKey key, Microsoft.EntityFrameworkCore.DataCompressionType? dataCompression, bool fromDataAnnotation = false);" + }, { "Member": "static void SetFillFactor(this Microsoft.EntityFrameworkCore.Metadata.IMutableKey key, int? fillFactor);" }, diff --git a/src/EFCore.SqlServer/Extensions/SqlServerKeyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerKeyBuilderExtensions.cs index 4164b264750..2ce911fffbf 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerKeyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerKeyBuilderExtensions.cs @@ -178,4 +178,86 @@ public static bool CanSetFillFactor( int? fillFactor, bool fromDataAnnotation = false) => keyBuilder.CanSetAnnotation(SqlServerAnnotationNames.FillFactor, fillFactor, fromDataAnnotation); + + /// + /// Configures whether the key is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The builder for the key being configured. + /// A value indicating the data compression option to be used. + /// A builder to further configure the key. + public static KeyBuilder UseDataCompression(this KeyBuilder keyBuilder, DataCompressionType dataCompressionType) + { + keyBuilder.Metadata.SetDataCompression(dataCompressionType); + + return keyBuilder; + } + + /// + /// Configures whether the key is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The builder for the key being configured. + /// A value indicating the data compression option to be used. + /// A builder to further configure the key. + public static KeyBuilder UseDataCompression( + this KeyBuilder keyBuilder, + DataCompressionType dataCompressionType) + => (KeyBuilder)UseDataCompression((KeyBuilder)keyBuilder, dataCompressionType); + + /// + /// Configures whether the key is created with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The builder for the key 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 IConventionKeyBuilder? UseDataCompression( + this IConventionKeyBuilder keyBuilder, + DataCompressionType? dataCompressionType, + bool fromDataAnnotation = false) + { + if (keyBuilder.CanSetDataCompression(dataCompressionType, fromDataAnnotation)) + { + keyBuilder.Metadata.SetDataCompression(dataCompressionType, fromDataAnnotation); + + return keyBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the key can be configured with data compression option when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The builder for the key being configured. + /// A value indicating the data compression option to be used. + /// Indicates whether the configuration was specified using a data annotation. + /// if the key can be configured with data compression option when targeting SQL Server. + public static bool CanSetDataCompression( + this IConventionKeyBuilder keyBuilder, + DataCompressionType? dataCompressionType, + bool fromDataAnnotation = false) + => keyBuilder.CanSetAnnotation(SqlServerAnnotationNames.DataCompression, dataCompressionType, fromDataAnnotation); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs index be2a27d2ec0..c8d8cc485c9 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs @@ -163,4 +163,71 @@ public static void SetFillFactor(this IMutableKey key, int? fillFactor) /// The for whether the key uses the fill factor. public static ConfigurationSource? GetFillFactorConfigurationSource(this IConventionKey key) => key.FindAnnotation(SqlServerAnnotationNames.FillFactor)?.GetConfigurationSource(); + + /// + /// Returns the data compression that the key uses. + /// + /// The key. + /// The data compression that the key uses. + public static DataCompressionType? GetDataCompression(this IReadOnlyKey key) + => (key is RuntimeKey) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (DataCompressionType?)key[SqlServerAnnotationNames.DataCompression]; + + /// + /// Returns the data compression that the key uses. + /// + /// The key. + /// The identifier of the store object. + /// The data compression that the key uses. + public static DataCompressionType? GetDataCompression(this IReadOnlyKey key, in StoreObjectIdentifier storeObject) + { + if (key is RuntimeKey) + { + throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + } + + var annotation = key.FindAnnotation(SqlServerAnnotationNames.DataCompression); + if (annotation != null) + { + return (DataCompressionType?)annotation.Value; + } + + var sharedTableRootKey = key.FindSharedObjectRootKey(storeObject); + return sharedTableRootKey?.GetDataCompression(storeObject); + } + + /// + /// Sets a value indicating the data compression the key uses. + /// + /// The key. + /// The value to set. + public static void SetDataCompression(this IMutableKey key, DataCompressionType? dataCompression) + => key.SetAnnotation( + SqlServerAnnotationNames.DataCompression, + dataCompression); + + /// + /// Sets a value indicating the data compression the key uses. + /// + /// The key. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static DataCompressionType? SetDataCompression( + this IConventionKey key, + DataCompressionType? dataCompression, + bool fromDataAnnotation = false) + => (DataCompressionType?)key.SetAnnotation( + SqlServerAnnotationNames.DataCompression, + dataCompression, + fromDataAnnotation)?.Value; + + /// + /// Returns the for the data compression the key uses. + /// + /// The key. + /// The for the data compression the key uses. + public static ConfigurationSource? GetDataCompressionConfigurationSource(this IConventionKey key) + => key.FindAnnotation(SqlServerAnnotationNames.DataCompression)?.GetConfigurationSource(); } diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs index abec9d2859b..eafe45f69b2 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs @@ -117,6 +117,7 @@ protected override void ProcessKeyAnnotations( { annotations.Remove(SqlServerAnnotationNames.Clustered); annotations.Remove(SqlServerAnnotationNames.FillFactor); + annotations.Remove(SqlServerAnnotationNames.DataCompression); } } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index 32742db8969..ab6792cc453 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -175,6 +175,11 @@ public override IEnumerable For(IUniqueConstraint constraint, bool { yield return new Annotation(SqlServerAnnotationNames.FillFactor, fillFactor); } + + if (key.GetDataCompression() is { } dataCompression) + { + yield return new Annotation(SqlServerAnnotationNames.DataCompression, dataCompression); + } } /// diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 71298a8d1ca..5c2f745899b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -390,6 +390,44 @@ CONSTRAINT [AK_People_TheAlternateKey] UNIQUE ([TheAlternateKey]) WITH (FILLFACT """); } + [Fact] + public virtual async Task Create_table_with_data_compression() + { + await Test( + _ => { }, + builder => + { + builder.Entity("People").Property("TheKey"); + builder.Entity("People").Property("TheAlternateKey"); + builder.Entity("People").HasKey("TheKey").UseDataCompression(DataCompressionType.Page); + builder.Entity("People").HasAlternateKey("TheAlternateKey").UseDataCompression(DataCompressionType.Row); + }, + model => + { + var table = Assert.Single(model.Tables); + + // DATA_COMPRESSION is not currently reverse-engineered, so it round-trips as null on the + // scaffolded model (same as the index case); the generated SQL below is the real assertion. + var primaryKey = table.PrimaryKey; + Assert.NotNull(primaryKey); + Assert.Null(primaryKey[SqlServerAnnotationNames.DataCompression]); + + var uniqueConstraint = table.UniqueConstraints.FirstOrDefault(); + Assert.NotNull(uniqueConstraint); + Assert.Null(uniqueConstraint[SqlServerAnnotationNames.DataCompression]); + }); + + AssertSql( + """ +CREATE TABLE [People] ( + [TheKey] int NOT NULL IDENTITY, + [TheAlternateKey] uniqueidentifier NOT NULL, + CONSTRAINT [PK_People] PRIMARY KEY ([TheKey]) WITH (DATA_COMPRESSION = PAGE), + CONSTRAINT [AK_People_TheAlternateKey] UNIQUE ([TheAlternateKey]) WITH (DATA_COMPRESSION = ROW) +); +"""); + } + public override async Task Drop_table() { await base.Drop_table(); diff --git a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs index 85702153fa2..a5aca5baec0 100644 --- a/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs +++ b/test/EFCore.SqlServer.Tests/Metadata/SqlServerBuilderExtensionsTest.cs @@ -157,6 +157,36 @@ public void Can_set_key_with_fillfactor_non_generic() Assert.Equal(90, key.GetFillFactor()); } + [Fact] + public void Can_set_key_with_data_compression() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity() + .HasKey(e => e.Id) + .UseDataCompression(DataCompressionType.Page); + + var key = modelBuilder.Model.FindEntityType(typeof(Customer)).FindPrimaryKey(); + + Assert.Equal(DataCompressionType.Page, key.GetDataCompression()); + } + + [Fact] + public void Can_set_key_with_data_compression_non_generic() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity(typeof(Customer)) + .HasKey("Id") + .UseDataCompression(DataCompressionType.Row); + + var key = modelBuilder.Model.FindEntityType(typeof(Customer)).FindPrimaryKey(); + + Assert.Equal(DataCompressionType.Row, key.GetDataCompression()); + } + [Fact] public void Can_set_index_include() {