Skip to content

SQLite: Simplify !Regex.IsMatch to NOT REGEXP #29781

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
1 commit merged into from
Jan 3, 2023
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
11 changes: 8 additions & 3 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1527,7 +1527,12 @@ private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlBinaryExpression sql
return sqlBinaryExpression;
}

private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression)
/// <summary>
/// Attempts to simplify a unary not operation on a non-nullable operand.
/// </summary>
/// <param name="sqlUnaryExpression">The expression to simplify.</param>
/// <returns>The simplified expression, or the original expression if it cannot be simplified.</returns>
protected virtual SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression)
{
if (sqlUnaryExpression.OperatorType != ExpressionType.Not)
{
Expand Down Expand Up @@ -1632,7 +1637,7 @@ private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUna
sqlBinaryOperand.TypeMapping)!;
}
}
break;
break;
}

return sqlUnaryExpression;
Expand Down Expand Up @@ -1842,7 +1847,7 @@ private SqlExpression ProcessNullNotNull(SqlUnaryExpression sqlUnaryExpression,
return result;
}
}
break;
break;
}

return sqlUnaryExpression;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ protected override void GenerateSetOperationOperand(SetOperationBase setOperatio
private Expression VisitGlob(GlobExpression globExpression)
{
Visit(globExpression.Match);

if (globExpression.IsNegated)
{
Sql.Append(" NOT");
}

Sql.Append(" GLOB ");
Visit(globExpression.Pattern);

Expand All @@ -100,6 +106,12 @@ private Expression VisitGlob(GlobExpression globExpression)
private Expression VisitRegexp(RegexpExpression regexpExpression)
{
Visit(regexpExpression.Match);

if (regexpExpression.IsNegated)
{
Sql.Append(" NOT");
}

Sql.Append(" REGEXP ");
Visit(regexpExpression.Pattern);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,15 @@ public virtual SqlFunctionExpression Date(
/// 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>
public virtual GlobExpression Glob(SqlExpression match, SqlExpression pattern)
public virtual GlobExpression Glob(SqlExpression match, SqlExpression pattern, bool negated = false)
{
var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(match, pattern)
?? Dependencies.TypeMappingSource.FindMapping(match.Type, Dependencies.Model);

match = ApplyTypeMapping(match, inferredTypeMapping);
pattern = ApplyTypeMapping(pattern, inferredTypeMapping);

return new GlobExpression(match, pattern, _boolTypeMapping);
return new GlobExpression(match, pattern, negated, _boolTypeMapping);
}

/// <summary>
Expand All @@ -134,15 +134,15 @@ public virtual GlobExpression Glob(SqlExpression match, SqlExpression pattern)
/// 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>
public virtual RegexpExpression Regexp(SqlExpression match, SqlExpression pattern)
public virtual RegexpExpression Regexp(SqlExpression match, SqlExpression pattern, bool negated = false)
{
var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(match, pattern)
?? Dependencies.TypeMappingSource.FindMapping(match.Type, Dependencies.Model);

match = ApplyTypeMapping(match, inferredTypeMapping);
pattern = ApplyTypeMapping(pattern, inferredTypeMapping);

return new RegexpExpression(match, pattern, _boolTypeMapping);
return new RegexpExpression(match, pattern, negated, _boolTypeMapping);
}

/// <summary>
Expand Down Expand Up @@ -171,7 +171,7 @@ private SqlExpression ApplyTypeMappingOnGlob(GlobExpression globExpression)
var pattern = ApplyTypeMapping(globExpression.Pattern, inferredTypeMapping);

return match != globExpression.Match || pattern != globExpression.Pattern || globExpression.TypeMapping != _boolTypeMapping
? new GlobExpression(match, pattern, _boolTypeMapping)
? new GlobExpression(match, pattern, globExpression.IsNegated, _boolTypeMapping)
: globExpression;
}

Expand All @@ -184,7 +184,7 @@ private SqlExpression ApplyTypeMappingOnGlob(GlobExpression globExpression)
var pattern = ApplyTypeMapping(regexpExpression.Pattern, inferredTypeMapping);

return match != regexpExpression.Match || pattern != regexpExpression.Pattern || regexpExpression.TypeMapping != _boolTypeMapping
? new RegexpExpression(match, pattern, _boolTypeMapping)
? new RegexpExpression(match, pattern, regexpExpression.IsNegated, _boolTypeMapping)
: regexpExpression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,27 @@ protected virtual SqlExpression VisitRegexp(

return regexpExpression.Update(match, pattern);
}

/// <summary>
/// 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>
protected override SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression)
{
if (sqlUnaryExpression.OperatorType == ExpressionType.Not)
{
switch (sqlUnaryExpression.Operand)
{
case GlobExpression globOperand:
return globOperand.Negate();

case RegexpExpression regexpOperand:
return regexpOperand.Negate();
}
}

return base.OptimizeNonNullableNotExpression(sqlUnaryExpression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ public class GlobExpression : SqlExpression
/// 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>
public GlobExpression(SqlExpression match, SqlExpression pattern, RelationalTypeMapping typeMapping)
public GlobExpression(SqlExpression match, SqlExpression pattern, bool negated, RelationalTypeMapping typeMapping)
: base(typeof(bool), typeMapping)
{
Match = match;
Pattern = pattern;
IsNegated = negated;
}

/// <summary>
Expand Down Expand Up @@ -51,6 +52,14 @@ public override RelationalTypeMapping TypeMapping
/// </summary>
public virtual SqlExpression Pattern { get; }

/// <summary>
/// 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>
public virtual bool IsNegated { get; }

/// <summary>
/// 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
Expand All @@ -65,6 +74,15 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
return Update(match, pattern);
}

/// <summary>
/// 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>
public virtual GlobExpression Negate()
=> new GlobExpression(Match, Pattern, !IsNegated, TypeMapping);

/// <summary>
/// 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
Expand All @@ -73,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// </summary>
public virtual GlobExpression Update(SqlExpression match, SqlExpression pattern)
=> match != Match || pattern != Pattern
? new GlobExpression(match, pattern, TypeMapping)
? new GlobExpression(match, pattern, IsNegated, TypeMapping)
: this;

/// <summary>
Expand All @@ -85,6 +103,12 @@ public virtual GlobExpression Update(SqlExpression match, SqlExpression pattern)
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Visit(Match);

if (IsNegated)
{
expressionPrinter.Append(" NOT");
}

expressionPrinter.Append(" GLOB ");
expressionPrinter.Visit(Pattern);
}
Expand All @@ -104,7 +128,8 @@ public override bool Equals(object? obj)
private bool Equals(GlobExpression globExpression)
=> base.Equals(globExpression)
&& Match.Equals(globExpression.Match)
&& Pattern.Equals(globExpression.Pattern);
&& Pattern.Equals(globExpression.Pattern)
&& IsNegated.Equals(globExpression.IsNegated);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -113,5 +138,5 @@ private bool Equals(GlobExpression globExpression)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Match, Pattern);
=> HashCode.Combine(base.GetHashCode(), Match, Pattern, IsNegated);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ public class RegexpExpression : SqlExpression
/// 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>
public RegexpExpression(SqlExpression match, SqlExpression pattern, RelationalTypeMapping typeMapping)
public RegexpExpression(SqlExpression match, SqlExpression pattern, bool negated, RelationalTypeMapping typeMapping)
: base(typeof(bool), typeMapping)
{
Match = match;
Pattern = pattern;
IsNegated = negated;
}

/// <summary>
Expand Down Expand Up @@ -51,6 +52,14 @@ public override RelationalTypeMapping TypeMapping
/// </summary>
public virtual SqlExpression Pattern { get; }

/// <summary>
/// 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>
public virtual bool IsNegated { get; }

/// <summary>
/// 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
Expand All @@ -65,6 +74,15 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
return Update(match, pattern);
}

