diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index b5bb747435..2025056dcb 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.Sql; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; @@ -50,6 +51,7 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { + // ReSharper disable once UnusedMember.Global public static class NpgsqlEntityFrameworkServicesBuilderExtensions { /// @@ -106,6 +108,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs index 3e2e4953be..7f4d2150eb 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayFragmentTranslator.cs @@ -49,8 +49,8 @@ public Expression Translate(Expression expression) if (!(expression is SubQueryExpression subQuery)) return null; - if (ContainsResult(subQuery) is Expression contains) - return contains; +// if (ContainsResult(subQuery) is Expression contains) +// return contains; if (subQuery.QueryModel.BodyClauses.Count != 1) return null; @@ -101,6 +101,13 @@ public Expression Translate(Expression expression) static Expression ContainsResult(SubQueryExpression expression) { var model = expression.QueryModel; + + if (!(model.MainFromClause.FromExpression is Expression from)) + return null; + + if (!IsArrayOrList(from.Type)) + return null; + if (model.BodyClauses.Count != 0) return null; @@ -110,7 +117,7 @@ static Expression ContainsResult(SubQueryExpression expression) if (!(model.ResultOperators[0] is ContainsResultOperator contains)) return null; - return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, model.MainFromClause.FromExpression); + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", contains.Item, from); } #endregion diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs new file mode 100644 index 0000000000..45a46793cf --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlExistsToAnyRewritingExpressionVisitor.cs @@ -0,0 +1,87 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Parsing.ExpressionVisitors; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors +{ + /// + /// An expression rewriter for . + /// + public class NpgsqlExistsToAnyRewritingExpressionVisitor : ExpressionVisitorBase + { + /// + /// The generic for . + /// + [NotNull] static readonly MethodInfo Exists = + typeof(Array).GetRuntimeMethods().Single(x => x.Name == nameof(Array.Exists)); + + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (!methodCallExpression.Method.MethodIsClosedFormOf(Exists)) + return methodCallExpression; + + if (!(methodCallExpression.Arguments[0] is Expression array)) + return methodCallExpression; + + if (!(methodCallExpression.Arguments[1] is LambdaExpression predicate)) + return methodCallExpression; + + var mainFromClause = + new MainFromClause( + "", + array.Type.GetElementType(), + array); + + var qsre = new QuerySourceReferenceExpression(mainFromClause); + var queryModel = new QueryModel(mainFromClause, new SelectClause(qsre)); + + var where = + new WhereClause( + ReplacingExpressionVisitor.Replace( + predicate.Parameters[0], + qsre, + predicate.Body)); + + queryModel.BodyClauses.Add(where); + queryModel.ResultOperators.Add(new AnyResultOperator()); + queryModel.ResultTypeOverride = typeof(bool); + + return new SubQueryExpression(queryModel); + } + } +} diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 5a38b2704c..4f86df8513 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -168,6 +168,9 @@ protected virtual Expression VisitArraySubQuery([NotNull] SubQueryExpression exp case ConcatResultOperator concatResultOperator: return VisitArrayConcat(array, concatResultOperator); + case ContainsResultOperator contains: + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", Visit(contains.Item) ?? contains.Item, array); + case CountResultOperator countResultOperator: return VisitArrayCount(array, countResultOperator); diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs new file mode 100644 index 0000000000..a10d1425f8 --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryOptimizer.cs @@ -0,0 +1,58 @@ +#region License + +// The PostgreSQL License +// +// Copyright (C) 2016 The Npgsql Development Team +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#endregion + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors; +using Remotion.Linq; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal +{ + /// + /// The default relational LINQ query optimizer for Npgsql. + /// + public class NpgsqlQueryOptimizer : QueryOptimizer + { + /// + /// The default expression visitors registered by the Npgsql provider. + /// + static readonly ExpressionVisitor[] ExpressionVisitors = + { + new NpgsqlExistsToAnyRewritingExpressionVisitor() + }; + + /// + public override void Optimize(QueryCompilationContext queryCompilationContext, QueryModel queryModel) + { + base.Optimize(queryCompilationContext, queryModel); + + for (int i = 0; i < ExpressionVisitors.Length; i++) + { + queryModel.TransformExpressions(ExpressionVisitors[i].Visit); + } + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index ca2875df1c..cff226be85 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -676,7 +676,7 @@ public void Array_Exists_equals_with_parameter_array_and_column_array_element() { var array = new[] { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => y == x.SomeArray[0])).ToList(); - AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] IN (0, 1, 2)"); } } @@ -687,11 +687,7 @@ public void List_Exists_equals_with_parameter_array_and_column_list_element() { var list = new List { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => list.Exists(y => y == x.SomeList[0])).ToList(); - - // What we don't want: - AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); - // What we do want: - AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeList""[1] IN (0, 1, 2)"); } } @@ -702,7 +698,7 @@ public void Array_Exists_equals_with_parameter_array_and_column_array_element_fl { var array = new[] { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => Array.Exists(array, y => x.SomeArray[0] == y)).ToList(); - AssertContainsInSql(@"WHERE x.""SomeArray""[1] = ANY (@__array_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeArray""[1] IN (0, 1, 2)"); } } @@ -713,11 +709,7 @@ public void List_Exists_equals_with_parameter_array_and_column_list_element_flip { var list = new List { 0, 1, 2 }; var _ = ctx.SomeEntities.Where(x => list.Exists(y => x.SomeList[0] == y)).ToList(); - - // What we don't want: - AssertDoesNotContainInSql(@"WHERE (x.""SomeList""[1]) IN (0, 1, 2)"); - // What we do want: - AssertContainsInSql(@"WHERE x.""SomeList""[1] = ANY (@__list_0) = TRUE"); + AssertContainsInSql(@"WHERE x.""SomeList""[1] IN (0, 1, 2)"); } }