From 1c970031563754c48413af289e10d1f4136a154b Mon Sep 17 00:00:00 2001 From: Brice Lambson Date: Mon, 5 Dec 2022 16:38:13 -0800 Subject: [PATCH] SQLite: Simplify !Regex.IsMatch to NOT REGEXP --- .../Query/SqlNullabilityProcessor.cs | 11 +++++-- .../Query/Internal/SqliteQuerySqlGenerator.cs | 12 +++++++ .../Internal/SqliteSqlExpressionFactory.cs | 12 +++---- .../Internal/SqliteSqlNullabilityProcessor.cs | 23 +++++++++++++ .../SqlExpressions/Internal/GlobExpression.cs | 33 ++++++++++++++++--- .../Internal/RegexpExpression.cs | 33 ++++++++++++++++--- .../NorthwindDbFunctionsQuerySqliteTest.cs | 19 +++++++++++ .../NorthwindFunctionsQuerySqliteTest.cs | 20 +++++++++++ 8 files changed, 146 insertions(+), 17 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index ab3fc91c5a1..5b42172288c 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -1527,7 +1527,12 @@ private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlBinaryExpression sql return sqlBinaryExpression; } - private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression) + /// + /// Attempts to simplify a unary not operation on a non-nullable operand. + /// + /// The expression to simplify. + /// The simplified expression, or the original expression if it cannot be simplified. + protected virtual SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression) { if (sqlUnaryExpression.OperatorType != ExpressionType.Not) { @@ -1632,7 +1637,7 @@ private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUna sqlBinaryOperand.TypeMapping)!; } } - break; + break; } return sqlUnaryExpression; @@ -1842,7 +1847,7 @@ private SqlExpression ProcessNullNotNull(SqlUnaryExpression sqlUnaryExpression, return result; } } - break; + break; } return sqlUnaryExpression; diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs index 766076182cb..7316f6fb164 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs @@ -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); @@ -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); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs index c58f11be60b..86e7df0a6ee 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs @@ -117,7 +117,7 @@ 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. /// - 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); @@ -125,7 +125,7 @@ public virtual GlobExpression Glob(SqlExpression match, SqlExpression pattern) match = ApplyTypeMapping(match, inferredTypeMapping); pattern = ApplyTypeMapping(pattern, inferredTypeMapping); - return new GlobExpression(match, pattern, _boolTypeMapping); + return new GlobExpression(match, pattern, negated, _boolTypeMapping); } /// @@ -134,7 +134,7 @@ 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. /// - 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); @@ -142,7 +142,7 @@ public virtual RegexpExpression Regexp(SqlExpression match, SqlExpression patter match = ApplyTypeMapping(match, inferredTypeMapping); pattern = ApplyTypeMapping(pattern, inferredTypeMapping); - return new RegexpExpression(match, pattern, _boolTypeMapping); + return new RegexpExpression(match, pattern, negated, _boolTypeMapping); } /// @@ -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; } @@ -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; } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs index 9385ab82464..b5144975ade 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs @@ -83,4 +83,27 @@ protected virtual SqlExpression VisitRegexp( return regexpExpression.Update(match, pattern); } + + /// + /// 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. + /// + 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); + } } diff --git a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/GlobExpression.cs b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/GlobExpression.cs index 1901cd67b4a..f8349759558 100644 --- a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/GlobExpression.cs +++ b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/GlobExpression.cs @@ -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. /// - 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; } /// @@ -51,6 +52,14 @@ public override RelationalTypeMapping TypeMapping /// public virtual SqlExpression Pattern { get; } + /// + /// 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. + /// + public virtual bool IsNegated { get; } + /// /// 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 @@ -65,6 +74,15 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return Update(match, pattern); } + /// + /// 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. + /// + public virtual GlobExpression Negate() + => new GlobExpression(Match, Pattern, !IsNegated, TypeMapping); + /// /// 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 @@ -73,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// public virtual GlobExpression Update(SqlExpression match, SqlExpression pattern) => match != Match || pattern != Pattern - ? new GlobExpression(match, pattern, TypeMapping) + ? new GlobExpression(match, pattern, IsNegated, TypeMapping) : this; /// @@ -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); } @@ -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); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -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. /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Match, Pattern); + => HashCode.Combine(base.GetHashCode(), Match, Pattern, IsNegated); } diff --git a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/RegexpExpression.cs b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/RegexpExpression.cs index 101cced8c7c..c06634d33b9 100644 --- a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/RegexpExpression.cs +++ b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/RegexpExpression.cs @@ -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. /// - 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; } /// @@ -51,6 +52,14 @@ public override RelationalTypeMapping TypeMapping /// public virtual SqlExpression Pattern { get; } + /// + /// 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. + /// + public virtual bool IsNegated { get; } + /// /// 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 @@ -65,6 +74,15 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return Update(match, pattern); } + /// + /// 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. + /// + public virtual RegexpExpression Negate() + => new RegexpExpression(Match, Pattern, !IsNegated, TypeMapping); + /// /// 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 @@ -73,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// public virtual RegexpExpression Update(SqlExpression match, SqlExpression pattern) => match != Match || pattern != Pattern - ? new RegexpExpression(match, pattern, TypeMapping) + ? new RegexpExpression(match, pattern, IsNegated, TypeMapping) : this; /// @@ -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); } @@ -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); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -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. /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Match, Pattern); + => HashCode.Combine(base.GetHashCode(), Match, Pattern, IsNegated); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindDbFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindDbFunctionsQuerySqliteTest.cs index 01c358b6466..3e0b0d247aa 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindDbFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindDbFunctionsQuerySqliteTest.cs @@ -35,6 +35,25 @@ SELECT COUNT(*) """); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Glob_negated(bool async) + { + await AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + 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"; diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 7cc2a1d474d..3c248bd668e 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -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< @@ -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().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);