/// <summary>
/// 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>
public virtual RegexpExpression Negate()
=> new RegexpExpression(Match, Pattern, !IsNegated, TypeMapping);

/// <summary>
/// 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
Expand All @@ -73,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// </summary>
public virtual RegexpExpression Update(SqlExpression match, SqlExpression pattern)
=> match != Match || pattern != Pattern
? new RegexpExpression(match, pattern, TypeMapping)
? new RegexpExpression(match, pattern, IsNegated, TypeMapping)
: this;

/// <summary>
Expand All @@ -85,6 +103,12 @@ public virtual RegexpExpression Update(SqlExpression match, SqlExpression patter
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Visit(Match);

if (IsNegated)
{
expressionPrinter.Append(" NOT");
}

expressionPrinter.Append(" REGEXP ");
expressionPrinter.Visit(Pattern);
}
Expand All @@ -104,7 +128,8 @@ public override bool Equals(object? obj)
private bool Equals(RegexpExpression regexpExpression)
=> base.Equals(regexpExpression)
&& Match.Equals(regexpExpression.Match)
&& Pattern.Equals(regexpExpression.Pattern);
&& Pattern.Equals(regexpExpression.Pattern)
&& IsNegated.Equals(regexpExpression.IsNegated);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -113,5 +138,5 @@ private bool Equals(RegexpExpression regexpExpression)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Match, Pattern);
=> HashCode.Combine(base.GetHashCode(), Match, Pattern, IsNegated);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ SELECT COUNT(*)
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Glob_negated(bool async)
{
await AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => !EF.Functions.Glob(c.CustomerID, "T*"),
c => !c.CustomerID.StartsWith("T"));

AssertSql(
"""
SELECT COUNT(*)
FROM "Customers" AS "c"
WHERE "c"."CustomerID" NOT GLOB 'T*'
""");
}

protected override string CaseInsensitiveCollation
=> "NOCASE";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;

namespace Microsoft.EntityFrameworkCore.Query;

public class NorthwindFunctionsQuerySqliteTest : NorthwindFunctionsQueryRelationalTestBase<
Expand Down Expand Up @@ -828,6 +831,23 @@ public override async Task Regex_IsMatch_MethodCall_constant_input(bool async)
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Regex_IsMatch_MethodCall_negated(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Customer>().Where(o => !Regex.IsMatch(o.CustomerID, "^[^T]")),
entryCount: 6);

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"."CustomerID" NOT REGEXP '^[^T]'
""");
}

public override async Task IsNullOrEmpty_in_predicate(bool async)
{
await base.IsNullOrEmpty_in_predicate(async);
Expand Down