Skip to content

Commit

Permalink
Implement BulkDelete
Browse files Browse the repository at this point in the history
Part of #795
  • Loading branch information
smitpatel committed Jul 28, 2022
1 parent 7cc7429 commit 56332fa
Show file tree
Hide file tree
Showing 56 changed files with 3,221 additions and 63 deletions.
59 changes: 59 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public static DbCommand CreateDbCommand(this IQueryable source)
throw new NotSupportedException(RelationalStrings.NoDbCommand);
}

#region FromSql

/// <summary>
/// Creates a LINQ query based on a raw SQL query.
/// </summary>
Expand Down Expand Up @@ -162,6 +164,10 @@ private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot(
Expression.Constant(arguments));
}

#endregion

#region SplitQuery

/// <summary>
/// Returns a new query which is configured to load the collections in the query results in a single database query.
/// </summary>
Expand Down Expand Up @@ -224,4 +230,57 @@ public static IQueryable<TEntity> AsSplitQuery<TEntity>(

internal static readonly MethodInfo AsSplitQueryMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSplitQuery))!;

#endregion

#region BulkDelete

/// <summary>
/// Deletes all entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <returns>The total number of entity instances deleted from the database.</returns>
public static int BulkDelete<TSource>(this IQueryable<TSource> source)
=> source.Provider.Execute<int>(Expression.Call(BulkDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression));

/// <summary>
/// Asynchronously deletes all entity instances which match the LINQ query from the database.
/// </summary>
/// <remarks>
/// <para>
/// This operation executes immediately against the database, rather than being deferred until
/// <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
/// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
/// to reflect the changes.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="source">The source query.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>The total number of entity instances deleted from the database.</returns>
public static Task<int> BulkDeleteAsync<TSource>(this IQueryable<TSource> source, CancellationToken cancellationToken = default)
=> source.Provider is IAsyncQueryProvider provider
? provider.ExecuteAsync<Task<int>>(
Expression.Call(BulkDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression), cancellationToken)
: throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);

internal static readonly MethodInfo BulkDeleteMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(BulkDelete))!;

#endregion
}
64 changes: 64 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@
<data name="BadSequenceType" xml:space="preserve">
<value>Invalid type for sequence. Valid types are long (the default), int, short, byte and decimal.</value>
</data>
<data name="BulkDeleteOnTableSplitting" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' is being applied on the table '{tableName}' which contains data for multiple entity types. Applying this delete operation will also delete data for other entity type(s) hence it is not supported.</value>
</data>
<data name="BulkOperationOnEntitySplitting" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' is being applied on entity type '{entityType}' is using entity splitting. Bulk operations on entity types using entity splitting is not allowed.</value>
</data>
<data name="BulkOperationOnKeylessEntityTypeWithUnsupportedOperator" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.</value>
</data>
<data name="BulkOperationOnNonEntityType" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' requires an entity type which corresponds to the database table to be modified. The current operation is being applied on a non-entity projection. Remove any projection to non-entity type.</value>
</data>
<data name="BulkOperationOnTPC" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' is being applied on entity type '{entityType}' which is using TPC mapping strategy and is not leaf type. Bulk operations on entity type participating TPC except for leaf type is not allowed.</value>
</data>
<data name="BulkOperationOnTPT" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' is being applied on entity type '{entityType}' which is using TPT mapping strategy. Bulk operations on hierarchy mapped as TPT is not allowed.</value>
</data>
<data name="BulkOperationWithUnsupportedOperatorInSqlGeneration" xml:space="preserve">
<value>The bulk operation '{bulkOperation}' contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported by provider during translation phase. This is an issue in provider.</value>
</data>
<data name="CannotChangeWhenOpen" xml:space="preserve">
<value>The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.</value>
</data>
Expand Down Expand Up @@ -792,6 +813,9 @@
<data name="NoneRelationalTypeMappingOnARelationalTypeMappingSource" xml:space="preserve">
<value>'FindMapping' was called on a 'RelationalTypeMappingSource' with a non-relational 'TypeMappingInfo'.</value>
</data>
<data name="NonQueryTranslationFailedWithDetails" xml:space="preserve">
<value>The LINQ expression '{expression}' could not be translated. Additional information: {details} See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.</value>
</data>
<data name="NonScalarFunctionCannotBeNullable" xml:space="preserve">
<value>Cannot set 'IsNullable' on DbFunction '{functionName}' since the function does not represent a scalar function.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public override bool Equals(object? obj)

