Add a clause/filter to the index to ignore nulls
parent
306585af13
commit
0eb8baefa9
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue