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

SQLServer: Support fill factor for index #20634

Merged
merged 4 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public override MethodCallCodeFragment GenerateFluentApi(IIndex index, IAnnotati
return new MethodCallCodeFragment(nameof(SqlServerIndexBuilderExtensions.IncludeProperties), annotation.Value);
}

if (annotation.Name == SqlServerAnnotationNames.FillFactor)
{
return new MethodCallCodeFragment(nameof(SqlServerIndexBuilderExtensions.HasFillFactor), annotation.Value);
}

return null;
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,76 @@ public static bool CanSetIsCreatedOnline(

return indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.CreatedOnline, createdOnline, fromDataAnnotation);
}

/// <summary>
/// Configures whether the index is created with fill factor option when targeting SQL Server.
/// </summary>
/// <param name="indexBuilder"> The builder for the index being configured. </param>
/// <param name="fillFactor"> A value indicating whether the index is created with fill factor option. </param>
/// <returns> A builder to further configure the index. </returns>
public static IndexBuilder HasFillFactor([NotNull] this IndexBuilder indexBuilder, int fillFactor)
{
Check.NotNull(indexBuilder, nameof(indexBuilder));

indexBuilder.Metadata.SetFillFactor(fillFactor);

return indexBuilder;
}

/// <summary>
/// Configures whether the index is created with fill factor option when targeting SQL Server.
/// </summary>
/// <param name="indexBuilder"> The builder for the index being configured. </param>
/// <param name="fillFactor"> A value indicating whether the index is created with fill factor option. </param>
/// <returns> A builder to further configure the index. </returns>
public static IndexBuilder<TEntity> HasFillFactor<TEntity>(
[NotNull] this IndexBuilder<TEntity> indexBuilder, int fillFactor)
=> (IndexBuilder<TEntity>)HasFillFactor((IndexBuilder)indexBuilder, fillFactor);

/// <summary>
/// Configures whether the index is created with fill factor option when targeting SQL Server.
/// </summary>
/// <param name="indexBuilder"> The builder for the index being configured. </param>
/// <param name="fillFactor"> A value indicating whether the index 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,
/// <c>null</c> otherwise.
/// </returns>
public static IConventionIndexBuilder HasFillFactor(
[NotNull] this IConventionIndexBuilder indexBuilder,
int? fillFactor,
bool fromDataAnnotation = false)
{
if (indexBuilder.CanSetFillFactor(fillFactor, fromDataAnnotation))
{
indexBuilder.Metadata.SetFillFactor(fillFactor, fromDataAnnotation);

return indexBuilder;
}

return null;
}

/// <summary>
/// Returns a value indicating whether the index can be configured with fill factor option when targeting SQL Server.
/// </summary>
/// <param name="indexBuilder"> The builder for the index being configured. </param>
/// <param name="fillFactor"> A value indicating whether the index 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,
/// <c>null</c> otherwise.
/// </returns>
/// <returns> <c>true</c> if the index can be configured with fill factor option when targeting SQL Server. </returns>
lajones marked this conversation as resolved.
Show resolved Hide resolved
public static bool CanSetFillFactor(
[NotNull] this IConventionIndexBuilder indexBuilder,
int? fillFactor,
bool fromDataAnnotation = false)
{
Check.NotNull(indexBuilder, nameof(indexBuilder));

return indexBuilder.CanSetAnnotation(SqlServerAnnotationNames.FillFactor, fillFactor, fromDataAnnotation);
}
}
}
59 changes: 59 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerIndexExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
Expand Down Expand Up @@ -145,5 +146,63 @@ public static void SetIsCreatedOnline([NotNull] this IMutableIndex index, bool?
/// <returns> The <see cref="ConfigurationSource" /> for whether the index is online. </returns>
public static ConfigurationSource? GetIsCreatedOnlineConfigurationSource([NotNull] this IConventionIndex index)
=> index.FindAnnotation(SqlServerAnnotationNames.CreatedOnline)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether the index uses the fill factor.
/// </summary>
/// <param name="index"> The index. </param>
/// <returns> <c>true</c> if the index is online. </returns>
public static int? GetFillFactor([NotNull] this IIndex index)
=> (int?)index[SqlServerAnnotationNames.FillFactor];

/// <summary>
/// Sets a value indicating whether the index uses the fill factor.
/// </summary>
/// <param name="index"> The index. </param>
/// <param name="fillFactor"> The value to set. </param>
public static void SetFillFactor([NotNull] this IMutableIndex index, int? fillFactor)
{
if (fillFactor != null && (fillFactor <= 0 || fillFactor > 100))
{
throw new ArgumentOutOfRangeException(nameof(fillFactor));
}

index.SetOrRemoveAnnotation(
SqlServerAnnotationNames.FillFactor,
fillFactor);
}

/// <summary>
/// Defines a value indicating whether the index uses the fill factor.
/// </summary>
/// <param name="index"> The index. </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(
[NotNull] this IConventionIndex index,
int? fillFactor,
bool fromDataAnnotation = false)
{
if (fillFactor != null && (fillFactor <= 0 || fillFactor > 100))
{
throw new ArgumentOutOfRangeException(nameof(fillFactor));
}

index.SetOrRemoveAnnotation(
lajones marked this conversation as resolved.
Show resolved Hide resolved
SqlServerAnnotationNames.FillFactor,
fillFactor,
fromDataAnnotation);

return fillFactor;
}

