diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs
index 9945b5f0a..c50263d19 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,16 +20,20 @@
// 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;
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.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
@@ -36,8 +41,42 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionVisitors
{
public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVisitor
{
- private 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,
@@ -45,22 +84,51 @@ 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)
{
- // Prefer the default EF Core translation if one exists
- var result = base.VisitSubQuery(expression);
- if (result != null)
- return result;
+ 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 +144,77 @@ protected override Expression VisitSubQuery(SubQueryExpression expression)
{
var containsItem = Visit(contains.Item);
if (containsItem != null)
- return new ArrayAnyExpression(containsItem, Visit(fromExpression));
+ return new ArrayAnyAllExpression(ArrayComparisonType.ANY, "=", containsItem, Visit(fromExpression));
}
}
return null;
}
- protected override Expression VisitBinary(BinaryExpression expression)
+ ///
+ /// Visits a and attempts to translate a LIKE/ILIKE ANY/ALL expression.
+ ///
+ /// The expression to visit.
+ ///
+ /// A 'LIKE ANY', 'LIKE ALL', 'ILIKE ANY', or 'ILIKE ALL' expression or null.
+ ///
+ [CanBeNull]
+ protected virtual Expression VisitLikeAnyAll([NotNull] SubQueryExpression expression)
{
- if (expression.NodeType == ExpressionType.ArrayIndex)
- {
- 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);
+ var queryModel = expression.QueryModel;
+ var results = queryModel.ResultOperators;
+ var body = queryModel.BodyClauses;
- return left != null && right != null
- ? Expression.MakeBinary(ExpressionType.ArrayIndex, left, right)
+ if (results.Count != 1)
+ return null;
+
+ ArrayComparisonType comparisonType;
+ MethodCallExpression call;
+ switch (results[0])
+ {
+ case AnyResultOperator _:
+ comparisonType = ArrayComparisonType.ANY;
+ call =
+ body.Count == 1 &&
+ body[0] is WhereClause whereClause &&
+ whereClause.Predicate is MethodCallExpression methocCall
+ ? methocCall
: null;
- }
+ break;
+
+ case AllResultOperator allResult:
+ comparisonType = ArrayComparisonType.ALL;
+ call = allResult.Predicate as MethodCallExpression;
+ break;
+
+ default:
+ return null;
}
- return base.VisitBinary(expression);
+
+ if (call is null)
+ return null;
+
+ var source = queryModel.MainFromClause.FromExpression;
+
+ // ReSharper disable AssignNullToNotNullAttribute
+ switch (call.Method)
+ {
+ case MethodInfo m when m == Like2MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source));
+
+ case MethodInfo m when m == Like3MethodInfo:
+ return new ArrayAnyAllExpression(comparisonType, "LIKE", Visit(call.Arguments[1]), Visit(source));
+
+ 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:
+ return null;
+ }
+ // ReSharper restore AssignNullToNotNullAttribute
}
}
}
diff --git a/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs
new file mode 100644
index 000000000..04054d3a4
--- /dev/null
+++ b/src/EFCore.PG/Query/Expressions/Internal/ArrayAnyAllExpression.cs
@@ -0,0 +1,156 @@
+#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 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 ArrayAnyAllExpression : Expression, IEquatable
+ {
+ ///
+ 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 array of values or patterns to test for the .
+ ///
+ public virtual Expression Array { get; }
+
+ ///
+ /// The operator.
+ ///
+ public virtual string Operator { get; }
+
+ ///
+ /// The comparison type.
+ ///
+ public virtual ArrayComparisonType ArrayComparisonType { get; }
+
+ ///
+ /// Constructs a .
+ ///
+ /// The comparison type.
+ /// The operator symbol to the array expression.
+ /// The value to find.
+ /// The array to search.
+ ///
+ public ArrayAnyAllExpression(
+ ArrayComparisonType arrayComparisonType,
+ [NotNull] string operatorSymbol,
+ [NotNull] Expression operand,
+ [NotNull] Expression array)
+ {
+ Check.NotNull(array, nameof(operatorSymbol));
+ Check.NotNull(operand, nameof(operand));
+ Check.NotNull(array, nameof(array));
+
+ ArrayComparisonType = arrayComparisonType;
+ Operator = operatorSymbol;
+ Operand = operand;
+ Array = array;
+ }
+
+ ///
+ protected override Expression Accept(ExpressionVisitor visitor)
+ => visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
+ ? 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(ArrayAnyAllExpression)} 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 == Array
+ ? this
+ : new ArrayAnyAllExpression(ArrayComparisonType, Operator, operand, collection);
+ }
+
+ ///
+ public override bool Equals(object obj)
+ => obj is ArrayAnyAllExpression likeAnyExpression && Equals(likeAnyExpression);
+
+ ///
+ 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 * ArrayComparisonType.GetHashCode()) ^
+ (397 * Array.GetHashCode()));
+
+ ///
+ public override string ToString()
+ => $"{Operand} {Operator} {ArrayComparisonType.ToString()} ({Array})";
+ }
+
+ ///
+ /// Represents whether an array comparison is ANY or ALL.
+ ///
+ public enum ArrayComparisonType
+ {
+ // ReSharper disable once InconsistentNaming
+ ///
+ /// Represents an ANY array comparison.
+ ///
+ ANY,
+
+ // ReSharper disable once InconsistentNaming
+ ///
+ /// Represents an ALL array comparison.
+ ///
+ ALL
+ }
+}
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/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
index 287764238..6396485c3 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 VisitArrayAnyAll(ArrayAnyAllExpression arrayAnyAllExpression)
{
- Visit(arrayAnyExpression.Operand);
- Sql.Append(" = ANY (");
- Visit(arrayAnyExpression.Array);
- Sql.Append(")");
- return arrayAnyExpression;
+ Visit(arrayAnyAllExpression.Operand);
+ Sql.Append(' ');
+ Sql.Append(arrayAnyAllExpression.Operator);
+ Sql.Append(' ');
+ Sql.Append(arrayAnyAllExpression.ArrayComparisonType.ToString());
+ Sql.Append(" (");
+ Visit(arrayAnyAllExpression.Array);
+ Sql.Append(')');
+ return arrayAnyAllExpression;
}
- // 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/LikeAnyAllQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs
new file mode 100644
index 000000000..92aa1d27d
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/LikeAnyAllQueryNpgsqlTest.cs
@@ -0,0 +1,236 @@
+using System;
+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 LikeAnyAllQueryNpgsqlTest : IClassFixture
+ {
+ #region Setup
+
+ ///
+ /// Provides resources for unit tests.
+ ///
+ LikeAnyQueryNpgsqlFixture Fixture { get; }
+
+ ///
+ /// Initializes resources for unit tests.
+ ///
+ /// The fixture of resources for testing.
+ public LikeAnyAllQueryNpgsqlTest(LikeAnyQueryNpgsqlFixture fixture)
+ {
+ Fixture = fixture;
+ Fixture.TestSqlLoggerFactory.Clear();
+ }
+
+ #endregion
+
+ #region LikeTests
+
+ [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_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)))
+ .ToArray();
+
+ AssertContainsSql("WHERE x.\"Animal\" ILIKE ANY (@__collection_0) = TRUE");
+ }
+ }
+
+ [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
+
+ ///
+ /// Represents a fixture suitable for testing LIKE ANY expressions.
+ ///
+ public class LikeAnyQueryNpgsqlFixture : IDisposable
+ {
+ ///
+ /// The used for testing.
+ ///
+ readonly NpgsqlTestStore _testStore;
+
+ ///
+ /// The used for testing.
+ ///
+ 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() => new LikeAnyContext(_options);
+
+ ///
+ public void Dispose() => _testStore.Dispose();
+ }
+
+ ///
+ /// Represents an entity suitable for testing LIKE ANY expressions.
+ ///
+ public class LikeAnyTestEntity
+ {
+ ///
+ /// The primary 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
+ }
+}