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);