From 0eb8baefa9eb800211be1304b3c915320286434e Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 26 Apr 2023 01:08:56 +0800 Subject: [PATCH] Add a clause/filter to the index to ignore nulls --- .../Conventions/JetConventionSetBuilder.cs | 50 ++-- .../Conventions/JetIndexConvention.cs | 231 ++++++++++++++++++ .../Migrations/JetMigrationsSqlGenerator.cs | 8 +- 3 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs b/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs index 0d07d9f..8b768e4 100644 --- a/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs +++ b/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs @@ -2,7 +2,9 @@ using EntityFrameworkCore.Jet.Data; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace @@ -10,41 +12,47 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { public class JetConventionSetBuilder : RelationalConventionSetBuilder { + private readonly ISqlGenerationHelper _sqlGenerationHelper; public JetConventionSetBuilder( [NotNull] ProviderConventionSetBuilderDependencies dependencies, - [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) + [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies, + ISqlGenerationHelper sqlGenerationHelper) : base(dependencies, relationalDependencies) { + _sqlGenerationHelper = sqlGenerationHelper; } public override ConventionSet CreateConventionSet() { var conventionSet = base.CreateConventionSet(); - var valueGenerationStrategyConvention = new JetValueGenerationStrategyConvention(Dependencies, RelationalDependencies); + conventionSet.Add(new JetValueGenerationStrategyConvention(Dependencies, RelationalDependencies)); + conventionSet.Add(new RelationalMaxIdentifierLengthConvention(64, Dependencies, RelationalDependencies)); + conventionSet.Add(new JetIndexConvention(Dependencies, RelationalDependencies, _sqlGenerationHelper)); - conventionSet.ModelInitializedConventions.Add(valueGenerationStrategyConvention); - conventionSet.ModelInitializedConventions.Add( - new RelationalMaxIdentifierLengthConvention(64, Dependencies, RelationalDependencies)); - - ValueGenerationConvention valueGenerationConvention = - new JetValueGenerationConvention(Dependencies, RelationalDependencies); - - ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); - ReplaceConvention(conventionSet.EntityTypeAnnotationChangedConventions, (RelationalValueGenerationConvention) valueGenerationConvention); - ReplaceConvention(conventionSet.EntityTypePrimaryKeyChangedConventions, valueGenerationConvention); - ReplaceConvention(conventionSet.ForeignKeyAddedConventions, valueGenerationConvention); - ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); - - StoreGenerationConvention storeGenerationConvention = new JetStoreGenerationConvention(Dependencies, RelationalDependencies); - ReplaceConvention(conventionSet.PropertyAnnotationChangedConventions, storeGenerationConvention); - ReplaceConvention(conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention) valueGenerationConvention); - ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention); + conventionSet.Replace( + new JetStoreGenerationConvention(Dependencies, RelationalDependencies)); + conventionSet.Replace( + new JetValueGenerationConvention(Dependencies, RelationalDependencies)); return conventionSet; } public static ConventionSet Build() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return ConventionSet.CreateConventionSet(context); + } + + public static ModelBuilder CreateModelBuilder() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return new ModelBuilder(ConventionSet.CreateConventionSet(context), context.GetService()); + } + + private static IServiceScope CreateServiceScope() { var serviceProvider = new ServiceCollection() .AddEntityFrameworkJet() @@ -54,9 +62,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions .UseInternalServiceProvider(p)) .BuildServiceProvider(); - using var serviceScope = serviceProvider.GetRequiredService().CreateScope(); - using var context = serviceScope.ServiceProvider.GetRequiredService(); - return ConventionSet.CreateConventionSet(context); + return serviceProvider.GetRequiredService().CreateScope(); } } } \ No newline at end of file diff --git a/src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs b/src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs new file mode 100644 index 0000000..3bccd34 --- /dev/null +++ b/src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using EntityFrameworkCore.Jet.Metadata.Internal; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that configures the filter for unique non-clustered indexes with nullable columns +/// to filter out null values. +/// +/// +/// See Model building conventions, and +/// Accessing SQL Server and SQL Azure databases with EF Core +/// for more information and examples. +/// +public class JetIndexConvention : + IEntityTypeBaseTypeChangedConvention, + IIndexAddedConvention, + IIndexUniquenessChangedConvention, + IIndexAnnotationChangedConvention, + IPropertyNullabilityChangedConvention, + IPropertyAnnotationChangedConvention +{ + private readonly ISqlGenerationHelper _sqlGenerationHelper; + + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + /// SQL command generation helper service. + public JetIndexConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies, + ISqlGenerationHelper sqlGenerationHelper) + { + _sqlGenerationHelper = sqlGenerationHelper; + + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + /// Called after the base type of an entity type changes. + /// + /// The builder for the entity type. + /// The new base entity type. + /// The old base entity type. + /// Additional information associated with convention execution. + public virtual void ProcessEntityTypeBaseTypeChanged( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionEntityType? newBaseType, + IConventionEntityType? oldBaseType, + IConventionContext context) + { + if (oldBaseType == null + || newBaseType == null) + { + foreach (var index in entityTypeBuilder.Metadata.GetDeclaredIndexes()) + { + SetIndexFilter(index.Builder); + } + } + } + + /// + /// Called after an index is added to the entity type. + /// + /// The builder for the index. + /// Additional information associated with convention execution. + public virtual void ProcessIndexAdded( + IConventionIndexBuilder indexBuilder, + IConventionContext context) + => SetIndexFilter(indexBuilder); + + /// + /// Called after the uniqueness for an index is changed. + /// + /// The builder for the index. + /// Additional information associated with convention execution. + public virtual void ProcessIndexUniquenessChanged( + IConventionIndexBuilder indexBuilder, + IConventionContext context) + => SetIndexFilter(indexBuilder); + + /// + /// Called after the nullability for a property is changed. + /// + /// The builder for the property. + /// Additional information associated with convention execution. + public virtual void ProcessPropertyNullabilityChanged( + IConventionPropertyBuilder propertyBuilder, + IConventionContext context) + { + foreach (var index in propertyBuilder.Metadata.GetContainingIndexes()) + { + SetIndexFilter(index.Builder); + } + } + + /// + /// Called after an annotation is changed on an index. + /// + /// The builder for the index. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + public virtual void ProcessIndexAnnotationChanged( + IConventionIndexBuilder indexBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (name == JetAnnotationNames.Clustered) + { + SetIndexFilter(indexBuilder); + } + } + + /// + /// Called after an annotation is changed on a property. + /// + /// The builder for the property. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + public virtual void ProcessPropertyAnnotationChanged( + IConventionPropertyBuilder propertyBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (name == RelationalAnnotationNames.ColumnName) + { + foreach (var index in propertyBuilder.Metadata.GetContainingIndexes()) + { + SetIndexFilter(index.Builder, columnNameChanged: true); + } + } + } + + private void SetIndexFilter(IConventionIndexBuilder indexBuilder, bool columnNameChanged = false) + { + var index = indexBuilder.Metadata; + if (index.IsUnique + && index.IsClustered() != true + && GetNullableColumns(index) is List nullableColumns + && nullableColumns.Count > 0) + { + if (columnNameChanged + || index.GetFilter() == null) + { + indexBuilder.HasFilter(CreateIndexFilter(nullableColumns)); + } + } + else + { + if (index.GetFilter() != null) + { + indexBuilder.HasFilter(null); + } + } + } + + private string CreateIndexFilter(List nullableColumns) + { + var builder = new StringBuilder(); + + if (nullableColumns.Any()) + { + builder.Append("IGNORE NULL"); + } + + return builder.ToString(); + } + + private static List? GetNullableColumns(IReadOnlyIndex index) + { + var tableName = index.DeclaringEntityType.GetTableName(); + if (tableName == null) + { + return null; + } + + var nullableColumns = new List(); + var table = StoreObjectIdentifier.Table(tableName, index.DeclaringEntityType.GetSchema()); + foreach (var property in index.Properties) + { + var columnName = property.GetColumnName(table); + if (columnName == null) + { + return null; + } + + if (!property.IsColumnNullable(table)) + { + continue; + } + + nullableColumns.Add(columnName); + } + + return nullableColumns; + } +} diff --git a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs index b7f8314..335cbbc 100644 --- a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs +++ b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs @@ -426,7 +426,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations if (!string.IsNullOrEmpty(operation.Filter)) { builder - .Append(" WHERE ") + .Append(" WITH ") .Append(operation.Filter); } @@ -715,7 +715,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations Check.NotEmpty(name, nameof(name)); Check.NotNull(operation, nameof(operation)); Check.NotNull(builder, nameof(builder)); - + if (operation.ComputedColumnSql != null) { ComputedColumnDefinition(schema, table, name, operation, model, builder); @@ -746,7 +746,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations [CanBeNull] IModel? model) { var storeType = operation.ColumnType; - + if (IsIdentity(operation) && (storeType == null || Dependencies.TypeMappingSource.FindMapping(storeType) is JetIntTypeMapping)) { @@ -762,7 +762,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations } storeType ??= base.GetColumnType(schema, table, name, operation, model); - + if (string.Equals(storeType, "counter", StringComparison.OrdinalIgnoreCase) && operation[JetAnnotationNames.Identity] is string identity && !string.IsNullOrEmpty(identity) &&