Skip to content

Commit

Permalink
Implement bulk delete
Browse files Browse the repository at this point in the history
Following dotnet/efcore#28492

Part of #2450
  • Loading branch information
roji committed Aug 5, 2022
1 parent 2524b30 commit a1aa03e
Show file tree
Hide file tree
Showing 21 changed files with 1,400 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio
.TryAdd<IHistoryRepository, NpgsqlHistoryRepository>()
.TryAdd<ICompiledQueryCacheKeyGenerator, NpgsqlCompiledQueryCacheKeyGenerator>()
.TryAdd<IExecutionStrategyFactory, NpgsqlExecutionStrategyFactory>()
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, NpgsqlQueryableMethodTranslatingExpressionVisitorFactory>()
.TryAdd<IMethodCallTranslatorProvider, NpgsqlMethodCallTranslatorProvider>()
.TryAdd<IAggregateMethodCallTranslatorProvider, NpgsqlAggregateMethodCallTranslatorProvider>()
.TryAdd<IMemberTranslatorProvider, NpgsqlMemberTranslatorProvider>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;

public sealed class PostgresDeleteExpression : Expression, IPrintableExpression
{
/// <summary>
/// The tables that rows are to be deleted from.
/// </summary>
public TableExpression Table { get; }

/// <summary>
/// Additional tables which can be referenced in the predicate.
/// </summary>
public IReadOnlyList<TableExpressionBase> FromItems { get; }

/// <summary>
/// The WHERE predicate for the DELETE.
/// </summary>
public SqlExpression? Predicate { get; }

public PostgresDeleteExpression(TableExpression table, IReadOnlyList<TableExpressionBase> fromItems, SqlExpression? predicate)
=> (Table, FromItems, Predicate) = (table, fromItems, predicate);

/// <inheritdoc />
public override Type Type
=> typeof(object);

/// <inheritdoc />
public override ExpressionType NodeType
=> ExpressionType.Extension;

protected override Expression VisitChildren(ExpressionVisitor visitor)
=> Predicate is null
? this
: Update((SqlExpression?)visitor.Visit(Predicate));

public PostgresDeleteExpression Update(SqlExpression? predicate)
=> predicate == Predicate
? this
: new PostgresDeleteExpression(Table, FromItems, predicate);

public void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.AppendLine($"DELETE FROM {Table.Name} AS {Table.Alias}");

if (FromItems.Count > 0)
{
var first = true;
foreach (var fromItem in FromItems)
{
if (first)
{
expressionPrinter.Append("USING ");
first = false;
}
else
{
expressionPrinter.Append(", ");
}

expressionPrinter.Visit(fromItem);
}
}

if (Predicate is not null)
{
expressionPrinter.Append("WHERE ");
expressionPrinter.Visit(Predicate);
}
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is PostgresDeleteExpression pgDeleteExpression
&& Equals(pgDeleteExpression));

private bool Equals(PostgresDeleteExpression pgDeleteExpression)
=> Table == pgDeleteExpression.Table
&& FromItems.SequenceEqual(pgDeleteExpression.FromItems)
&& (Predicate is null ? pgDeleteExpression.Predicate is null : Predicate.Equals(pgDeleteExpression.Predicate));

/// <inheritdoc />
public override int GetHashCode() => Table.GetHashCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

/// <summary>
/// Converts the relational <see cref="NonQueryExpression" /> into a PG-specific <see cref="PostgresDeleteExpression" />, which
/// precisely models a DELETE statement in PostgreSQL. This is done to handle the PG-specific USING syntax for table joining.
/// </summary>
public class NonQueryConvertingExpressionVisitor : ExpressionVisitor
{
public virtual Expression Process(Expression node)
=> node switch
{
DeleteExpression deleteExpression => VisitDelete(deleteExpression),

_ => node
};

protected virtual Expression VisitDelete(DeleteExpression deleteExpression)
{
var selectExpression = deleteExpression.SelectExpression;

if (selectExpression.Offset != null
|| selectExpression.Limit != null
|| selectExpression.Having != null
|| selectExpression.Orderings.Count > 0
|| selectExpression.GroupBy.Count > 0
|| selectExpression.Projection.Count > 0)
{
throw new InvalidOperationException(
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(
nameof(RelationalQueryableExtensions.ExecuteDelete)));
}

var fromItems = new List<TableExpressionBase>();
SqlExpression? joinPredicates = null;

// The SelectExpression also contains the target table being modified (same as deleteExpression.Table).
// If it has additional inner joins, use the PostgreSQL-specific USING syntax to express the join.
// Note that the non-join TableExpression isn't necessary the target table - through projection the last table being
// joined may be the one being modified.
foreach (var tableBase in selectExpression.Tables)
{
switch (tableBase)
{
case TableExpression tableExpression:
if (tableExpression != deleteExpression.Table)
{
fromItems.Add(tableExpression);
}

break;

case InnerJoinExpression { Table: { } tableExpression } innerJoinExpression:
if (tableExpression != deleteExpression.Table)
{
fromItems.Add(tableExpression);
}

joinPredicates = joinPredicates is null
? innerJoinExpression.JoinPredicate
: new SqlBinaryExpression(
ExpressionType.AndAlso, joinPredicates, innerJoinExpression.JoinPredicate, typeof(bool),
innerJoinExpression.JoinPredicate.TypeMapping);
break;

default:
throw new InvalidOperationException(
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(
nameof(RelationalQueryableExtensions.ExecuteDelete)));
}
}

// Combine the join predicates (if any) before the user-provided predicate
var predicate = (joinPredicates, selectExpression.Predicate) switch
{
(null, not null) => selectExpression.Predicate,
(not null, null) => joinPredicates,
(null, null) => null,
(not null, not null) => new SqlBinaryExpression(
ExpressionType.AndAlso, joinPredicates, selectExpression.Predicate, typeof(bool), joinPredicates.TypeMapping)
};

return new PostgresDeleteExpression(deleteExpression.Table, fromItems, predicate);
}
}
12 changes: 12 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ public NpgsqlParameterBasedSqlProcessor(
{
}

public override Expression Optimize(
Expression queryExpression,
IReadOnlyDictionary<string, object?> parametersValues,
out bool canCache)
{
queryExpression = base.Optimize(queryExpression, parametersValues, out canCache);

queryExpression = new NonQueryConvertingExpressionVisitor().Process(queryExpression);

return queryExpression;
}

/// <inheritdoc />
protected override Expression ProcessSqlNullability(
Expression selectExpression, IReadOnlyDictionary<string, object?> parametersValues, out bool canCache)
Expand Down
58 changes: 58 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
PostgresAnyExpression anyExpression => VisitArrayAny(anyExpression),
PostgresArrayIndexExpression arrayIndexExpression => VisitArrayIndex(arrayIndexExpression),
PostgresBinaryExpression binaryExpression => VisitPostgresBinary(binaryExpression),
PostgresDeleteExpression deleteExpression => VisitPostgresDelete(deleteExpression),
PostgresFunctionExpression functionExpression => VisitPostgresFunction(functionExpression),
PostgresILikeExpression iLikeExpression => VisitILike(iLikeExpression),
PostgresJsonTraversalExpression jsonTraversalExpression => VisitJsonPathTraversal(jsonTraversalExpression),
Expand Down Expand Up @@ -206,6 +207,45 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression binary)
}
}

