Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -907,29 +925,25 @@ protected virtual Expression VisitArraySlice(PgArraySliceExpression expression)
}

/// <summary>
/// Visits the children of a <see cref="PgRegexMatchExpression" />.
/// Produces SQL for PostgreSQL regex matching.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>
/// An <see cref="Expression" />.
/// </returns>
/// <remarks>
/// See: http://www.postgresql.org/docs/current/static/functions-matching.html
/// </remarks>
protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression)
protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, bool negated = false)
{
var options = expression.Options;

Visit(expression.Match);

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
Expand Down Expand Up @@ -1012,16 +1026,22 @@ protected virtual Expression VisitRowValue(PgRowValueExpression rowValueExpressi
}

/// <summary>
/// Visits the children of an <see cref="PgILikeExpression" />.
/// 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.
/// </summary>
/// <param name="likeExpression">The expression.</param>
/// <returns>
/// An <see cref="Expression" />.
/// </returns>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ await AssertQuery(
""");
}

[Theory]
[MemberData(nameof(IsAsyncData))]
public async Task Regex_IsMatch_negated(bool async)
{
await AssertQuery(
async,
cs => cs.Set<Customer>().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)
Expand Down Expand Up @@ -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<Customer>().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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Customer>().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)
Expand Down Expand Up @@ -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<Customer>().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)
Expand Down