Add a clause/filter to the index to ignore nulls

pull/137/head
Christopher Jolly 3 years ago
parent 306585af13
commit 0eb8baefa9

@ -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<StoreGenerationConvention>(
new JetStoreGenerationConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<ValueGenerationConvention>(
new JetValueGenerationConvention(Dependencies, RelationalDependencies));
return conventionSet;
}
public static ConventionSet Build()
{
using var serviceScope = CreateServiceScope();
using var context = serviceScope.ServiceProvider.GetRequiredService<DbContext>();
return ConventionSet.CreateConventionSet(context);
}
public static ModelBuilder CreateModelBuilder()
{
using var serviceScope = CreateServiceScope();
using var context = serviceScope.ServiceProvider.GetRequiredService<DbContext>();
return new ModelBuilder(ConventionSet.CreateConventionSet(context), context.GetService<ModelDependencies>());
}
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<IServiceScopeFactory>().CreateScope();
using var context = serviceScope.ServiceProvider.GetRequiredService<DbContext>();
return ConventionSet.CreateConventionSet(context);
return serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}
}
}

@ -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;
/// <summary>
/// A convention that configures the filter for unique non-clustered indexes with nullable columns
/// to filter out null values.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
public class JetIndexConvention :
IEntityTypeBaseTypeChangedConvention,
IIndexAddedConvention,
IIndexUniquenessChangedConvention,
IIndexAnnotationChangedConvention,
IPropertyNullabilityChangedConvention,
IPropertyAnnotationChangedConvention
{
private readonly ISqlGenerationHelper _sqlGenerationHelper;
/// <summary>
/// Creates a new instance of <see cref="JetIndexConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="relationalDependencies"> Parameter object containing relational dependencies for this convention.</param>
/// <param name="sqlGenerationHelper">SQL command generation helper service.</param>
public JetIndexConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies,
ISqlGenerationHelper sqlGenerationHelper)
{
_sqlGenerationHelper = sqlGenerationHelper;
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}
/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }
/// <summary>
/// Called after the base type of an entity type changes.
/// </summary>
/// <param name="entityTypeBuilder">The builder for the entity type.</param>
/// <param name="newBaseType">The new base entity type.</param>
/// <param name="oldBaseType">The old base entity type.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if (oldBaseType == null
|| newBaseType == null)
{
foreach (var index in entityTypeBuilder.Metadata.GetDeclaredIndexes())
{
SetIndexFilter(index.Builder);
}
}
}
/// <summary>
/// Called after an index is added to the entity type.
/// </summary>
/// <param name="indexBuilder">The builder for the index.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessIndexAdded(
IConventionIndexBuilder indexBuilder,
IConventionContext<IConventionIndexBuilder> context)
=> SetIndexFilter(indexBuilder);
/// <summary>
/// Called after the uniqueness for an index is changed.
/// </summary>
/// <param name="indexBuilder">The builder for the index.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessIndexUniquenessChanged(
IConventionIndexBuilder indexBuilder,
IConventionContext<bool?> context)
=> SetIndexFilter(indexBuilder);
/// <summary>
/// Called after the nullability for a property is changed.
/// </summary>
/// <param name="propertyBuilder">The builder for the property.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessPropertyNullabilityChanged(
IConventionPropertyBuilder propertyBuilder,
IConventionContext<bool?> context)
{
foreach (var index in propertyBuilder.Metadata.GetContainingIndexes())
{
SetIndexFilter(index.Builder);
}
}
/// <summary>
/// Called after an annotation is changed on an index.
/// </summary>
/// <param name="indexBuilder">The builder for the index.</param>
/// <param name="name">The annotation name.</param>
/// <param name="annotation">The new annotation.</param>
/// <param name="oldAnnotation">The old annotation.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessIndexAnnotationChanged(
IConventionIndexBuilder indexBuilder,
string name,
IConventionAnnotation? annotation,
IConventionAnnotation? oldAnnotation,
IConventionContext<IConventionAnnotation> context)
{
if (name == JetAnnotationNames.Clustered)
{
SetIndexFilter(indexBuilder);
}
}
/// <summary>
/// Called after an annotation is changed on a property.
/// </summary>
/// <param name="propertyBuilder">The builder for the property.</param>
/// <param name="name">The annotation name.</param>
/// <param name="annotation">The new annotation.</param>
/// <param name="oldAnnotation">The old annotation.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessPropertyAnnotationChanged(
IConventionPropertyBuilder propertyBuilder,
string name,
IConventionAnnotation? annotation,
IConventionAnnotation? oldAnnotation,
IConventionContext<IConventionAnnotation> 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<string> 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<string> nullableColumns)
{
var builder = new StringBuilder();
if (nullableColumns.Any())
{
builder.Append("IGNORE NULL");
}
return builder.ToString();
}
private static List<string>? GetNullableColumns(IReadOnlyIndex index)
{
var tableName = index.DeclaringEntityType.GetTableName();
if (tableName == null)
{
return null;
}
var nullableColumns = new List<string>();
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;
}
}

@ -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) &&

Loading…
Cancel
Save