diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 61583bb4e..3418d5045 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -614,11 +614,29 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio
Visit(sqlUnaryExpression.Operand);
return sqlUnaryExpression;
- // Not operation on full-text queries
+ // NOT operation on full-text queries
case ExpressionType.Not when sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(NpgsqlTsQuery):
Sql.Append("!!");
Visit(sqlUnaryExpression.Operand);
return sqlUnaryExpression;
+
+ // NOT over expression types which have fancy embedded negation
+ case ExpressionType.Not
+ when sqlUnaryExpression.Type == typeof(bool):
+ {
+ switch (sqlUnaryExpression.Operand)
+ {
+ case PgRegexMatchExpression regexMatch:
+ VisitRegexMatch(regexMatch, negated: true);
+ return sqlUnaryExpression;
+
+ case PgILikeExpression iLike:
+ VisitILike(iLike, negated: true);
+ return sqlUnaryExpression;
+ }
+
+ break;
+ }
}
return base.VisitSqlUnary(sqlUnaryExpression);
@@ -907,16 +925,12 @@ protected virtual Expression VisitArraySlice(PgArraySliceExpression expression)
}
///
- /// Visits the children of a .
+ /// Produces SQL for PostgreSQL regex matching.
///
- /// The expression.
- ///
- /// An .
- ///
///
/// See: http://www.postgresql.org/docs/current/static/functions-matching.html
///
- protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression)
+ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, bool negated = false)
{
var options = expression.Options;
@@ -924,12 +938,12 @@ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression)
if (options.HasFlag(RegexOptions.IgnoreCase))
{
- Sql.Append(" ~* ");
+ Sql.Append(negated ? " !~* " : " ~* ");
options &= ~RegexOptions.IgnoreCase;
}
else
{
- Sql.Append(" ~ ");
+ Sql.Append(negated ? " !~ " : " ~ ");
}
// PG regexps are single-line by default
@@ -1012,16 +1026,22 @@ protected virtual Expression VisitRowValue(PgRowValueExpression rowValueExpressi
}
///
- /// Visits the children of an .
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- /// The expression.
- ///
- /// An .
- ///
- protected virtual Expression VisitILike(PgILikeExpression likeExpression)
+ protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool negated = false)
{
Visit(likeExpression.Match);
+
+ if (negated)
+ {
+ Sql.Append(" NOT");
+ }
+
Sql.Append(" ILIKE ");
+
Visit(likeExpression.Pattern);
if (likeExpression.EscapeChar is not null)
diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs
index 0b13fc070..425b5782e 100644
--- a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs
@@ -117,7 +117,7 @@ public void String_ILike_negated()
"""
SELECT count(*)::int
FROM "Customers" AS c
-WHERE NOT (c."ContactName" ILIKE '%M%') OR c."ContactName" IS NULL
+WHERE c."ContactName" NOT ILIKE '%M%' OR c."ContactName" IS NULL
""");
}
diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs
index c8fd85fe5..f522b5ac4 100644
--- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs
@@ -131,6 +131,22 @@ await AssertQuery(
""");
}
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Regex_IsMatch_negated(bool async)
+ {
+ await AssertQuery(
+ async,
+ cs => cs.Set().Where(c => !Regex.IsMatch(c.CompanyName, "^A")));
+
+ AssertSql(
+ """
+SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
+FROM "Customers" AS c
+WHERE c."CompanyName" !~ '(?p)^A'
+""");
+ }
+
[Theory]
[MemberData(nameof(IsAsyncData))]
public async Task Regex_IsMatchOptionsNone(bool async)
@@ -163,6 +179,22 @@ await AssertQuery(
""");
}
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Regex_IsMatch_with_IgnoreCase_negated(bool async)
+ {
+ await AssertQuery(
+ async,
+ cs => cs.Set().Where(c => !Regex.IsMatch(c.CompanyName, "^a", RegexOptions.IgnoreCase)));
+
+ AssertSql(
+ """
+SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
+FROM "Customers" AS c
+WHERE c."CompanyName" !~* '(?p)^a'
+""");
+ }
+
[Theory]
[MemberData(nameof(IsAsyncData))]
public async Task Regex_IsMatch_with_Multiline(bool async)
diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs
index cc8aa9153..521f2ff6c 100644
--- a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs
@@ -355,6 +355,28 @@ public async Task Array_All_Like(bool async)
""");
}
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Array_All_Like_negated(bool async)
+ {
+ await using var context = CreateContext();
+
+ var collection = new[] { "A%", "B%", "C%" };
+ var query = context.Set().Where(c => !collection.All(y => EF.Functions.Like(c.Address, y)));
+ var result = async ? await query.ToListAsync() : query.ToList();
+
+ Assert.NotEmpty(result);
+
+ AssertSql(
+ """
+@__collection_1={ 'A%', 'B%', 'C%' } (DbType = Object)
+
+SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
+FROM "Customers" AS c
+WHERE NOT (c."Address" LIKE ALL (@__collection_1))
+""");
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Array_Any_ILike(bool async)
@@ -401,6 +423,35 @@ public async Task Array_Any_ILike(bool async)
""");
}
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Array_Any_ILike_negated(bool async)
+ {
+ await using var context = CreateContext();
+
+ var collection = new[] { "a%", "b%", "c%" };
+ var query = context.Set().Where(c => !collection.Any(y => EF.Functions.ILike(c.Address, y)));
+ var result = async ? await query.ToListAsync() : query.ToList();
+
+ Assert.Equal(
+ [
+ "ALFKI",
+ "ANTON",
+ "AROUT",
+ "BLAUS",
+ "BLONP"
+ ], result.Select(e => e.CustomerID).Order().Take(5));
+
+ AssertSql(
+ """
+@__collection_1={ 'a%', 'b%', 'c%' } (DbType = Object)
+
+SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
+FROM "Customers" AS c
+WHERE NOT (c."Address" ILIKE ANY (@__collection_1))
+""");
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task Array_All_ILike(bool async)