public bool Equals(CommandCacheKey commandCacheKey)
{
// Intentionally reference equals
// Intentionally reference equal, don't check internal components
if (!ReferenceEquals(_queryExpression, commandCacheKey._queryExpression))
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ public SelectExpressionProjectionApplyingExpressionVisitor(QuerySplittingBehavio
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression is ShapedQueryExpression shapedQueryExpression
&& shapedQueryExpression.QueryExpression is SelectExpression selectExpression
? shapedQueryExpression.UpdateShaperExpression(
=> extensionExpression switch
{
ShapedQueryExpression shapedQueryExpression
when shapedQueryExpression.QueryExpression is SelectExpression selectExpression
=> shapedQueryExpression.UpdateShaperExpression(
selectExpression.ApplyProjection(
shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior))
: base.VisitExtension(extensionExpression);
shapedQueryExpression.ShaperExpression, shapedQueryExpression.ResultCardinality, _querySplittingBehavior)),
NonQueryExpression nonQueryExpression => nonQueryExpression,
_ => base.VisitExtension(extensionExpression),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public class SelectExpressionPruningExpressionVisitor : ExpressionVisitor
relationalSplitCollectionShaperExpression.SelectExpression.Prune(),
Visit(relationalSplitCollectionShaperExpression.InnerShaper));

case DeleteExpression deleteExpression:
return deleteExpression.Update(deleteExpression.SelectExpression.Prune());

default:
return base.Visit(expression);
}
Expand Down
55 changes: 55 additions & 0 deletions src/EFCore.Relational/Query/NonQueryExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query;

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public class NonQueryExpression : Expression, IPrintableExpression
{
public NonQueryExpression(DeleteExpression deleteExpression)
{
DeleteExpression = deleteExpression;
}

public virtual DeleteExpression DeleteExpression { get; }

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

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

protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var deleteExpression = (DeleteExpression)visitor.Visit(DeleteExpression);

return Update(deleteExpression);
}

public virtual NonQueryExpression Update(DeleteExpression deleteExpression)
=> deleteExpression != DeleteExpression
? new NonQueryExpression(deleteExpression)
: this;

/// <inheritdoc />
public virtual void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append($"({nameof(NonQueryExpression)}: ");
expressionPrinter.Visit(DeleteExpression);
}

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

private bool Equals(NonQueryExpression nonQueryExpression)
=> DeleteExpression == nonQueryExpression.DeleteExpression;

/// <inheritdoc />
public override int GetHashCode() => DeleteExpression.GetHashCode();
}
39 changes: 36 additions & 3 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public virtual IRelationalCommand GetCommand(Expression queryExpression)
switch (queryExpression)
{
case SelectExpression selectExpression:
{
GenerateTagsHeaderComment(selectExpression);

if (selectExpression.IsNonComposedFromSql())
Expand All @@ -82,8 +81,11 @@ public virtual IRelationalCommand GetCommand(Expression queryExpression)
{
VisitSelect(selectExpression);
}
}
break;
break;

case DeleteExpression deleteExpression:
VisitDelete(deleteExpression);
break;

default:
throw new InvalidOperationException();
Expand Down Expand Up @@ -147,6 +149,37 @@ private static bool IsNonComposedSetOperation(SelectExpression selectExpression)
column.Name, setOperation.Source1.Projection[index].Alias, StringComparison.Ordinal))
.All(e => e);


/// <inheritdoc />
protected override 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.Tables.Count == 1
&& selectExpression.Tables[0] == deleteExpression.Table
&& selectExpression.Projection.Count == 0)
{
_relationalCommandBuilder.Append("DELETE FROM ");
Visit(deleteExpression.Table);

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

return deleteExpression;
}

throw new InvalidOperationException(
RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.BulkDelete)));
}

/// <inheritdoc />
protected override Expression VisitSelect(SelectExpression selectExpression)
{
Expand Down
Loading

0 comments on commit 56332fa

Please sign in to comment.