using System.Diagnostics.CodeAnalysis; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; /// /// 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. /// public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { /// /// 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. /// public NpgsqlQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, relationalDependencies, queryCompilationContext) { } /// /// 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. /// protected override bool IsValidSelectExpressionForExecuteUpdate( SelectExpression selectExpression, EntityShaperExpression entityShaperExpression, [NotNullWhen(true)] out TableExpression? tableExpression) { if (!base.IsValidSelectExpressionForExecuteUpdate(selectExpression, entityShaperExpression, out tableExpression)) { return false; } // PostgreSQL doesn't support referencing the main update table from anywhere except for the UPDATE WHERE clause. // This specifically makes it impossible to have joins which reference the main table in their predicate (ON ...). // Because of this, we detect all such inner joins and lift their predicates to the main WHERE clause (where a reference to the // main table is allowed) - see NpgsqlQuerySqlGenerator.VisitUpdate. // For any other type of join which contains a reference to the main table, we return false to trigger a subquery pushdown instead. OuterReferenceFindingExpressionVisitor? visitor = null; for (var i = 0; i < selectExpression.Tables.Count; i++) { var table = selectExpression.Tables[i]; if (ReferenceEquals(table, tableExpression)) { continue; } visitor ??= new OuterReferenceFindingExpressionVisitor(tableExpression); // For inner joins, if the predicate contains a reference to the main table, NpgsqlQuerySqlGenerator will lift the predicate // to the WHERE clause; so we only need to check the inner join's table (i.e. subquery) for such a reference. // Cross join and cross/outer apply (lateral joins) don't have predicates, so just check the entire join for a reference to // the main table, and switch to subquery syntax if one is found. // Left join does have a predicate, but it isn't possible to lift it to the main WHERE clause; so also check the entire // join. if (table is InnerJoinExpression innerJoin) { table = innerJoin.Table; } if (visitor.ContainsReferenceToMainTable(table)) { return false; } } return true; } /// /// 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. /// 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; } private sealed class OuterReferenceFindingExpressionVisitor : ExpressionVisitor { private readonly TableExpression _mainTable; private bool _containsReference; public OuterReferenceFindingExpressionVisitor(TableExpression mainTable) => _mainTable = mainTable; public bool ContainsReferenceToMainTable(TableExpressionBase tableExpression) { _containsReference = false; Visit(tableExpression); return _containsReference; } [return: NotNullIfNotNull("expression")] public override Expression? Visit(Expression? expression) { if (_containsReference) { return expression; } if (expression is ColumnExpression columnExpression && columnExpression.Table == _mainTable) { _containsReference = true; return expression; } return base.Visit(expression); } } }