Skip to content

Commit

Permalink
Add fill factor to keys and unique constraints (#32900)
Browse files Browse the repository at this point in the history
* Add FILLFACTOR for keys

Fixes #32803

* Tidy up KeyWithOptions method and summary test

---------

Co-authored-by: Dean Hunter <[email protected]>
  • Loading branch information
deano-hunter and Dean Hunter authored Feb 16, 2024
1 parent a418545 commit fb2dbcf
Show file tree
Hide file tree
Showing 15 changed files with 773 additions and 8 deletions.
17 changes: 17 additions & 0 deletions src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,11 @@ protected virtual void Generate(
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ADD ");

PrimaryKeyConstraint(operation, model, builder);

KeyWithOptions(operation, builder);

if (terminate)
{
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
Expand All @@ -263,7 +266,11 @@ protected virtual void Generate(
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ADD ");

UniqueConstraint(operation, model, builder);

KeyWithOptions(operation, builder);

builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
EndStatement(builder);
}
Expand Down Expand Up @@ -1709,6 +1716,16 @@ protected virtual void CheckConstraint(
.Append(")");
}

/// <summary>
/// Generates a SQL fragment for extra with options of a key from a
/// <see cref="AddPrimaryKeyOperation" /> or <see cref="AddUniqueConstraintOperation" />.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="builder">The command builder to use to add the SQL fragment.</param>
protected virtual void KeyWithOptions(MigrationOperation operation, MigrationCommandListBuilder builder)
{
}

/// <summary>
/// Generates a SQL fragment for traits of an index from a <see cref="CreateIndexOperation" />,
/// <see cref="AddPrimaryKeyOperation" />, or <see cref="AddUniqueConstraintOperation" />.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ private static readonly MethodInfo KeyIsClusteredMethodInfo
= typeof(SqlServerKeyBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerKeyBuilderExtensions.IsClustered), [typeof(KeyBuilder), typeof(bool)])!;

private static readonly MethodInfo KeyHasFillFactorMethodInfo
= typeof(SqlServerKeyBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerKeyBuilderExtensions.HasFillFactor), [typeof(KeyBuilder), typeof(int)])!;

private static readonly MethodInfo TableIsTemporalMethodInfo
= typeof(SqlServerTableBuilderExtensions).GetRuntimeMethod(
nameof(SqlServerTableBuilderExtensions.IsTemporal), [typeof(TableBuilder), typeof(bool)])!;
Expand Down Expand Up @@ -328,11 +332,16 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override MethodCallCodeFragment? GenerateFluentApi(IKey key, IAnnotation annotation)
=> annotation.Name == SqlServerAnnotationNames.Clustered
? (bool)annotation.Value! == false
=> annotation.Name switch
{
SqlServerAnnotationNames.Clustered => (bool)annotation.Value! == false
? new MethodCallCodeFragment(KeyIsClusteredMethodInfo, false)
: new MethodCallCodeFragment(KeyIsClusteredMethodInfo)
: null;
: new MethodCallCodeFragment(KeyIsClusteredMethodInfo),

SqlServerAnnotationNames.FillFactor => new MethodCallCodeFragment(KeyHasFillFactorMethodInfo, annotation.Value),

_ => null
};

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public override void Generate(IKey key, CSharpRuntimeAnnotationCodeGeneratorPara
{
var annotations = parameters.Annotations;
annotations.Remove(SqlServerAnnotationNames.Clustered);
annotations.Remove(SqlServerAnnotationNames.FillFactor);
}

base.Generate(key, parameters);
Expand All @@ -144,6 +145,7 @@ public override void Generate(IUniqueConstraint uniqueConstraint, CSharpRuntimeA
{
var annotations = parameters.Annotations;
annotations.Remove(SqlServerAnnotationNames.Clustered);
annotations.Remove(SqlServerAnnotationNames.FillFactor);
}

base.Generate(uniqueConstraint, parameters);
Expand Down
82 changes: 82 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerKeyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,86 @@ public static bool CanSetIsClustered(
bool? clustered,
bool fromDataAnnotation = false)
=> keyBuilder.CanSetAnnotation(SqlServerAnnotationNames.Clustered, clustered, fromDataAnnotation);

/// <summary>
/// Configures whether the key is created with fill factor option when targeting SQL Server.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="fillFactor">A value indicating whether the key is created with fill factor option.</param>
/// <returns>A builder to further configure the key.</returns>
public static KeyBuilder HasFillFactor(this KeyBuilder keyBuilder, int fillFactor)
{
keyBuilder.Metadata.SetFillFactor(fillFactor);

return keyBuilder;
}

/// <summary>
/// Configures whether the key is created with fill factor option when targeting SQL Server.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="fillFactor">A value indicating whether the key is created with fill factor option.</param>
/// <returns>A builder to further configure the key.</returns>
public static KeyBuilder<TEntity> HasFillFactor<TEntity>(
this KeyBuilder<TEntity> keyBuilder,
int fillFactor)
=> (KeyBuilder<TEntity>)HasFillFactor((KeyBuilder)keyBuilder, fillFactor);

/// <summary>
/// Configures whether the key is created with fill factor option when targeting SQL Server.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="fillFactor">A value indicating whether the key is created with fill factor option.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
public static IConventionKeyBuilder? HasFillFactor(
this IConventionKeyBuilder keyBuilder,
int? fillFactor,
bool fromDataAnnotation = false)
{
if (keyBuilder.CanSetFillFactor(fillFactor, fromDataAnnotation))
{
keyBuilder.Metadata.SetFillFactor(fillFactor, fromDataAnnotation);

return keyBuilder;
}

return null;
}

/// <summary>
/// Returns a value indicating whether the key can be configured with fill factor option when targeting SQL Server.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="keyBuilder">The builder for the key being configured.</param>
/// <param name="fillFactor">A value indicating whether the key is created with fill factor option.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the key can be configured with fill factor option when targeting SQL Server.</returns>
public static bool CanSetFillFactor(
this IConventionKeyBuilder keyBuilder,
int? fillFactor,
bool fromDataAnnotation = false)
=> keyBuilder.CanSetAnnotation(SqlServerAnnotationNames.FillFactor, fillFactor, fromDataAnnotation);
}
81 changes: 81 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerKeyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,85 @@ public static void SetIsClustered(this IMutableKey key, bool? clustered)
/// <returns>The <see cref="ConfigurationSource" /> for whether the key is clustered.</returns>
public static ConfigurationSource? GetIsClusteredConfigurationSource(this IConventionKey key)
=> key.FindAnnotation(SqlServerAnnotationNames.Clustered)?.GetConfigurationSource();

/// <summary>
/// Returns the fill factor that the key uses.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The fill factor that the key uses</returns>
public static int? GetFillFactor(this IReadOnlyKey key)
=> (key is RuntimeKey)
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
: (int?)key[SqlServerAnnotationNames.FillFactor];

/// <summary>
/// Returns the fill factor that the key uses.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="storeObject">The identifier of the store object.</param>
/// <returns>The fill factor that the key uses</returns>
public static int? GetFillFactor(this IReadOnlyKey key, in StoreObjectIdentifier storeObject)
{
if (key is RuntimeKey)
{
throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData);
}

var annotation = key.FindAnnotation(SqlServerAnnotationNames.FillFactor);
if (annotation != null)
{
return (int?)annotation.Value;
}

var sharedTableRootKey = key.FindSharedObjectRootKey(storeObject);
return sharedTableRootKey?.GetFillFactor(storeObject);
}

/// <summary>
/// Sets a value for fill factor the key uses.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="fillFactor">The value to set.</param>
public static void SetFillFactor(this IMutableKey key, int? fillFactor)
{
if (fillFactor is <= 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(fillFactor));
}

key.SetAnnotation(
SqlServerAnnotationNames.FillFactor,
fillFactor);
}

