From ab16cc49dd8aefbf24713fb4f890053287e71e24 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sat, 26 May 2018 21:22:24 -0400 Subject: [PATCH 1/4] Adds simple support to translates LIKE ANY expressions - Defines the `CustomArrayExpression` type. - Replaces `ArrayAnyExpression`. - Provides hooks for both `ANY` and `ALL` operations. Related: - https://www.postgresql.org/docs/current/static/functions-comparisons.html --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 112 ++++++-- .../Internal/ArrayAnyExpression.cs | 167 ------------ .../Internal/CustomArrayExpression.cs | 132 ++++++++++ .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 36 ++- .../Query/LikeAnyQueryNpgsqlTest.cs | 243 ++++++++++++++++++ 5 files changed, 490 insertions(+), 200 deletions(-) delete mode 100644 src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs create mode 100644 src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 9945b5f0a..bbdf936ea 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -1,4 +1,5 @@ #region License + // The PostgreSQL License // // Copyright (C) 2016 The Npgsql Development Team @@ -19,6 +20,7 @@ // 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; @@ -29,6 +31,7 @@ using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; @@ -36,7 +39,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors { public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor { - private readonly RelationalQueryModelVisitor _queryModelVisitor; + readonly RelationalQueryModelVisitor _queryModelVisitor; public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, @@ -52,15 +55,56 @@ public NpgsqlSqlTranslatingExpressionVisitor( protected override Expression VisitSubQuery(SubQueryExpression expression) { // Prefer the default EF Core translation if one exists - var result = base.VisitSubQuery(expression); - if (result != null) + if (base.VisitSubQuery(expression) is Expression result) return result; + if (VisitLikeAny(expression) is Expression likeAny) + return likeAny; + + if (VisitEqualsAny(expression) is Expression equalsAny) + return equalsAny; + + return null; + } + + protected override Expression VisitBinary(BinaryExpression expression) + { + if (expression.NodeType == ExpressionType.ArrayIndex) + { + var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath( + expression.Left, _queryModelVisitor.QueryCompilationContext, out _); + if (properties.Count == 0) + return base.VisitBinary(expression); + var lastPropertyType = properties[properties.Count - 1].ClrType; + if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1) + { + var left = Visit(expression.Left); + var right = Visit(expression.Right); + + return left != null && right != null + ? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right) + : null; + } + } + + return base.VisitBinary(expression); + } + + /// + /// Visits a and attempts to translate a '= ANY' expression. + /// + /// The expression to visit. + /// + /// An '= ANY' expression or null. + /// + [CanBeNull] + protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression expression) + { var subQueryModel = expression.QueryModel; var fromExpression = subQueryModel.MainFromClause.FromExpression; var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath( - fromExpression, _queryModelVisitor.QueryCompilationContext, out var qsre); + fromExpression, _queryModelVisitor.QueryCompilationContext, out _); if (properties.Count == 0) return null; @@ -76,33 +120,59 @@ protected override Expression VisitSubQuery(SubQueryExpression expression) { var containsItem = Visit(contains.Item); if (containsItem != null) - return new ArrayAnyExpression(containsItem, Visit(fromExpression)); + return new CustomArrayExpression(containsItem, Visit(fromExpression), "=", true); } } return null; } - protected override Expression VisitBinary(BinaryExpression expression) + /// + /// Visits a and attempts to translate a 'LIKE ANY' or 'ILIKE ANY' expression. + /// + /// The expression to visit. + /// + /// A 'LIKE ANY' or 'ILIKE ANY' expression or null. + /// + [CanBeNull] + protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expression) { - if (expression.NodeType == ExpressionType.ArrayIndex) + var queryModel = expression.QueryModel; + var results = queryModel.ResultOperators; + + if (results.Count != 1 || !(results[0] is AnyResultOperator)) + return null; + + var bodyClauses = queryModel.BodyClauses; + + if (bodyClauses.Count != 1 || + !(bodyClauses[0] is WhereClause whereClause) || + !(whereClause.Predicate is MethodCallExpression methodCallExpression)) + return null; + + if (!(Visit(methodCallExpression.Object ?? methodCallExpression.Arguments[1]) is Expression instance)) + return null; + + if (!(Visit(queryModel.MainFromClause.FromExpression) is Expression source)) + return null; + + switch (methodCallExpression.Method.Name) { - var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath( - expression.Left, _queryModelVisitor.QueryCompilationContext, out var qsre); - if (properties.Count == 0) - return base.VisitBinary(expression); - var lastPropertyType = properties[properties.Count - 1].ClrType; - if (lastPropertyType.IsArray && lastPropertyType.GetArrayRank() == 1) - { - var left = Visit(expression.Left); - var right = Visit(expression.Right); + case "StartsWith": + return new CustomArrayExpression(instance, source, "LIKE", true); - return left != null && right != null - ? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right) - : null; - } + case "EndsWith": + return new CustomArrayExpression(instance, source, "LIKE", true); + + case "Like": + return new CustomArrayExpression(instance, source, "LIKE", true); + + case "ILike": + return new CustomArrayExpression(instance, source, "ILIKE", true); + + default: + return null; } - return base.VisitBinary(expression); } } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs deleted file mode 100644 index b3826dd7f..000000000 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyExpression.cs +++ /dev/null @@ -1,167 +0,0 @@ -#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.Diagnostics; -using System.Linq.Expressions; -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal -{ - /// - /// Represents a PostgreSQL ANY expression (e.g. scalar = ANY (array)) - /// - /// - /// See https://www.postgresql.org/docs/current/static/functions-comparisons.html - /// - public class ArrayAnyExpression : Expression - { - /// - /// Creates a new instance of InExpression. - /// - /// The operand. - /// The array. - public ArrayAnyExpression( - [NotNull] Expression operand, - [NotNull] Expression array) - { - Check.NotNull(operand, nameof(operand)); - Check.NotNull(array, nameof(array)); - Debug.Assert(array.Type.IsArray); - - Operand = operand; - Array = array; - } - - /// - /// Gets the operand. - /// - /// - /// The operand. - /// - public virtual Expression Operand { get; } - - /// - /// Gets the array. - /// - /// - /// The array. - /// - public virtual Expression Array { get; } - - /// - /// Returns the node type of this . (Inherited from .) - /// - /// The that represents this expression. - public override ExpressionType NodeType => ExpressionType.Extension; - - /// - /// Gets the static type of the expression that this represents. (Inherited from .) - /// - /// The that represents the static type of the expression. - public override Type Type => typeof(bool); - - /// - /// Dispatches to the specific visit method for this node type. - /// - protected override Expression Accept(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - return visitor is NpgsqlQuerySqlGenerator npsgqlGenerator - ? npsgqlGenerator.VisitArrayAny(this) - : base.Accept(visitor); - } - - /// - /// Reduces the node and then calls the method passing the - /// reduced expression. - /// Throws an exception if the node isn't reducible. - /// - /// An instance of . - /// The expression being visited, or an expression which should replace it in the tree. - /// - /// Override this method to provide logic to walk the node's children. - /// A typical implementation will call visitor.Visit on each of its - /// children, and if any of them change, should return a new copy of - /// itself with the modified children. - /// - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - var newOperand = visitor.Visit(Operand); - var newArray = visitor.Visit(Array); - - return newOperand != Operand || newArray != Array - ? new ArrayAnyExpression(newOperand, newArray) - : this; - } - - /// - /// Tests if this object is considered equal to another. - /// - /// The object to compare with the current object. - /// - /// true if the objects are considered equal, false if they are not. - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj.GetType() == GetType() && Equals((ArrayAnyExpression)obj); - } - - bool Equals(ArrayAnyExpression other) - => Operand.Equals(other.Operand) && Array.Equals(other.Array); - - /// - /// Returns a hash code for this object. - /// - /// - /// A hash code for this object. - /// - public override int GetHashCode() - { - unchecked - { - return (Operand.GetHashCode() * 397) ^ Array.GetHashCode(); - } - } - - /// - /// Creates a representation of the Expression. - /// - /// A representation of the Expression. - public override string ToString() => $"{Operand} = ANY ({Array})"; - } -} diff --git a/src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs new file mode 100644 index 000000000..0cbe29763 --- /dev/null +++ b/src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs @@ -0,0 +1,132 @@ +#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.Expressions; +using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal +{ + /// + /// Represents a PostgreSQL ANY expression (e.g. 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}')). + /// + /// + /// See https://www.postgresql.org/docs/current/static/functions-comparisons.html + /// + public class CustomArrayExpression : Expression, IEquatable + { + /// + /// True if the operator is modified by ANY; otherwise, false to modify with ALL. + /// + readonly bool _any; + + /// + public override ExpressionType NodeType { get; } = ExpressionType.Extension; + + /// + public override Type Type { get; } = typeof(bool); + + /// + /// The value to test against the . + /// + public virtual Expression Operand { get; } + + /// + /// The collection of values or patterns to test for the . + /// + public virtual Expression Collection { get; } + + /// + /// The operator. + /// + public virtual string Operator { get; } + + /// + /// The type of the operator expression (ANY or ALL). + /// + public virtual string OperatorType => _any ? "ANY" : "ALL"; + + /// + /// Constructs a . + /// + /// The value to find. + /// The collection to search. + /// The operator symbol to the array expression. + /// True for ANY; false for ALL. + /// + public CustomArrayExpression([NotNull] Expression operand, [NotNull] Expression collection, [NotNull] string operatorSymbol, bool any) + { + Check.NotNull(operand, nameof(operand)); + Check.NotNull(collection, nameof(collection)); + + Operand = operand; + Operator = operatorSymbol; + Collection = collection; + _any = any; + } + + /// + protected override Expression Accept(ExpressionVisitor visitor) + => visitor is NpgsqlQuerySqlGenerator npsgqlGenerator + ? npsgqlGenerator.VisitArrayOperator(this) + : base.Accept(visitor); + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + if (!(visitor.Visit(Operand) is Expression operand)) + throw new ArgumentException($"The {nameof(operand)} of a {nameof(CustomArrayExpression)} cannot be null."); + + if (!(visitor.Visit(Collection) is Expression collection)) + throw new ArgumentException($"The {nameof(collection)} of a {nameof(CustomArrayExpression)} cannot be null."); + + return + operand == Operand && collection == Collection + ? this + : new CustomArrayExpression(operand, collection, Operator, _any); + } + + /// + public override bool Equals(object obj) + => obj is CustomArrayExpression likeAnyExpression && Equals(likeAnyExpression); + + /// + public bool Equals(CustomArrayExpression other) + => Operand.Equals(other?.Operand) && Collection.Equals(other?.Collection); + + /// + public override int GetHashCode() + => unchecked((397 * Operand.GetHashCode()) ^ + (397 * Operator.GetHashCode()) ^ + (397 * OperatorType.GetHashCode()) ^ + (397 * Collection.GetHashCode())); + + /// + public override string ToString() + => $"{Operand} {Operator} {OperatorType} ({Collection})"; + } +} diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 287764238..03d1d133f 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -41,8 +41,9 @@ public class NpgsqlQuerySqlGenerator : DefaultQuerySqlGenerator { readonly bool _reverseNullOrderingEnabled; - protected override string TypedTrueLiteral => "TRUE::bool"; - protected override string TypedFalseLiteral => "FALSE::bool"; + protected override string TypedTrueLiteral { get; } = "TRUE::bool"; + + protected override string TypedFalseLiteral { get; } = "FALSE::bool"; public NpgsqlQuerySqlGenerator( [NotNull] QuerySqlGeneratorDependencies dependencies, @@ -176,23 +177,34 @@ protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression) Sql.Append(']'); } - public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression) + /// + /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). + /// + public Expression VisitArrayOperator(CustomArrayExpression arrayExpression) { - Visit(arrayAnyExpression.Operand); - Sql.Append(" = ANY ("); - Visit(arrayAnyExpression.Array); - Sql.Append(")"); - return arrayAnyExpression; + Visit(arrayExpression.Operand); + Sql.Append(' '); + Sql.Append(arrayExpression.Operator); + Sql.Append(' '); + Sql.Append(arrayExpression.OperatorType); + Sql.Append(" ("); + Visit(arrayExpression.Collection); + Sql.Append(')'); + return arrayExpression; } - // PostgreSQL array indexing is 1-based. If the index happens to be a constant, - // just increment it. Otherwise, append a +1 in the SQL. - Expression GenerateOneBasedIndexExpression(Expression expression) + /// + /// PostgreSQL array indexing is 1-based. If the index happens to be a constant, + /// just increment it. Otherwise, append a +1 in the SQL. + /// + static Expression GenerateOneBasedIndexExpression(Expression expression) => expression is ConstantExpression constantExpression ? Expression.Constant(Convert.ToInt32(constantExpression.Value) + 1) : (Expression)Expression.Add(expression, Expression.Constant(1)); - // See http://www.postgresql.org/docs/current/static/functions-matching.html + /// + /// See: http://www.postgresql.org/docs/current/static/functions-matching.html + /// public Expression VisitRegexMatch([NotNull] RegexMatchExpression regexMatchExpression) { Check.NotNull(regexMatchExpression, nameof(regexMatchExpression)); diff --git a/test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs new file mode 100644 index 000000000..0062b9a80 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs @@ -0,0 +1,243 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Xunit; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class LikeAnyQueryNpgsqlTest : IClassFixture + { + #region Setup + + /// + /// Provides resources for unit tests. + /// + LikeAnyQueryNpgsqlFixture Fixture { get; } + + /// + /// Initializes resources for unit tests. + /// + /// The fixture of resources for testing. + public LikeAnyQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + } + + #endregion + + #region Tests + + [Fact] + public void Array_Any_StartsWith() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.Any(y => x.Animal.StartsWith(y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE"); + } + } + + [Fact] + public void Array_Any_EndsWith() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.Any(y => x.Animal.EndsWith(y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE"); + } + } + + [Fact] + public void Array_Any_Like() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.Any(y => EF.Functions.Like(x.Animal, y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE"); + } + } + + [Fact] + public void Array_Any_ILike() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.Any(y => EF.Functions.ILike(x.Animal, y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" ILIKE ANY (@__collection_0) = TRUE"); + } + } + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing LIKE ANY expressions. + /// + public class LikeAnyQueryNpgsqlFixture : IDisposable + { + /// + /// The used for testing. + /// + private readonly NpgsqlTestStore _testStore; + + /// + /// The used for testing. + /// + private readonly DbContextOptions _options; + + /// + /// The logger factory used for testing. + /// + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } + + /// + /// Initializes a . + /// + public LikeAnyQueryNpgsqlFixture() + { + TestSqlLoggerFactory = new TestSqlLoggerFactory(); + + _testStore = NpgsqlTestStore.CreateScratch(); + + _options = + new DbContextOptionsBuilder() + .UseNpgsql(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkNpgsql() + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (LikeAnyContext context = CreateContext()) + { + context.Database.EnsureCreated(); + + context.LikeAnyTestEntities + .AddRange( + new LikeAnyTestEntity + { + Id = 1, + Animal = "cat" + }, + new LikeAnyTestEntity + { + Id = 2, + Animal = "dog" + }, + new LikeAnyTestEntity + { + Id = 3, + Animal = "turtle" + }, + new LikeAnyTestEntity + { + Id = 4, + Animal = "bird" + }); + + context.SaveChanges(); + } + } + + /// + /// Creates a new . + /// + /// + /// A for testing. + /// + public LikeAnyContext CreateContext() + { + return new LikeAnyContext(_options); + } + + /// + public void Dispose() + { + _testStore.Dispose(); + } + } + + /// + /// Represents an entity suitable for testing LIKE ANY expressions. + /// + public class LikeAnyTestEntity + { + /// + /// The primary key. + /// + [Key] + public int Id { get; set; } + + /// + /// The value. + /// + public string Animal { get; set; } + } + + /// + /// Represents a database suitable for testing range operators. + /// + public class LikeAnyContext : DbContext + { + /// + /// Represents a set of entities with a string property. + /// + public DbSet LikeAnyTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public LikeAnyContext(DbContextOptions options) : base(options) { } + } + + #endregion + + #region Helpers + + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + public void AssertContainsSql(string sql) + { + Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + } + + #endregion + } +} From 4ea4fba76e887b57e52fc7ce3a2b057e796ece6b Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sun, 27 May 2018 10:30:13 -0400 Subject: [PATCH 2/4] Adjusts naming for precision and refactors to use ArrayComparisonType enum - Recognizes `AnyResultOperator` vs `AllResultOperator` --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 53 +++++------ ...Expression.cs => ArrayAnyAllExpression.cs} | 88 ++++++++++++------- .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 12 +-- 3 files changed, 89 insertions(+), 64 deletions(-) rename src/EFCore.PG/Query/Expressions/Internal/{CustomArrayExpression.cs => ArrayAnyAllExpression.cs} (57%) diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index bbdf936ea..fbe2f9c86 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -53,19 +53,7 @@ public NpgsqlSqlTranslatingExpressionVisitor( } protected override Expression VisitSubQuery(SubQueryExpression expression) - { - // Prefer the default EF Core translation if one exists - if (base.VisitSubQuery(expression) is Expression result) - return result; - - if (VisitLikeAny(expression) is Expression likeAny) - return likeAny; - - if (VisitEqualsAny(expression) is Expression equalsAny) - return equalsAny; - - return null; - } + => base.VisitSubQuery(expression) ?? VisitLikeAny(expression) ?? VisitEqualsAny(expression); protected override Expression VisitBinary(BinaryExpression expression) { @@ -120,7 +108,7 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express { var containsItem = Visit(contains.Item); if (containsItem != null) - return new CustomArrayExpression(containsItem, Visit(fromExpression), "=", true); + return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, Visit(fromExpression)); } } @@ -140,39 +128,52 @@ protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expressio var queryModel = expression.QueryModel; var results = queryModel.ResultOperators; - if (results.Count != 1 || !(results[0] is AnyResultOperator)) + if (results.Count != 1) + return null; + + ArrayComparisonType comparisonType; + switch (results[0]) + { + case AnyResultOperator _: + comparisonType = ArrayComparisonType.ANY; + break; + + case AllResultOperator _: + comparisonType = ArrayComparisonType.ALL; + break; + + default: return null; + } var bodyClauses = queryModel.BodyClauses; if (bodyClauses.Count != 1 || !(bodyClauses[0] is WhereClause whereClause) || - !(whereClause.Predicate is MethodCallExpression methodCallExpression)) - return null; - - if (!(Visit(methodCallExpression.Object ?? methodCallExpression.Arguments[1]) is Expression instance)) + !(whereClause.Predicate is MethodCallExpression call)) return null; - if (!(Visit(queryModel.MainFromClause.FromExpression) is Expression source)) - return null; + Expression source = queryModel.MainFromClause.FromExpression; - switch (methodCallExpression.Method.Name) + // ReSharper disable AssignNullToNotNullAttribute + switch (call.Method.Name) { case "StartsWith": - return new CustomArrayExpression(instance, source, "LIKE", true); + return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); case "EndsWith": - return new CustomArrayExpression(instance, source, "LIKE", true); + return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); case "Like": - return new CustomArrayExpression(instance, source, "LIKE", true); + return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); case "ILike": - return new CustomArrayExpression(instance, source, "ILIKE", true); + return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source)); default: return null; } + // ReSharper restore AssignNullToNotNullAttribute } } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs similarity index 57% rename from src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs rename to src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index 0cbe29763..276b6803e 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/CustomArrayExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -32,18 +32,16 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal { /// - /// Represents a PostgreSQL ANY expression (e.g. 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}')). + /// Represents a PostgreSQL array ANY or ALL expression. /// + /// + /// 1 = ANY ('{0,1,2}'), 'cat' LIKE ANY ('{a%,b%,c%}') + /// /// /// See https://www.postgresql.org/docs/current/static/functions-comparisons.html /// - public class CustomArrayExpression : Expression, IEquatable + public class ArrayAnyAllExpression : Expression, IEquatable { - /// - /// True if the operator is modified by ANY; otherwise, false to modify with ALL. - /// - readonly bool _any; - /// public override ExpressionType NodeType { get; } = ExpressionType.Extension; @@ -51,14 +49,14 @@ public class CustomArrayExpression : Expression, IEquatable - /// The value to test against the . + /// The value to test against the . /// public virtual Expression Operand { get; } /// - /// The collection of values or patterns to test for the . + /// The array of values or patterns to test for the . /// - public virtual Expression Collection { get; } + public virtual Expression Array { get; } /// /// The operator. @@ -66,67 +64,93 @@ public class CustomArrayExpression : Expression, IEquatable - /// The type of the operator expression (ANY or ALL). + /// The comparison type. /// - public virtual string OperatorType => _any ? "ANY" : "ALL"; + public virtual ArrayComparisonType ArrayComparisonType { get; } /// - /// Constructs a . + /// Constructs a . /// - /// The value to find. - /// The collection to search. + /// The comparison operator. /// The operator symbol to the array expression. - /// True for ANY; false for ALL. + /// The value to find. + /// The array to search. /// - public CustomArrayExpression([NotNull] Expression operand, [NotNull] Expression collection, [NotNull] string operatorSymbol, bool any) + public ArrayAnyAllExpression( + ArrayComparisonType arrayComparisonType, + [NotNull] string operatorSymbol, + [NotNull] Expression operand, + [NotNull] Expression array) { Check.NotNull(operand, nameof(operand)); - Check.NotNull(collection, nameof(collection)); + Check.NotNull(array, nameof(array)); + Check.NotNull(array, nameof(operatorSymbol)); Operand = operand; Operator = operatorSymbol; - Collection = collection; - _any = any; + Array = array; + ArrayComparisonType = arrayComparisonType; } /// protected override Expression Accept(ExpressionVisitor visitor) => visitor is NpgsqlQuerySqlGenerator npsgqlGenerator - ? npsgqlGenerator.VisitArrayOperator(this) + ? npsgqlGenerator.VisitArrayAnyAll(this) : base.Accept(visitor); /// protected override Expression VisitChildren(ExpressionVisitor visitor) { if (!(visitor.Visit(Operand) is Expression operand)) - throw new ArgumentException($"The {nameof(operand)} of a {nameof(CustomArrayExpression)} cannot be null."); + throw new ArgumentException($"The {nameof(operand)} of a {nameof(ArrayAnyAllExpression)} cannot be null."); - if (!(visitor.Visit(Collection) is Expression collection)) - throw new ArgumentException($"The {nameof(collection)} of a {nameof(CustomArrayExpression)} cannot be null."); + if (!(visitor.Visit(Array) is Expression collection)) + throw new ArgumentException($"The {nameof(collection)} of a {nameof(ArrayAnyAllExpression)} cannot be null."); return - operand == Operand && collection == Collection + operand == Operand && collection == Array ? this - : new CustomArrayExpression(operand, collection, Operator, _any); + : new ArrayAnyAllExpression(ArrayComparisonType, Operator, operand, collection); } /// public override bool Equals(object obj) - => obj is CustomArrayExpression likeAnyExpression && Equals(likeAnyExpression); + => obj is ArrayAnyAllExpression likeAnyExpression && Equals(likeAnyExpression); /// - public bool Equals(CustomArrayExpression other) - => Operand.Equals(other?.Operand) && Collection.Equals(other?.Collection); + public bool Equals(ArrayAnyAllExpression other) + => Operand.Equals(other?.Operand) && + Operator.Equals(other?.Operator) && + ArrayComparisonType.Equals(other?.ArrayComparisonType) && + Array.Equals(other?.Array); /// public override int GetHashCode() => unchecked((397 * Operand.GetHashCode()) ^ (397 * Operator.GetHashCode()) ^ - (397 * OperatorType.GetHashCode()) ^ - (397 * Collection.GetHashCode())); + (397 * ArrayComparisonType.GetHashCode()) ^ + (397 * Array.GetHashCode())); /// public override string ToString() - => $"{Operand} {Operator} {OperatorType} ({Collection})"; + => $"{Operand} {Operator} {ArrayComparisonType.ToString()} ({Array})"; + } + + /// + /// Represents whether an array comparison is ANY or ALL. + /// + public enum ArrayComparisonType : byte + { + // ReSharper disable once InconsistentNaming + /// + /// Represents an ANY array comparison. + /// + ANY = 0, + + // ReSharper disable once InconsistentNaming + /// + /// Represents an ALL array comparison. + /// + ALL = 1 << 0 } } diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 03d1d133f..6396485c3 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -180,17 +180,17 @@ protected virtual void VisitArrayIndex([NotNull] BinaryExpression expression) /// /// Produces expressions like: 1 = ANY ('{0,1,2}') or 'cat' LIKE ANY ('{a%,b%,c%}'). /// - public Expression VisitArrayOperator(CustomArrayExpression arrayExpression) + public Expression VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression) { - Visit(arrayExpression.Operand); + Visit(arrayAnyAllExpression.Operand); Sql.Append(' '); - Sql.Append(arrayExpression.Operator); + Sql.Append(arrayAnyAllExpression.Operator); Sql.Append(' '); - Sql.Append(arrayExpression.OperatorType); + Sql.Append(arrayAnyAllExpression.ArrayComparisonType.ToString()); Sql.Append(" ("); - Visit(arrayExpression.Collection); + Visit(arrayAnyAllExpression.Array); Sql.Append(')'); - return arrayExpression; + return arrayAnyAllExpression; } /// From 5b1f13898104ab306c57e42088b7336644a47516 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Sun, 27 May 2018 10:30:13 -0400 Subject: [PATCH 3/4] Disables (temporarily) StartsWith(...) and EndsWith(...) support - Details need to be worked out for `StartsWith(...)` and `EndsWith(...)`. - But this should not block standard `LIKE`/`ILIKE` `ANY`/`ALL` translation. - Added tests + fixes for `ALL` comparisons. --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 46 +++++--- .../Internal/ArrayAnyAllExpression.cs | 14 +-- ...qlTest.cs => LikeAnyAllQueryNpgsqlTest.cs} | 109 ++++++++++++++---- 3 files changed, 124 insertions(+), 45 deletions(-) rename test/EFCore.PG.FunctionalTests/Query/{LikeAnyQueryNpgsqlTest.cs => LikeAnyAllQueryNpgsqlTest.cs} (71%) diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index fbe2f9c86..32deaf7f7 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -31,6 +31,7 @@ using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Remotion.Linq; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; @@ -53,7 +54,7 @@ public NpgsqlSqlTranslatingExpressionVisitor( } protected override Expression VisitSubQuery(SubQueryExpression expression) - => base.VisitSubQuery(expression) ?? VisitLikeAny(expression) ?? VisitEqualsAny(expression); + => base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression); protected override Expression VisitBinary(BinaryExpression expression) { @@ -116,14 +117,14 @@ protected virtual Expression VisitEqualsAny([NotNull] SubQueryExpression express } /// - /// Visits a and attempts to translate a 'LIKE ANY' or 'ILIKE ANY' expression. + /// Visits a and attempts to translate a LIKE/ILIKE ANY/ALL expression. /// /// The expression to visit. /// - /// A 'LIKE ANY' or 'ILIKE ANY' expression or null. + /// A 'LIKE ANY', 'LIKE ALL', 'ILIKE ANY', or 'ILIKE ALL' expression or null. /// [CanBeNull] - protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expression) + protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expression) { var queryModel = expression.QueryModel; var results = queryModel.ResultOperators; @@ -132,37 +133,37 @@ protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expressio return null; ArrayComparisonType comparisonType; + MethodCallExpression call; switch (results[0]) { case AnyResultOperator _: comparisonType = ArrayComparisonType.ANY; + call = ComputeAny(queryModel); break; - case AllResultOperator _: + case AllResultOperator allResult: comparisonType = ArrayComparisonType.ALL; + call = allResult.Predicate as MethodCallExpression; break; default: return null; } - var bodyClauses = queryModel.BodyClauses; - - if (bodyClauses.Count != 1 || - !(bodyClauses[0] is WhereClause whereClause) || - !(whereClause.Predicate is MethodCallExpression call)) + if (call is null) return null; - Expression source = queryModel.MainFromClause.FromExpression; + var source = queryModel.MainFromClause.FromExpression; // ReSharper disable AssignNullToNotNullAttribute switch (call.Method.Name) { - case "StartsWith": - return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); - - case "EndsWith": - return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); +// TODO: Can StartsWith and EndsWith be implemented correctly? +// case "StartsWith": +// return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); +// +// case "EndsWith": +// return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); case "Like": return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); @@ -175,5 +176,18 @@ protected virtual Expression VisitLikeAny([NotNull] SubQueryExpression expressio } // ReSharper restore AssignNullToNotNullAttribute } + + [CanBeNull] + static MethodCallExpression ComputeAny([NotNull] QueryModel queryModel) + { + var bodyClauses = queryModel.BodyClauses; + + if (bodyClauses.Count == 1 && + bodyClauses[0] is WhereClause whereClause && + whereClause.Predicate is MethodCallExpression call) + return call; + + return null; + } } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs index 276b6803e..04054d3a4 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs @@ -71,7 +71,7 @@ public class ArrayAnyAllExpression : Expression, IEquatable /// Constructs a . /// - /// The comparison operator. + /// The comparison type. /// The operator symbol to the array expression. /// The value to find. /// The array to search. @@ -82,14 +82,14 @@ public ArrayAnyAllExpression( [NotNull] Expression operand, [NotNull] Expression array) { + Check.NotNull(array, nameof(operatorSymbol)); Check.NotNull(operand, nameof(operand)); Check.NotNull(array, nameof(array)); - Check.NotNull(array, nameof(operatorSymbol)); - Operand = operand; + ArrayComparisonType = arrayComparisonType; Operator = operatorSymbol; + Operand = operand; Array = array; - ArrayComparisonType = arrayComparisonType; } /// @@ -139,18 +139,18 @@ public override string ToString() /// /// Represents whether an array comparison is ANY or ALL. /// - public enum ArrayComparisonType : byte + public enum ArrayComparisonType { // ReSharper disable once InconsistentNaming /// /// Represents an ANY array comparison. /// - ANY = 0, + ANY, // ReSharper disable once InconsistentNaming /// /// Represents an ALL array comparison. /// - ALL = 1 << 0 + ALL } } diff --git a/test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs similarity index 71% rename from test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs index 0062b9a80..fa6744f9b 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LikeAnyQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -10,7 +9,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { - public class LikeAnyQueryNpgsqlTest : IClassFixture + public class LikeAnyAllQueryNpgsqlTest : IClassFixture { #region Setup @@ -23,7 +22,7 @@ public class LikeAnyQueryNpgsqlTest : IClassFixture /// The fixture of resources for testing. - public LikeAnyQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture) + public LikeAnyAllQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); @@ -31,9 +30,9 @@ public LikeAnyQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture) #endregion - #region Tests + #region StartsWithTests - [Fact] + [Fact(Skip = "StartsWith not currently supported")] public void Array_Any_StartsWith() { using (LikeAnyContext context = Fixture.CreateContext()) @@ -49,7 +48,27 @@ public void Array_Any_StartsWith() } } - [Fact] + [Fact(Skip = "StartsWith not currently supported")] + public void Array_All_StartsWith() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.All(y => x.Animal.StartsWith(y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE"); + } + } + + #endregion + + #region EndsWithTests + + [Fact(Skip = "EndsWith not currently supported")] public void Array_Any_EndsWith() { using (LikeAnyContext context = Fixture.CreateContext()) @@ -65,6 +84,26 @@ public void Array_Any_EndsWith() } } + [Fact(Skip = "EndsWith not currently supported")] + public void Array_All_EndsWith() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.All(y => x.Animal.EndsWith(y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE"); + } + } + + #endregion + + #region LikeTests + [Fact] public void Array_Any_Like() { @@ -82,12 +121,32 @@ public void Array_Any_Like() } [Fact] - public void Array_Any_ILike() + public void Array_All_Like() { using (LikeAnyContext context = Fixture.CreateContext()) { var collection = new string[] { "a", "b", "c" }; + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.All(y => EF.Functions.Like(x.Animal, y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE"); + } + } + + #endregion + + #region ILikeTests + + [Fact] + public void Array_Any_ILike() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c%" }; + LikeAnyTestEntity[] _ = context.LikeAnyTestEntities .Where(x => collection.Any(y => EF.Functions.ILike(x.Animal, y))) @@ -97,6 +156,22 @@ public void Array_Any_ILike() } } + [Fact] + public void Array_All_ILike() + { + using (LikeAnyContext context = Fixture.CreateContext()) + { + var collection = new string[] { "a", "b", "c%" }; + + LikeAnyTestEntity[] _ = + context.LikeAnyTestEntities + .Where(x => collection.All(y => EF.Functions.ILike(x.Animal, y))) + .ToArray(); + + AssertContainsSql("WHERE x.\"Animal\" ILIKE ALL (@__collection_0) = TRUE"); + } + } + #endregion #region Fixtures @@ -109,12 +184,12 @@ public class LikeAnyQueryNpgsqlFixture : IDisposable /// /// The used for testing. /// - private readonly NpgsqlTestStore _testStore; + readonly NpgsqlTestStore _testStore; /// /// The used for testing. /// - private readonly DbContextOptions _options; + readonly DbContextOptions _options; /// /// The logger factory used for testing. @@ -177,16 +252,10 @@ public LikeAnyQueryNpgsqlFixture() /// /// A for testing. /// - public LikeAnyContext CreateContext() - { - return new LikeAnyContext(_options); - } + public LikeAnyContext CreateContext() => new LikeAnyContext(_options); /// - public void Dispose() - { - _testStore.Dispose(); - } + public void Dispose() => _testStore.Dispose(); } /// @@ -197,7 +266,6 @@ public class LikeAnyTestEntity /// /// The primary key. /// - [Key] public int Id { get; set; } /// @@ -233,10 +301,7 @@ public LikeAnyContext(DbContextOptions options) : base(options) { } /// Asserts that the SQL fragment appears in the logs. /// /// The SQL statement or fragment to search for in the logs. - public void AssertContainsSql(string sql) - { - Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); - } + public void AssertContainsSql(string sql) => Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); #endregion } From cafe41fe7177beac34977f6d4a88ce0acdfabae4 Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 29 May 2018 10:01:30 -0400 Subject: [PATCH 4/4] Replaces name switch with MethodInfo comparison Also removes future features that were commented/skipped. --- .../NpgsqlSqlTranslatingExpressionVisitor.cs | 83 ++++++++++++------- .../Query/LikeAnyAllQueryNpgsqlTest.cs | 72 ---------------- 2 files changed, 55 insertions(+), 100 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index 32deaf7f7..c50263d19 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -25,13 +25,14 @@ using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; -using Remotion.Linq; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; @@ -40,8 +41,42 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors { public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor { - readonly RelationalQueryModelVisitor _queryModelVisitor; + /// + /// The for . + /// + [NotNull] static readonly MethodInfo Like2MethodInfo = + typeof(DbFunctionsExtensions) + .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) }); + + /// + /// The for . + /// + [NotNull] static readonly MethodInfo Like3MethodInfo = + typeof(DbFunctionsExtensions) + .GetRuntimeMethod(nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) }); + + // ReSharper disable once InconsistentNaming + /// + /// The for . + /// + [NotNull] static readonly MethodInfo ILike2MethodInfo = + typeof(NpgsqlDbFunctionsExtensions) + .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string) }); + + // ReSharper disable once InconsistentNaming + /// + /// The for . + /// + [NotNull] static readonly MethodInfo ILike3MethodInfo = + typeof(NpgsqlDbFunctionsExtensions) + .GetRuntimeMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) }); + + /// + /// The query model visitor. + /// + [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor; + /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, [NotNull] RelationalQueryModelVisitor queryModelVisitor, @@ -49,13 +84,13 @@ public NpgsqlSqlTranslatingExpressionVisitor( [CanBeNull] Expression topLevelPredicate = null, bool inProjection = false) : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) - { - _queryModelVisitor = queryModelVisitor; - } + => _queryModelVisitor = queryModelVisitor; + /// protected override Expression VisitSubQuery(SubQueryExpression expression) => base.VisitSubQuery(expression) ?? VisitLikeAnyAll(expression) ?? VisitEqualsAny(expression); + /// protected override Expression VisitBinary(BinaryExpression expression) { if (expression.NodeType == ExpressionType.ArrayIndex) @@ -128,6 +163,7 @@ protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expres { var queryModel = expression.QueryModel; var results = queryModel.ResultOperators; + var body = queryModel.BodyClauses; if (results.Count != 1) return null; @@ -138,7 +174,12 @@ protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expres { case AnyResultOperator _: comparisonType = ArrayComparisonType.ANY; - call = ComputeAny(queryModel); + call = + body.Count == 1 && + body[0] is WhereClause whereClause && + whereClause.Predicate is MethodCallExpression methocCall + ? methocCall + : null; break; case AllResultOperator allResult: @@ -156,19 +197,18 @@ protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expres var source = queryModel.MainFromClause.FromExpression; // ReSharper disable AssignNullToNotNullAttribute - switch (call.Method.Name) + switch (call.Method) { -// TODO: Can StartsWith and EndsWith be implemented correctly? -// case "StartsWith": -// return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); -// -// case "EndsWith": -// return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Object), Visit(source)); + case MethodInfo m when m == Like2MethodInfo: + return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); - case "Like": + case MethodInfo m when m == Like3MethodInfo: return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source)); - case "ILike": + case MethodInfo m when m == ILike2MethodInfo: + return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source)); + + case MethodInfo m when m == ILike3MethodInfo: return new ArrayAnyAllExpression(comparisonType, "ILIKE", Visit(call.Arguments[1]), Visit(source)); default: @@ -176,18 +216,5 @@ protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expres } // ReSharper restore AssignNullToNotNullAttribute } - - [CanBeNull] - static MethodCallExpression ComputeAny([NotNull] QueryModel queryModel) - { - var bodyClauses = queryModel.BodyClauses; - - if (bodyClauses.Count == 1 && - bodyClauses[0] is WhereClause whereClause && - whereClause.Predicate is MethodCallExpression call) - return call; - - return null; - } } } diff --git a/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs index fa6744f9b..92aa1d27d 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs @@ -30,78 +30,6 @@ public LikeAnyAllQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture) #endregion - #region StartsWithTests - - [Fact(Skip = "StartsWith not currently supported")] - public void Array_Any_StartsWith() - { - using (LikeAnyContext context = Fixture.CreateContext()) - { - var collection = new string[] { "a", "b", "c" }; - - LikeAnyTestEntity[] _ = - context.LikeAnyTestEntities - .Where(x => collection.Any(y => x.Animal.StartsWith(y))) - .ToArray(); - - AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE"); - } - } - - [Fact(Skip = "StartsWith not currently supported")] - public void Array_All_StartsWith() - { - using (LikeAnyContext context = Fixture.CreateContext()) - { - var collection = new string[] { "a", "b", "c" }; - - LikeAnyTestEntity[] _ = - context.LikeAnyTestEntities - .Where(x => collection.All(y => x.Animal.StartsWith(y))) - .ToArray(); - - AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE"); - } - } - - #endregion - - #region EndsWithTests - - [Fact(Skip = "EndsWith not currently supported")] - public void Array_Any_EndsWith() - { - using (LikeAnyContext context = Fixture.CreateContext()) - { - var collection = new string[] { "a", "b", "c" }; - - LikeAnyTestEntity[] _ = - context.LikeAnyTestEntities - .Where(x => collection.Any(y => x.Animal.EndsWith(y))) - .ToArray(); - - AssertContainsSql("WHERE x.\"Animal\" LIKE ANY (@__collection_0) = TRUE"); - } - } - - [Fact(Skip = "EndsWith not currently supported")] - public void Array_All_EndsWith() - { - using (LikeAnyContext context = Fixture.CreateContext()) - { - var collection = new string[] { "a", "b", "c" }; - - LikeAnyTestEntity[] _ = - context.LikeAnyTestEntities - .Where(x => collection.All(y => x.Animal.EndsWith(y))) - .ToArray(); - - AssertContainsSql("WHERE x.\"Animal\" LIKE ALL (@__collection_0) = TRUE"); - } - } - - #endregion - #region LikeTests [Fact]