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 + } +}