/// <summary>
/// Sets a value for fill factor the key uses.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="fillFactor">The value to set.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static int? SetFillFactor(
this IConventionKey key,
int? fillFactor,
bool fromDataAnnotation = false)
{
if (fillFactor is <= 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(fillFactor));
}

return (int?)key.SetAnnotation(
SqlServerAnnotationNames.FillFactor,
fillFactor,
fromDataAnnotation)?.Value;
}

/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for whether the key uses the fill factor.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The <see cref="ConfigurationSource" /> for whether the key uses the fill factor.</returns>
public static ConfigurationSource? GetFillFactorConfigurationSource(this IConventionKey key)
=> key.FindAnnotation(SqlServerAnnotationNames.FillFactor)?.GetConfigurationSource();
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ protected override void ProcessKeyAnnotations(
if (!runtime)
{
annotations.Remove(SqlServerAnnotationNames.Clustered);
annotations.Remove(SqlServerAnnotationNames.FillFactor);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ public override IEnumerable<IAnnotation> For(IUniqueConstraint constraint, bool
{
yield return new Annotation(SqlServerAnnotationNames.Clustered, isClustered);
}

if (key.GetFillFactor() is int fillFactor)
{
yield return new Annotation(SqlServerAnnotationNames.FillFactor, fillFactor);
}
}

/// <summary>
Expand Down
24 changes: 24 additions & 0 deletions src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,30 @@ protected virtual void Transfer(
}
}

/// <summary>
/// Generates a SQL fragment for extra with options of a key from a
/// <see cref="AddPrimaryKeyOperation" />, or <see cref="AddUniqueConstraintOperation" />.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="builder">The command builder to use to add the SQL fragment.</param>
protected override void KeyWithOptions(MigrationOperation operation, MigrationCommandListBuilder builder)
{
var options = new List<string>();

if (operation[SqlServerAnnotationNames.FillFactor] is int fillFactor)
{
options.Add("FILLFACTOR = " + fillFactor);
}

if (options.Count > 0)
{
builder
.Append(" WITH (")
.Append(string.Join(", ", options))
.Append(")");
}
}

/// <summary>
/// Generates a SQL fragment for traits of an index from a <see cref="CreateIndexOperation" />,
/// <see cref="AddPrimaryKeyOperation" />, or <see cref="AddUniqueConstraintOperation" />.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,8 @@ FROM [sys].[indexes] i
.GroupBy(
ddr =>
(Name: ddr.GetFieldValue<string>("index_name"),
TypeDesc: ddr.GetValueOrDefault<string>("type_desc")))
TypeDesc: ddr.GetValueOrDefault<string>("type_desc"),
FillFactor: ddr.GetValueOrDefault<byte>("fill_factor")))
.ToArray();

