Skip to content

Commit f7d7910

Browse files
authored
Negated syntax for regex and ILIKE (#3276)
Closes #2589
1 parent b11d279 commit f7d7910

File tree

4 files changed

+119
-16
lines changed

4 files changed

+119
-16
lines changed

src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs

+35-15
Original file line numberDiff line numberDiff line change
@@ -614,11 +614,29 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio
614614
Visit(sqlUnaryExpression.Operand);
615615
return sqlUnaryExpression;
616616

617-
// Not operation on full-text queries
617+
// NOT operation on full-text queries
618618
case ExpressionType.Not when sqlUnaryExpression.Operand.TypeMapping.ClrType == typeof(NpgsqlTsQuery):
619619
Sql.Append("!!");
620620
Visit(sqlUnaryExpression.Operand);
621621
return sqlUnaryExpression;
622+
623+
// NOT over expression types which have fancy embedded negation
624+
case ExpressionType.Not
625+
when sqlUnaryExpression.Type == typeof(bool):
626+
{
627+
switch (sqlUnaryExpression.Operand)
628+
{
629+
case PgRegexMatchExpression regexMatch:
630+
VisitRegexMatch(regexMatch, negated: true);
631+
return sqlUnaryExpression;
632+
633+
case PgILikeExpression iLike:
634+
VisitILike(iLike, negated: true);
635+
return sqlUnaryExpression;
636+
}
637+
638+
break;
639+
}
622640
}
623641

624642
return base.VisitSqlUnary(sqlUnaryExpression);
@@ -907,29 +925,25 @@ protected virtual Expression VisitArraySlice(PgArraySliceExpression expression)
907925
}
908926

909927
/// <summary>
910-
/// Visits the children of a <see cref="PgRegexMatchExpression" />.
928+
/// Produces SQL for PostgreSQL regex matching.
911929
/// </summary>
912-
/// <param name="expression">The expression.</param>
913-
/// <returns>
914-
/// An <see cref="Expression" />.
915-
/// </returns>
916930
/// <remarks>
917931
/// See: http://www.postgresql.org/docs/current/static/functions-matching.html
918932
/// </remarks>
919-
protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression)
933+
protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, bool negated = false)
920934
{
921935
var options = expression.Options;
922936

923937
Visit(expression.Match);
924938

925939
if (options.HasFlag(RegexOptions.IgnoreCase))
926940
{
927-
Sql.Append(" ~* ");
941+
Sql.Append(negated ? " !~* " : " ~* ");
928942
options &= ~RegexOptions.IgnoreCase;
929943
}
930944
else
931945
{
932-
Sql.Append(" ~ ");
946+
Sql.Append(negated ? " !~ " : " ~ ");
933947
}
934948

935949
// PG regexps are single-line by default
@@ -1012,16 +1026,22 @@ protected virtual Expression VisitRowValue(PgRowValueExpression rowValueExpressi
10121026
}
10131027

10141028
/// <summary>
1015-
/// Visits the children of an <see cref="PgILikeExpression" />.
1029+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1030+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1031+
/// any release. You should only use it directly in your code with extreme caution and knowing that
1032+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
10161033
/// </summary>
1017-
/// <param name="likeExpression">The expression.</param>
1018-
/// <returns>
1019-
/// An <see cref="Expression" />.
1020-
/// </returns>
1021-
protected virtual Expression VisitILike(PgILikeExpression likeExpression)
1034+
protected virtual Expression VisitILike(PgILikeExpression likeExpression, bool negated = false)
10221035
{
10231036
Visit(likeExpression.Match);
1037+
1038+
if (negated)
1039+
{
1040+
Sql.Append(" NOT");
1041+
}
1042+
10241043
Sql.Append(" ILIKE ");
1044+
10251045
Visit(likeExpression.Pattern);
10261046

10271047
if (likeExpression.EscapeChar is not null)

test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public void String_ILike_negated()
117117
"""
118118
SELECT count(*)::int
119119
FROM "Customers" AS c
120-
WHERE NOT (c."ContactName" ILIKE '%M%') OR c."ContactName" IS NULL
120+
WHERE c."ContactName" NOT ILIKE '%M%' OR c."ContactName" IS NULL
121121
""");
122122
}
123123

