From 2e0498a15bc1ae8afa7868037645802f62432561 Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Wed, 26 Oct 2016 18:34:52 +0300 Subject: [PATCH] feat(Migrations): SQL Server Filterable indexes. Fixed #5817 --- .../CSharpMigrationOperationGenerator.cs | 8 ++++ .../Design/CSharpSnapshotGenerator.cs | 11 ++++- .../Internal/IndexConfiguration.cs | 10 ++-- .../Metadata/IndexModel.cs | 1 + .../RelationalScaffoldingModelFactory.cs | 8 +++- .../Metadata/IRelationalIndexAnnotations.cs | 2 + .../Internal/RelationalAnnotationNames.cs | 6 +++ .../Internal/RelationalFullAnnotationNames.cs | 7 +++ .../RelationalIndexBuilderAnnotations.cs | 6 +++ .../Metadata/RelationalIndexAnnotations.cs | 15 ++++++ .../Internal/MigrationsModelDiffer.cs | 5 +- .../Migrations/MigrationBuilder.cs | 12 +++-- .../Migrations/MigrationsSqlGenerator.cs | 7 +++ .../Operations/CreateIndexOperation.cs | 1 + .../RelationalIndexBuilderExtensions.cs | 15 ++++++ .../Internal/SqlServerDatabaseModelFactory.cs | 9 +++- .../SqlServerIndexBuilderAnnotations.cs | 6 +++ .../SqlServerMigrationsSqlGenerator.cs | 3 +- .../RelationalBuilderExtensionsTest.cs | 17 +++++++ .../RelationalMetadataExtensionsTest.cs | 18 +++++++ .../Migrations/MigrationSqlGeneratorTest.cs | 9 ++++ .../MigrationSqlGeneratorTestBase.cs | 37 ++++++++++++++ ...re.SqlServer.Design.FunctionalTests.csproj | 6 +++ .../Expected/FilteredIndex.expected | 11 +++++ .../Expected/FilteredIndexContext.expected | 29 +++++++++++ .../ReverseEngineering/SqlServerE2ETests.cs | 48 +++++++++++++++++++ .../SqlServerMigrationSqlGeneratorTest.cs | 20 ++++++++ 27 files changed, 313 insertions(+), 14 deletions(-) create mode 100644 test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndex.expected create mode 100644 test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndexContext.expected diff --git a/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs index 42c3349cdcc..d2c8b7aa6c1 100644 --- a/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs @@ -623,6 +623,14 @@ protected virtual void Generate([NotNull] CreateIndexOperation operation, [NotNu .Append("unique: true"); } + if (operation.Filter != null) + { + builder + .AppendLine(",") + .Append("filter: ") + .Append(operation.Filter); + } + builder.Append(")"); Annotations(operation.GetAnnotations(), builder); diff --git a/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index a599ff9eb1c..d5c5c2825b5 100644 --- a/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -331,7 +331,7 @@ protected virtual void GenerateIndex( stringBuilder .AppendLine() - .Append("b.HasIndex(") + .Append($"b.{nameof(EntityTypeBuilder.HasIndex)}(") .Append(string.Join(", ", index.Properties.Select(p => _code.Literal(p.Name)))) .Append(")"); @@ -341,7 +341,14 @@ protected virtual void GenerateIndex( { stringBuilder .AppendLine() - .Append(".IsUnique()"); + .Append($".{nameof(IndexBuilder.IsUnique)}()"); + } + + if (index.Relational().Filter != null) + { + stringBuilder + .AppendLine() + .Append($".{nameof(RelationalIndexBuilderExtensions.HasFilter)}({index.Relational().Filter})"); } var annotations = index.GetAnnotations().ToList(); diff --git a/src/Microsoft.EntityFrameworkCore.Design/Scaffolding/Configuration/Internal/IndexConfiguration.cs b/src/Microsoft.EntityFrameworkCore.Design/Scaffolding/Configuration/Internal/IndexConfiguration.cs index 021a4b26107..86b6f8aee2e 100644 --- a/src/Microsoft.EntityFrameworkCore.Design/Scaffolding/Configuration/Internal/IndexConfiguration.cs +++ b/src/Microsoft.EntityFrameworkCore.Design/Scaffolding/Configuration/Internal/IndexConfiguration.cs @@ -65,13 +65,17 @@ public virtual ICollection FluentApiLines if (!string.IsNullOrEmpty(Index.Relational().Name)) { - lines.Add("." + nameof(RelationalIndexBuilderExtensions.HasName) + "(" - + CSharpUtilities.Instance.DelimitString(Index.Relational().Name) + ")"); + lines.Add($".{nameof(RelationalIndexBuilderExtensions.HasName)}({CSharpUtilities.Instance.DelimitString(Index.Relational().Name)})"); } if (Index.IsUnique) { - lines.Add("." + nameof(IndexBuilder.IsUnique) + "()"); + lines.Add($".{nameof(IndexBuilder.IsUnique)}()"); + } + + if (Index.Relational().Filter != null) + { + lines.Add($".{nameof(RelationalIndexBuilderExtensions.HasFilter)}(\"{Index.Relational().Filter}\")"); } return lines; diff --git a/src/Microsoft.EntityFrameworkCore.Relational.Design/Metadata/IndexModel.cs b/src/Microsoft.EntityFrameworkCore.Relational.Design/Metadata/IndexModel.cs index 3ed0eb6b032..67399db7d42 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational.Design/Metadata/IndexModel.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational.Design/Metadata/IndexModel.cs @@ -14,5 +14,6 @@ public class IndexModel : Annotatable public virtual string Name { get; [param: NotNull] set; } public virtual ICollection IndexColumns { get; [param: NotNull] set; } = new List(); public virtual bool IsUnique { get; [param: NotNull] set; } + public virtual string Filter { get; [param: CanBeNull] set; } } } diff --git a/src/Microsoft.EntityFrameworkCore.Relational.Design/RelationalScaffoldingModelFactory.cs b/src/Microsoft.EntityFrameworkCore.Relational.Design/RelationalScaffoldingModelFactory.cs index 2c5efff9a32..bd79d39d510 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational.Design/RelationalScaffoldingModelFactory.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational.Design/RelationalScaffoldingModelFactory.cs @@ -18,6 +18,7 @@ using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore.Extensions; namespace Microsoft.EntityFrameworkCore.Scaffolding { @@ -423,7 +424,7 @@ protected virtual IndexBuilder VisitIndex([NotNull] EntityTypeBuilder builder, [ var primaryKeyColumns = index.Table.Columns .Where(c => c.PrimaryKeyOrdinal.HasValue) .OrderBy(c => c.PrimaryKeyOrdinal); - if (columnNames.SequenceEqual(primaryKeyColumns.Select(c => c.Name))) + if (columnNames.SequenceEqual(primaryKeyColumns.Select(c => c.Name)) && index.Filter == null) { // index is supporting the primary key. So there is no need for // an extra index in the model. But if the index name does not @@ -445,6 +446,11 @@ protected virtual IndexBuilder VisitIndex([NotNull] EntityTypeBuilder builder, [ var indexBuilder = builder.HasIndex(propertyNames) .IsUnique(index.IsUnique); + if (index.Filter != null) + { + indexBuilder.HasFilter(index.Filter); + } + if (!string.IsNullOrEmpty(index.Name)) { indexBuilder.HasName(index.Name); diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/IRelationalIndexAnnotations.cs b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/IRelationalIndexAnnotations.cs index 91195d5317c..b42a1e29927 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/IRelationalIndexAnnotations.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/IRelationalIndexAnnotations.cs @@ -6,5 +6,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface IRelationalIndexAnnotations { string Name { get; } + + string Filter { get; } } } diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalAnnotationNames.cs b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalAnnotationNames.cs index 6ac8c8323b3..348ac2f0ad4 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalAnnotationNames.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalAnnotationNames.cs @@ -92,5 +92,11 @@ public static class RelationalAnnotationNames /// directly from your code. This API may change or be removed in future releases. /// public const string DiscriminatorValue = "DiscriminatorValue"; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public const string Filter = "Filter"; } } diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalFullAnnotationNames.cs b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalFullAnnotationNames.cs index e552bbbfcd6..d7309b0cf67 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalFullAnnotationNames.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalFullAnnotationNames.cs @@ -28,6 +28,7 @@ protected RelationalFullAnnotationNames(string prefix) SequencePrefix = prefix + RelationalAnnotationNames.SequencePrefix; DiscriminatorProperty = prefix + RelationalAnnotationNames.DiscriminatorProperty; DiscriminatorValue = prefix + RelationalAnnotationNames.DiscriminatorValue; + Filter = prefix + RelationalAnnotationNames.Filter; } /// @@ -96,6 +97,12 @@ protected RelationalFullAnnotationNames(string prefix) /// public readonly string Name; + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public readonly string Filter; + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalIndexBuilderAnnotations.cs b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalIndexBuilderAnnotations.cs index 15818f507a9..68b6233a2ca 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalIndexBuilderAnnotations.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/Internal/RelationalIndexBuilderAnnotations.cs @@ -28,5 +28,11 @@ public RelationalIndexBuilderAnnotations( /// directly from your code. This API may change or be removed in future releases. /// public virtual bool HasName([CanBeNull] string value) => SetName(value); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool HasFilter([CanBeNull] string value) => SetFilter(value); } } diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/RelationalIndexAnnotations.cs b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/RelationalIndexAnnotations.cs index e283e1cc65f..64c0437cfd0 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Metadata/RelationalIndexAnnotations.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Metadata/RelationalIndexAnnotations.cs @@ -48,6 +48,21 @@ public virtual string Name [param: CanBeNull] set { SetName(value); } } + public virtual string Filter + { + get + { + return (string)Annotations.GetAnnotation(RelationalFullAnnotationNames.Instance.Filter, ProviderFullAnnotationNames?.Filter); + } + [param: CanBeNull] set { SetFilter(value); } + } + + protected virtual bool SetFilter([CanBeNull] string value) + => Annotations.SetAnnotation( + RelationalFullAnnotationNames.Instance.Filter, + ProviderFullAnnotationNames?.Filter, + Check.NullButNotEmpty(value, nameof(value))); + protected virtual bool SetName([CanBeNull] string value) => Annotations.SetAnnotation( RelationalFullAnnotationNames.Instance.Name, diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 6ddc6926198..e7c5c751526 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -973,10 +973,12 @@ protected virtual IEnumerable Diff( Annotations.For(t).Name, StringComparison.OrdinalIgnoreCase) && s.IsUnique == t.IsUnique + && Annotations.For(s).Filter == Annotations.For(t).Filter && !HasDifferences(MigrationsAnnotations.For(s), MigrationsAnnotations.For(t)) && s.Properties.Select(diffContext.FindTarget).SequenceEqual(t.Properties), // ReSharper disable once ImplicitlyCapturedClosure (s, t) => s.IsUnique == t.IsUnique + && Annotations.For(s).Filter == Annotations.For(t).Filter && !HasDifferences(MigrationsAnnotations.For(s), MigrationsAnnotations.For(t)) && s.Properties.Select(diffContext.FindTarget).SequenceEqual(t.Properties)); @@ -1022,7 +1024,8 @@ protected virtual IEnumerable Add( Schema = targetEntityTypeAnnotations.Schema, Table = targetEntityTypeAnnotations.TableName, Columns = GetColumns(target.Properties), - IsUnique = target.IsUnique + IsUnique = target.IsUnique, + Filter = Annotations.For(target).Filter }; operation.AddAnnotations(MigrationsAnnotations.For(target)); diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationBuilder.cs b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationBuilder.cs index f623bae8482..e85f78bf730 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationBuilder.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationBuilder.cs @@ -300,20 +300,23 @@ public virtual OperationBuilder CreateIndex( [NotNull] string table, [NotNull] string column, [CanBeNull] string schema = null, - bool unique = false) + bool unique = false, + [CanBeNull] string filter = null) => CreateIndex( name, table, new[] { column }, schema, - unique); + unique, + filter); public virtual OperationBuilder CreateIndex( [NotNull] string name, [NotNull] string table, [NotNull] string[] columns, [CanBeNull] string schema = null, - bool unique = false) + bool unique = false, + [CanBeNull] string filter = null) { Check.NotEmpty(name, nameof(name)); Check.NotEmpty(table, nameof(table)); @@ -325,7 +328,8 @@ public virtual OperationBuilder CreateIndex( Table = table, Name = name, Columns = columns, - IsUnique = unique + IsUnique = unique, + Filter = filter }; Operations.Add(operation); diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationsSqlGenerator.cs index 97b2e24a8e4..c003887b388 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationsSqlGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/MigrationsSqlGenerator.cs @@ -298,6 +298,13 @@ protected virtual void Generate( .Append(ColumnList(operation.Columns)) .Append(")"); + if (operation.Filter != null) + { + builder + .Append(" WHERE ") + .Append(operation.Filter); + } + if (terminate) { builder.AppendLine(SqlGenerationHelper.StatementTerminator); diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Operations/CreateIndexOperation.cs b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Operations/CreateIndexOperation.cs index 5bc0fb44cb3..a5c79942dbf 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Operations/CreateIndexOperation.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Migrations/Operations/CreateIndexOperation.cs @@ -12,5 +12,6 @@ public class CreateIndexOperation : MigrationOperation public virtual string Schema { get; [param: CanBeNull] set; } public virtual string Table { get; [param: NotNull] set; } public virtual string[] Columns { get; [param: NotNull] set; } + public virtual string Filter { get; [param: NotNull] set; } } } diff --git a/src/Microsoft.EntityFrameworkCore.Relational/RelationalIndexBuilderExtensions.cs b/src/Microsoft.EntityFrameworkCore.Relational/RelationalIndexBuilderExtensions.cs index 2ec7e791ef1..1cad5e5244e 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/RelationalIndexBuilderExtensions.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/RelationalIndexBuilderExtensions.cs @@ -30,5 +30,20 @@ public static IndexBuilder HasName([NotNull] this IndexBuilder indexBuilder, [Ca return indexBuilder; } + + /// + /// Determines whether the specified index has filter expression. + /// + /// The builder for the index being configured. + /// The filter expression for the index. + /// A builder to further configure the index. + public static IndexBuilder HasFilter([NotNull] this IndexBuilder indexBuilder, [NotNull] string filterExpression) + { + Check.NotEmpty(filterExpression, nameof(filterExpression)); + + indexBuilder.Metadata.Relational().Filter = filterExpression; + + return indexBuilder; + } } } diff --git a/src/Microsoft.EntityFrameworkCore.SqlServer.Design/Internal/SqlServerDatabaseModelFactory.cs b/src/Microsoft.EntityFrameworkCore.SqlServer.Design/Internal/SqlServerDatabaseModelFactory.cs index 4be2cac31a0..0b7ac43a7ef 100644 --- a/src/Microsoft.EntityFrameworkCore.SqlServer.Design/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/Microsoft.EntityFrameworkCore.SqlServer.Design/Internal/SqlServerDatabaseModelFactory.cs @@ -449,7 +449,9 @@ private void GetIndexes() i.is_unique, c.name AS [column_name], i.type_desc, - ic.key_ordinal + ic.key_ordinal, + i.has_filter, + i.filter_definition FROM sys.indexes i INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND c.column_id = ic.column_id @@ -471,6 +473,8 @@ AND object_name(i.object_id) <> '" + HistoryRepository.DefaultTableName + @"'" + var typeDesc = reader.GetValueOrDefault("type_desc"); var columnName = reader.GetValueOrDefault("column_name"); var indexOrdinal = reader.GetValueOrDefault("key_ordinal"); + var hasFilter = reader.GetValueOrDefault("has_filter"); + var filterDefinition = reader.GetValueOrDefault("filter_definition"); Logger.LogDebug( RelationalDesignEventId.FoundIndexColumn, @@ -513,7 +517,8 @@ AND object_name(i.object_id) <> '" + HistoryRepository.DefaultTableName + @"'" + { Table = table, Name = indexName, - IsUnique = isUnique + IsUnique = isUnique, + Filter = hasFilter ? filterDefinition : null }; index.SqlServer().IsClustered = typeDesc == "CLUSTERED"; diff --git a/src/Microsoft.EntityFrameworkCore.SqlServer/Metadata/Internal/SqlServerIndexBuilderAnnotations.cs b/src/Microsoft.EntityFrameworkCore.SqlServer/Metadata/Internal/SqlServerIndexBuilderAnnotations.cs index c2690011444..e35c8cdf939 100644 --- a/src/Microsoft.EntityFrameworkCore.SqlServer/Metadata/Internal/SqlServerIndexBuilderAnnotations.cs +++ b/src/Microsoft.EntityFrameworkCore.SqlServer/Metadata/Internal/SqlServerIndexBuilderAnnotations.cs @@ -29,6 +29,12 @@ public SqlServerIndexBuilderAnnotations( /// public new virtual bool Name([CanBeNull] string value) => SetName(value); + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public new virtual bool HasFilter([CanBeNull] string value) => SetFilter(value); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/Microsoft.EntityFrameworkCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 91e2dfbaf93..a5fc2652607 100644 --- a/src/Microsoft.EntityFrameworkCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/Microsoft.EntityFrameworkCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1035,7 +1035,8 @@ protected virtual void CreateIndexes( Name = Annotations.For(index).Name, Schema = Annotations.For(index.DeclaringEntityType).Schema, Table = Annotations.For(index.DeclaringEntityType).TableName, - Columns = index.Properties.Select(p => Annotations.For(p).ColumnName).ToArray() + Columns = index.Properties.Select(p => Annotations.For(p).ColumnName).ToArray(), + Filter = Annotations.For(index).Filter }; operation.AddAnnotations(_migrationsAnnotations.For(index)); diff --git a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 0e1b7526744..b2cc360fb27 100644 --- a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -13,6 +13,23 @@ namespace Microsoft.EntityFrameworkCore.Relational.Tests.Metadata { public class RelationalBuilderExtensionsTest { + [Fact] + public void Can_write_index_builder_extension_with_where_clauses() + { + var builder = CreateConventionModelBuilder(); + + var returnedBuilder = builder + .Entity() + .HasIndex(e => e.Id) + .HasFilter("[Id] % 2 = 0"); + + Assert.IsType(returnedBuilder); + + var model = builder.Model; + var index = model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + Assert.Equal("[Id] % 2 = 0", index.Relational().Filter); + } + [Fact] public void Can_set_column_name() { diff --git a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index d118f7d99be..47340657d1c 100644 --- a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -12,6 +12,24 @@ namespace Microsoft.EntityFrameworkCore.Relational.Tests.Metadata { public class RelationalMetadataExtensionsTest { + [Fact] + public void Can_get_and_set_index_filter() + { + var modelBuilder = new ModelBuilder(new ConventionSet()); + + var property = modelBuilder + .Entity() + .HasIndex(e => e.Id) + .HasFilter("[Id] % 2 = 0") + .Metadata; + + Assert.Equal("[Id] % 2 = 0", property.Relational().Filter); + + property.Relational().Filter = "[Id] % 3 = 0"; + + Assert.Equal("[Id] % 3 = 0", property.Relational().Filter); + } + [Fact] public void Can_get_and_set_column_name() { diff --git a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs index d96c76975ea..b2804fcf4e5 100644 --- a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs +++ b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTest.cs @@ -196,6 +196,15 @@ public override void CreateIndexOperation_nonunique() Sql); } + public override void CreateIndexOperation_with_where_clauses() + { + base.CreateIndexOperation_with_where_clauses(); + + Assert.Equal( + "CREATE INDEX \"IX_People_Name\" ON \"People\" (\"Name\") WHERE [Id] > 2;" + EOL, + Sql); + } + public override void CreateSequenceOperation_with_minValue_and_maxValue() { base.CreateSequenceOperation_with_minValue_and_maxValue(); diff --git a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTestBase.cs b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTestBase.cs index 9b4d8686b36..aedd04f8380 100644 --- a/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTestBase.cs +++ b/test/Microsoft.EntityFrameworkCore.Relational.Tests/Migrations/MigrationSqlGeneratorTestBase.cs @@ -18,6 +18,31 @@ public abstract class MigrationSqlGeneratorTestBase protected virtual string Sql { get; set; } + [Fact] + public virtual void CreateIndexOperation_with_filter_where_clause() + => Generate( + modelBuilder => modelBuilder.Entity("People").Property("Name").IsRequired(), + new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Columns = new[] { "Name" }, + Filter = "[Name] IS NOT NULL" + }); + + [Fact] + public virtual void CreateIndexOperation_with_filter_where_clause_and_is_unique() + => Generate( + modelBuilder => modelBuilder.Entity("People").Property("Name").IsRequired(), + new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Columns = new[] { "Name" }, + IsUnique = true, + Filter = "[Name] IS NOT NULL AND <> ''" + }); + [Fact] public virtual void AddColumnOperation_with_defaultValue() => Generate( @@ -355,6 +380,18 @@ public virtual void CreateIndexOperation_nonunique() IsUnique = false }); + [Fact] + public virtual void CreateIndexOperation_with_where_clauses() + => Generate( + new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Columns = new[] { "Name" }, + IsUnique = false, + Filter = "[Id] > 2" + }); + [Fact] public virtual void CreateSequenceOperation_with_minValue_and_maxValue() => Generate( diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests.csproj b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests.csproj index e9b21006ef1..1e3927f57eb 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests.csproj +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests.csproj @@ -142,6 +142,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndex.expected b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndex.expected new file mode 100644 index 00000000000..3e4b2760389 --- /dev/null +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndex.expected @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace E2ETest.Namespace +{ + public partial class FilteredIndex + { + public int Id { get; set; } + public int? Number { get; set; } + } +} diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndexContext.expected b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndexContext.expected new file mode 100644 index 00000000000..62aac485174 --- /dev/null +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/Expected/FilteredIndexContext.expected @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace E2ETest.Namespace +{ + public partial class FilteredIndexContext : DbContext + { + public virtual DbSet FilteredIndex { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + #warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings. + optionsBuilder.UseSqlServer(@"{{connectionString}}"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Number) + .HasName("Unicorn_Filtered_Index") + .HasFilter("([Number]>(10))"); + + entity.Property(e => e.Id).ValueGeneratedNever(); + }); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/SqlServerE2ETests.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/SqlServerE2ETests.cs index 2951bda9498..1aa86988f06 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/SqlServerE2ETests.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Design.FunctionalTests/ReverseEngineering/SqlServerE2ETests.cs @@ -302,6 +302,54 @@ PRIMARY KEY (PrimaryKeyWithSequenceId) } } + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.SupportsSequences)] + public void Index_with_filter() + { + using (var scratch = SqlServerTestStore.Create("SqlServerE2E")) + { + scratch.ExecuteNonQuery(@" +CREATE TABLE FilteredIndex ( + Id int, + Number int, + PRIMARY KEY (Id) +); + +CREATE INDEX Unicorn_Filtered_Index + ON FilteredIndex (Number) WHERE Number > 10 +"); + + var configuration = new ReverseEngineeringConfiguration + { + ConnectionString = scratch.ConnectionString, + ProjectPath = TestProjectDir + Path.DirectorySeparatorChar, + ProjectRootNamespace = TestNamespace, + ContextClassName = "FilteredIndexContext", + UseFluentApiOnly = true + }; + var expectedFileSet = new FileSet(new FileSystemFileService(), + Path.Combine("ReverseEngineering", "Expected"), + contents => contents.Replace("{{connectionString}}", scratch.ConnectionString)) + { + Files = new List + { + "FilteredIndexContext.expected", + "FilteredIndex.expected", + } + }; + + var filePaths = Generator.GenerateAsync(configuration).GetAwaiter().GetResult(); + + var actualFileSet = new FileSet(InMemoryFiles, Path.GetFullPath(TestProjectDir)) + { + Files = new[] { filePaths.ContextFile }.Concat(filePaths.EntityTypeFiles).Select(Path.GetFileName).ToList() + }; + + AssertEqualFileContents(expectedFileSet, actualFileSet); + AssertCompile(actualFileSet); + } + } + protected override ICollection References { get; } = new List { #if NETCOREAPP1_1 diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/SqlServerMigrationSqlGeneratorTest.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/SqlServerMigrationSqlGeneratorTest.cs index 0e2b15f2a3a..1d484b41325 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/SqlServerMigrationSqlGeneratorTest.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.Tests/Migrations/SqlServerMigrationSqlGeneratorTest.cs @@ -37,6 +37,26 @@ protected override IMigrationsSqlGenerator SqlGenerator } } + [Fact] + public override void CreateIndexOperation_with_filter_where_clause() + { + base.CreateIndexOperation_with_filter_where_clause(); + + Assert.Equal( + "CREATE INDEX [IX_People_Name] ON [People] ([Name]) WHERE [Name] IS NOT NULL;" + EOL, + Sql); + } + + [Fact] + public override void CreateIndexOperation_with_filter_where_clause_and_is_unique() + { + base.CreateIndexOperation_with_filter_where_clause_and_is_unique(); + + Assert.Equal( + "CREATE UNIQUE INDEX [IX_People_Name] ON [People] ([Name]) WHERE [Name] IS NOT NULL AND <> '';" + EOL, + Sql); + } + [Fact] public virtual void AddColumnOperation_with_computedSql() {