/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for whether the index uses the fill factor.
/// </summary>
/// <param name="index"> The index. </param>
/// <returns> The <see cref="ConfigurationSource" /> for whether the index uses the fill factor. </returns>
public static ConfigurationSource? GetFillFactorConfigurationSource([NotNull] this IConventionIndex index)
=> index.FindAnnotation(SqlServerAnnotationNames.FillFactor)?.GetConfigurationSource();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,13 @@ public static class SqlServerAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string PerformanceLevelSql = Prefix + "PerformanceLevelSql";

/// <summary>
/// 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.
/// </summary>
public const string FillFactor = Prefix + "FillFactor";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ public override IEnumerable<IAnnotation> For(ITableIndex index)
SqlServerAnnotationNames.CreatedOnline,
isOnline.Value);
}

var fillFactor = modelIndex.GetFillFactor();
if (fillFactor.HasValue)
{
yield return new Annotation(
SqlServerAnnotationNames.FillFactor,
fillFactor.Value);
}
}

/// <summary>
Expand Down
17 changes: 17 additions & 0 deletions src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ public static bool AreCompatibleForSqlServer([NotNull] this IIndex index, [NotNu
return false;
}

if (index.GetFillFactor() != duplicateIndex.GetFillFactor())
{
if (shouldThrow)
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateIndexFillFactorMismatch(
index.Properties.Format(),
index.DeclaringEntityType.DisplayName(),
duplicateIndex.Properties.Format(),
duplicateIndex.DeclaringEntityType.DisplayName(),
index.DeclaringEntityType.GetSchemaQualifiedTableName(),
index.GetName()));
}

return false;
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1552,9 +1552,29 @@ protected override void IndexOptions(CreateIndexOperation operation, IModel mode

base.IndexOptions(operation, model, builder);

IndexWithOptions(operation, builder);
}

private void IndexWithOptions(CreateIndexOperation operation, MigrationCommandListBuilder builder)
{
var options = new List<string>();

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

if (operation[SqlServerAnnotationNames.CreatedOnline] is bool isOnline && isOnline)
{
builder.Append(" WITH (ONLINE = ON)");
options.Add("ONLINE = ON");
}

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

Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,7 @@
<data name="DuplicateIndexOnlineMismatch" xml:space="preserve">
<value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}' but with different online configuration.</value>
</data>
<data name="DuplicateIndexFillFactorMismatch" xml:space="preserve">
<value>The indexes {index1} on '{entityType1}' and {index2} on '{entityType2}' are both mapped to '{table}.{indexName}' but with different fill factor configuration.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ private void GetIndexes(DbConnection connection, IReadOnlyList<DatabaseTable> ta
[i].[is_unique],
[i].[has_filter],
[i].[filter_definition],
[i].[fill_factor],
COL_NAME([ic].[object_id], [ic].[column_id]) AS [column_name]
FROM [sys].[indexes] AS [i]
JOIN [sys].[tables] AS [t] ON [i].[object_id] = [t].[object_id]
Expand Down Expand Up @@ -965,7 +966,8 @@ FROM [sys].[indexes] i
TypeDesc: ddr.GetValueOrDefault<string>("type_desc"),
IsUnique: ddr.GetValueOrDefault<bool>("is_unique"),
HasFilter: ddr.GetValueOrDefault<bool>("has_filter"),
FilterDefinition: ddr.GetValueOrDefault<string>("filter_definition")))
FilterDefinition: ddr.GetValueOrDefault<string>("filter_definition"),
FillFactor: ddr.GetValueOrDefault<byte>("fill_factor")))
.ToArray();