test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs

+32
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,22 @@ await AssertQuery(
131131
""");
132132
}
133133

134+
[Theory]
135+
[MemberData(nameof(IsAsyncData))]
136+
public async Task Regex_IsMatch_negated(bool async)
137+
{
138+
await AssertQuery(
139+
async,
140+
cs => cs.Set<Customer>().Where(c => !Regex.IsMatch(c.CompanyName, "^A")));
141+
142+
AssertSql(
143+
"""
144+
SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
145+
FROM "Customers" AS c
146+
WHERE c."CompanyName" !~ '(?p)^A'
147+
""");
148+
}
149+
134150
[Theory]
135151
[MemberData(nameof(IsAsyncData))]
136152
public async Task Regex_IsMatchOptionsNone(bool async)
@@ -163,6 +179,22 @@ await AssertQuery(
163179
""");
164180
}
165181

182+
[Theory]
183+
[MemberData(nameof(IsAsyncData))]
184+
public async Task Regex_IsMatch_with_IgnoreCase_negated(bool async)
185+
{
186+
await AssertQuery(
187+
async,
188+
cs => cs.Set<Customer>().Where(c => !Regex.IsMatch(c.CompanyName, "^a", RegexOptions.IgnoreCase)));
189+
190+
AssertSql(
191+
"""
192+
SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
193+
FROM "Customers" AS c
194+
WHERE c."CompanyName" !~* '(?p)^a'
195+
""");
196+
}
197+
166198
[Theory]
167199
[MemberData(nameof(IsAsyncData))]
168200
public async Task Regex_IsMatch_with_Multiline(bool async)

test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs

+51
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,28 @@ public async Task Array_All_Like(bool async)
355355
""");
356356
}
357357

358+
[ConditionalTheory]
359+
[MemberData(nameof(IsAsyncData))]
360+
public async Task Array_All_Like_negated(bool async)
361+
{
362+
await using var context = CreateContext();
363+
364+
var collection = new[] { "A%", "B%", "C%" };
365+
var query = context.Set<Customer>().Where(c => !collection.All(y => EF.Functions.Like(c.Address, y)));
366+
var result = async ? await query.ToListAsync() : query.ToList();
367+
368+
Assert.NotEmpty(result);
369+
370+
AssertSql(
371+
"""
372+
@__collection_1={ 'A%', 'B%', 'C%' } (DbType = Object)
373+
374+
SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
375+
FROM "Customers" AS c
376+
WHERE NOT (c."Address" LIKE ALL (@__collection_1))
377+
""");
378+
}
379+
358380
[ConditionalTheory]
359381
[MemberData(nameof(IsAsyncData))]
360382
public async Task Array_Any_ILike(bool async)
@@ -401,6 +423,35 @@ public async Task Array_Any_ILike(bool async)
401423
""");
402424
}
403425

426+
[ConditionalTheory]
427+
[MemberData(nameof(IsAsyncData))]
428+
public async Task Array_Any_ILike_negated(bool async)
429+
{
430+
await using var context = CreateContext();
431+
432+
var collection = new[] { "a%", "b%", "c%" };
433+
var query = context.Set<Customer>().Where(c => !collection.Any(y => EF.Functions.ILike(c.Address, y)));
434+
var result = async ? await query.ToListAsync() : query.ToList();
435+
436+
Assert.Equal(
437+
[
438+
"ALFKI",
439+
"ANTON",
440+
"AROUT",
441+
"BLAUS",
442+
"BLONP"
443+
], result.Select(e => e.CustomerID).Order().Take(5));
444+
445+
AssertSql(
446+
"""
447+
@__collection_1={ 'a%', 'b%', 'c%' } (DbType = Object)
448+
449+
SELECT c."CustomerID", c."Address", c."City", c."CompanyName", c."ContactName", c."ContactTitle", c."Country", c."Fax", c."Phone", c."PostalCode", c."Region"
450+
FROM "Customers" AS c
451+
WHERE NOT (c."Address" ILIKE ANY (@__collection_1))
452+
""");
453+
}
454+
404455
[ConditionalTheory]
405456
[MemberData(nameof(IsAsyncData))]
406457
public async Task Array_All_ILike(bool async)

0 commit comments

Comments
 (0)