protected override void GenerateRootCommand(Expression rootExpression)
{
switch (rootExpression)
{
case PostgresDeleteExpression pgDeleteExpression:
VisitPostgresDelete(pgDeleteExpression);
return;

default:
base.GenerateRootCommand(rootExpression);
return;
}
}

// NonQueryConvertingExpressionVisitor converts the relational DeleteExpression to PostgresDeleteExpression, so we should never
// get here
protected override Expression VisitDelete(DeleteExpression deleteExpression)
=> throw new InvalidOperationException("Inconceivable!");

protected virtual Expression VisitPostgresDelete(PostgresDeleteExpression pgDeleteExpression)
{
Sql.Append("DELETE FROM ");
Visit(pgDeleteExpression.Table);

if (pgDeleteExpression.FromItems.Count > 0)
{
Sql.AppendLine().Append("USING ");
GenerateList(pgDeleteExpression.FromItems, t => Visit(t), sql => sql.Append(", "));
}

if (pgDeleteExpression.Predicate != null)
{
Sql.AppendLine().Append("WHERE ");
Visit(pgDeleteExpression.Predicate);
}

return pgDeleteExpression;
}

protected virtual Expression VisitPostgresNewArray(PostgresNewArrayExpression postgresNewArrayExpression)
{
Debug.Assert(postgresNewArrayExpression.TypeMapping is not null);
Expand Down Expand Up @@ -769,4 +809,22 @@ public virtual Expression VisitPostgresFunction(PostgresFunctionExpression e)

private static bool RequiresBrackets(SqlExpression expression)
=> expression is SqlBinaryExpression || expression is LikeExpression || expression is PostgresBinaryExpression;

private void GenerateList<T>(
IReadOnlyList<T> items,
Action<T> generationAction,
Action<IRelationalCommandBuilder>? joinAction = null)
{
joinAction ??= (isb => isb.Append(", "));

for (var i = 0; i < items.Count; i++)
{
if (i > 0)
{
joinAction(Sql);
}

generationAction(items[i]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Diagnostics.CodeAnalysis;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
public NpgsqlQueryableMethodTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
}

protected override bool IsValidSelectExpressionForExecuteDelete(
SelectExpression selectExpression,
EntityShaperExpression entityShaperExpression,
[NotNullWhen(true)] out TableExpression? tableExpression)
{
// The default relational behavior is to allow only single-table expressions, and the only permitted feature is a predicate.
// Here we extend this to also inner joins to tables, which we generate via the PostgreSQL-specific USING construct.
if (selectExpression.Offset == null
&& selectExpression.Limit == null
// If entity type has primary key then Distinct is no-op
&& (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null)
&& selectExpression.GroupBy.Count == 0
&& selectExpression.Having == null
&& selectExpression.Orderings.Count == 0)
{
TableExpressionBase? table = null;
if (selectExpression.Tables.Count == 1)
{
table = selectExpression.Tables[0];
}
else if (selectExpression.Tables.All(t => t is TableExpression or InnerJoinExpression))
{
var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression;
var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression);
var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First());
table = column.Table;
if (table is JoinExpressionBase joinExpressionBase)
{
table = joinExpressionBase.Table;
}
}

if (table is TableExpression te)
{
tableExpression = te;
return true;
}
}

tableExpression = null;
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

public class NpgsqlQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory
{
public NpgsqlQueryableMethodTranslatingExpressionVisitorFactory(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies)
{
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}

protected virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { get; }

protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies RelationalDependencies { get; }

public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext)
=> new NpgsqlQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates;

public class FiltersInheritanceBulkUpdatesNpgsqlFixture : InheritanceBulkUpdatesNpgsqlFixture
{
protected override bool EnableFilters
=> true;
}
Loading

0 comments on commit a1aa03e

Please sign in to comment.