Skip to content

Commit

Permalink
Implement joins via USING
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jul 27, 2022
1 parent 4d0e399 commit 5e877b9
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 24 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
92 changes: 79 additions & 13 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,85 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression binary)
}
}

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.Projection.Count == 0)
{
Sql.Append("DELETE FROM ");
Visit(deleteExpression.Table);

var predicate = selectExpression.Predicate;

// 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.
if (selectExpression.Tables.Count > 1)
{
Sql.AppendLine().Append("USING ");

var first = true;

for (var i = 0; i < selectExpression.Tables.Count; i++)
{
switch (selectExpression.Tables[i])
{
case InnerJoinExpression { Table: TableExpression tableExpression } innerJoinExpression:
// Add the table name and alias to the USING list, and add the join condition into the predicate
AppendToUsingList(tableExpression);

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

case TableExpression tableExpression:
AppendToUsingList(tableExpression);
break;

default:
throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration);

void AppendToUsingList(TableExpression tableExpression)
{
if (tableExpression == deleteExpression.Table)
{
return;
}

if (first)
{
first = false;
}
else
{
Sql.Append(", ");
}
Visit(tableExpression);
}
}
}
}

if (predicate is not null)
{
Sql.AppendLine().Append("WHERE ");

Visit(predicate);
}

return deleteExpression;
}

throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration);
}

protected virtual Expression VisitPostgresNewArray(PostgresNewArrayExpression postgresNewArrayExpression)
{
Debug.Assert(postgresNewArrayExpression.TypeMapping is not null);
Expand Down Expand Up @@ -426,19 +505,6 @@ protected override void GenerateSetOperationOperand(SetOperationBase setOperatio
base.GenerateSetOperationOperand(setOperation, operand);
}

protected override Expression VisitCollate(CollateExpression collateExpresion)
{
Check.NotNull(collateExpresion, nameof(collateExpresion));

Visit(collateExpresion.Operand);

Sql
.Append(" COLLATE ")
.Append(_sqlGenerationHelper.DelimitIdentifier(collateExpresion.Collation));

return collateExpresion;
}

public virtual Expression VisitArrayAll(PostgresAllExpression expression)
{
Visit(expression.Item);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Diagnostics.CodeAnalysis;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlQueryableMethodTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
}

protected override bool IsValidSelectExpressionForBulkDelete(
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
&& (!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 { Table: TableExpression }))
{
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,43 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class NpgsqlQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlQueryableMethodTranslatingExpressionVisitorFactory(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies)
{
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}

/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { get; }

/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies RelationalDependencies { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext)
=> new NpgsqlQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.BulkUpdates;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates;

public class FiltersInheritanceBulkUpdatesNpgsqlTest : FiltersInheritanceBulkUpdatesTestBase<FiltersInheritanceQueryNpgsqlFixture>
{
public FiltersInheritanceBulkUpdatesNpgsqlTest(FiltersInheritanceQueryNpgsqlFixture fixture)
: base(fixture)
{
}

[ConditionalFact]
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.BulkUpdates;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates;

public class InheritanceBulkUpdatesNpgsqlTest : InheritanceBulkUpdatesTestBase<InheritanceQueryNpgsqlFixture>
{
public InheritanceBulkUpdatesNpgsqlTest(InheritanceQueryNpgsqlFixture fixture)
: base(fixture)
{
}

[ConditionalFact]
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,9 @@ public override async Task Delete_SelectMany(bool async)
await base.Delete_SelectMany(async);

AssertSql(
@"DELETE FROM ""Order Details"" AS o
WHERE EXISTS (
SELECT 1
FROM ""Orders"" AS o0
INNER JOIN ""Order Details"" AS o1 ON o0.""OrderID"" = o1.""OrderID""
WHERE o0.""OrderID"" < 10250 AND o1.""OrderID"" = o.""OrderID"" AND o1.""ProductID"" = o.""ProductID"")");
@"DELETE FROM ""Order Details"" AS o0
USING ""Orders"" AS o
WHERE o.""OrderID"" = o0.""OrderID"" AND o.""OrderID"" < 10250");
}

public override async Task Delete_SelectMany_subquery(bool async)
Expand All @@ -270,11 +267,8 @@ public override async Task Delete_Where_using_navigation(bool async)

AssertSql(
@"DELETE FROM ""Order Details"" AS o
WHERE EXISTS (
SELECT 1
FROM ""Order Details"" AS o0
INNER JOIN ""Orders"" AS o1 ON o0.""OrderID"" = o1.""OrderID""
WHERE date_part('year', o1.""OrderDate"")::int = 2000 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")");
USING ""Orders"" AS o0
WHERE o.""OrderID"" = o0.""OrderID"" AND date_part('year', o0.""OrderDate"")::int = 2000");
}

public override async Task Delete_Where_using_navigation_2(bool async)
Expand Down

0 comments on commit 5e877b9

Please sign in to comment.