foreach (var indexGroup in indexGroups)
Expand All @@ -983,6 +985,11 @@ FROM [sys].[indexes] i
index[SqlServerAnnotationNames.Clustered] = true;
}

if (indexGroup.Key.FillFactor > 0 && indexGroup.Key.FillFactor <= 100)
{
index[SqlServerAnnotationNames.FillFactor] = indexGroup.Key.FillFactor;
}

foreach (var dataRecord in indexGroup)
{
var columnName = dataRecord.GetValueOrDefault<string>("column_name");
Expand Down
90 changes: 90 additions & 0 deletions test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,96 @@ FROM [sys].[default_constraints] [d]
@"CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) INCLUDE ([FirstName], [LastName]) WHERE [Name] IS NOT NULL WITH (ONLINE = ON);");
}

[ConditionalFact(Skip = "#19668, Online index operations can only be performed in Enterprise edition of SQL Server")]
[SqlServerCondition(SqlServerCondition.SupportsOnlineIndexes)]
public virtual async Task Create_index_unique_with_include_filter_online_and_fillfactor()
{
await Test(
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("FirstName");
e.Property<string>("LastName");
e.Property<string>("Name").IsRequired();
}),
builder => { },
builder => builder.Entity("People").HasIndex("Name")
.IsUnique()
.IncludeProperties("FirstName", "LastName")
.HasFilter("[Name] IS NOT NULL")
.IsCreatedOnline()
.HasFillFactor(90),
model =>
{
var table = Assert.Single(model.Tables);
var index = Assert.Single(table.Indexes);
Assert.True(index.IsUnique);
Assert.Equal("([Name] IS NOT NULL)", index.Filter);
// TODO: This is a scaffolding bug, #17083
Assert.Equal(3, index.Columns.Count);
Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns);
Assert.Contains(table.Columns.Single(c => c.Name == "FirstName"), index.Columns);
Assert.Contains(table.Columns.Single(c => c.Name == "LastName"), index.Columns);
// TODO: Online index not scaffolded?
});

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]) WHERE [Name] IS NOT NULL WITH (FILLFACTOR = 90, ONLINE = ON);");
}

[ConditionalFact]
public virtual async Task Create_index_unique_with_include_filter_and_fillfactor()
{
await Test(
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("FirstName");
e.Property<string>("LastName");
e.Property<string>("Name").IsRequired();
}),
builder => { },
builder => builder.Entity("People").HasIndex("Name")
.IsUnique()
.IncludeProperties("FirstName", "LastName")
.HasFilter("[Name] IS NOT NULL")
.HasFillFactor(90),
model =>
{
var table = Assert.Single(model.Tables);
var index = Assert.Single(table.Indexes);
Assert.True(index.IsUnique);
Assert.Equal("([Name] IS NOT NULL)", index.Filter);
// TODO: This is a scaffolding bug, #17083
Assert.Equal(3, index.Columns.Count);
Assert.Contains(table.Columns.Single(c => c.Name == "Name"), index.Columns);
Assert.Contains(table.Columns.Single(c => c.Name == "FirstName"), index.Columns);
Assert.Contains(table.Columns.Single(c => c.Name == "LastName"), index.Columns);
// TODO: Online index not scaffolded?
});

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]) WHERE [Name] IS NOT NULL WITH (FILLFACTOR = 90);");
}

[ConditionalFact]
[SqlServerCondition(SqlServerCondition.SupportsMemoryOptimized)]
public virtual async Task Create_index_memoryOptimized_unique_nullable()
Expand Down
Loading