Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fill factor to keys and unique constraints #32900

Merged
merged 2 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1671,6 +1678,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -1741,6 +1741,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 @@ -1081,7 +1081,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 @@ -1100,7 +1101,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 @@ -1136,7 +1138,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 @@ -1146,6 +1148,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 @@ -1165,7 +1172,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 @@ -1175,6 +1182,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
Loading