Check.DebugAssert(primaryKeyGroups.Length is 0 or 1, "Multiple primary keys found");
Expand All @@ -1106,7 +1107,8 @@ FROM [sys].[indexes] i
.GroupBy(
ddr =>
(Name: ddr.GetValueOrDefault<string>("index_name"),
TypeDesc: ddr.GetValueOrDefault<string>("type_desc")))
TypeDesc: ddr.GetValueOrDefault<string>("type_desc"),
FillFactor: ddr.GetValueOrDefault<byte>("fill_factor")))
.ToArray();

foreach (var uniqueConstraintGroup in uniqueConstraintGroups)
Expand Down Expand Up @@ -1142,7 +1144,7 @@ FROM [sys].[indexes] i
}

bool TryGetPrimaryKey(
IGrouping<(string Name, string? TypeDesc), DbDataRecord> primaryKeyGroup,
IGrouping<(string Name, string? TypeDesc, byte FillFactor), DbDataRecord> primaryKeyGroup,
[NotNullWhen(true)] out DatabasePrimaryKey? primaryKey)
{
primaryKey = new DatabasePrimaryKey { Table = table, Name = primaryKeyGroup.Key.Name };
Expand All @@ -1152,6 +1154,11 @@ bool TryGetPrimaryKey(
primaryKey[SqlServerAnnotationNames.Clustered] = false;
}

if (primaryKeyGroup.Key.FillFactor is > 0 and <= 100)
{
primaryKey[SqlServerAnnotationNames.FillFactor] = (int)primaryKeyGroup.Key.FillFactor;
}

foreach (var dataRecord in primaryKeyGroup)
{
var columnName = dataRecord.GetValueOrDefault<string>("column_name");
Expand All @@ -1171,7 +1178,7 @@ bool TryGetPrimaryKey(
}

bool TryGetUniqueConstraint(
IGrouping<(string? Name, string? TypeDesc), DbDataRecord> uniqueConstraintGroup,
IGrouping<(string? Name, string? TypeDesc, byte FillFactor), DbDataRecord> uniqueConstraintGroup,
[NotNullWhen(true)] out DatabaseUniqueConstraint? uniqueConstraint)
{
uniqueConstraint = new DatabaseUniqueConstraint { Table = table, Name = uniqueConstraintGroup.Key.Name };
Expand All @@ -1181,6 +1188,11 @@ bool TryGetUniqueConstraint(
uniqueConstraint[SqlServerAnnotationNames.Clustered] = true;
}

if (uniqueConstraintGroup.Key.FillFactor is > 0 and <= 100)
{
uniqueConstraint[SqlServerAnnotationNames.FillFactor] = (int)uniqueConstraintGroup.Key.FillFactor;
}

foreach (var dataRecord in uniqueConstraintGroup)
{
var columnName = dataRecord.GetValueOrDefault<string>("column_name");
Expand Down
Loading

0 comments on commit fb2dbcf

Please sign in to comment.