Repository: dotnet/efcore

Issue:
Cosmos: Fixup to Regex.IsMatch translation
Fixup to https://github.com/dotnet/efcore/pull/28121

* ~Throw a detailed exception for unsupported options when translating Regex.IsMatch (see discussion below)~
* "Constantize" regex options via EvaluatableFilter (https://github.com/dotnet/efcore/pull/28121#discussion_r886897927)
* Avoid generating empty options parameter if RegexOptions.None is specified (https://github.com/dotnet/efcore/pull/28121#discussion_r886896534)


Hints:
Depend on #26410, right? 
Not really blocked, the method translator should throw exception from itself.
Don't we avoid doing that since it would block client eval?
Depends if we want to do client eval of certain things.
OK, I'll bring this up in design, possibly with a proposal for #26410.
If you think we would need #26410 for this, we can wait till we put that one in plan. 
Just to figure out how we see things... If we're OK with throwing in method translators, #26410 becomes much less important, maybe even closeable (note it's already in the milestone).

I still think that ideally we just do #26410, e.g. by having translator return some NotTranslatableExpression (which wraps details); and then we don't have to discuss throwing because that always allows client eval and provides nicer exceptions. But we can discuss all that.
I'll prepare a PR for this
@Marusyk sure, but we'll need to decide what we want to do with unsupported options (design discussion pending).
Design decision: we'll do #26410 at some point, but for now it should be fine to just return null for unsupported options, so the user will just get the generic "cannot translate" exception. @Marusyk this should unblock you for working on this issue.
So, should I return the previous version with [HashSet](https://github.com/dotnet/efcore/pull/28121/files/b8cba0c574c3295aa622a7cc71a954a446f43e83#diff-ddd4f5b756ddfba1b854430190d2fe812fc0bed705ff3263e9d05074a9d24a46R22)?

> "Constantize" regex options via EvaluatableFilter

I'm not sure that understand this task. Do I need to implement CosmosEvaluatableExpressionFilter?

> So, should I return the previous version with [HashSet](https://github.com/dotnet/efcore/pull/28121/files/b8cba0c574c3295aa622a7cc71a954a446f43e83#diff-ddd4f5b756ddfba1b854430190d2fe812fc0bed705ff3263e9d05074a9d24a46R22)?

Nope, I don't see any reason for doing so. The current code does the right thing (for now) - if there's any unsupported option, it just returns null.

> Do I need to implement CosmosEvaluatableExpressionFilter?

Yeah, @smitpatel can probably offer some more assistance if needed.
@Marusyk do you intend to work on this?
Unfortunately, I don't have time to investigate EvaluatableExpressionFilter. 

Patch:
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs
index 51e735e7681..bf765963620 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs
@@ -19,8 +19,6 @@ public class CosmosRegexTranslator : IMethodCallTranslator
     private static readonly MethodInfo IsMatchWithRegexOptions =
         typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string), typeof(RegexOptions) })!;
 
-    private const RegexOptions SupportedOptions = RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace;
-
     private readonly ISqlExpressionFactory _sqlExpressionFactory;
 
     /// <summary>
@@ -53,48 +51,49 @@ public CosmosRegexTranslator(ISqlExpressionFactory sqlExpressionFactory)
 
         var (input, pattern) = (arguments[0], arguments[1]);
         var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern);
+        (input, pattern) = (
+            _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping),
+            _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping));
 
-        if (method == IsMatch)
+        if (method == IsMatch || arguments[2] is SqlConstantExpression { Value: RegexOptions.None })
         {
-            return _sqlExpressionFactory.Function(
-                "RegexMatch",
-                new[] {
-                    _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping),
-                    _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping)
-                },
-                typeof(bool));
+            return _sqlExpressionFactory.Function("RegexMatch", new[] { input, pattern }, typeof(bool));
         }
-        else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions })
+
+        if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions })
         {
-            string modifier = "";
+            var modifier = "";
+
             if (regexOptions.HasFlag(RegexOptions.Multiline))
             {
+                regexOptions &= ~RegexOptions.Multiline;
                 modifier += "m";
             }
+
             if (regexOptions.HasFlag(RegexOptions.Singleline))
             {
+                regexOptions &= ~RegexOptions.Singleline;
                 modifier += "s";
             }
+
             if (regexOptions.HasFlag(RegexOptions.IgnoreCase))
             {
+                regexOptions &= ~RegexOptions.IgnoreCase;
                 modifier += "i";
             }
+
             if (regexOptions.HasFlag(RegexOptions.IgnorePatternWhitespace))
             {
+                regexOptions &= ~RegexOptions.IgnorePatternWhitespace;
                 modifier += "x";
             }
 
-            return (regexOptions & ~SupportedOptions) == 0
+            return regexOptions == 0
                 ? _sqlExpressionFactory.Function(
                     "RegexMatch",
-                     new[]
-                     {
-                        _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping),
-                        _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping),
-                        _sqlExpressionFactory.Constant(modifier)
-                     },
+                     new[] { input, pattern, _sqlExpressionFactory.Constant(modifier) },
                      typeof(bool))
-                : null;
+                : null; // TODO: Report unsupported RegexOption, #26410
         }
 
         return null;


Test patch:
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
index 7529b91fa3d..a67e1e4ced6 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
@@ -1235,7 +1235,7 @@ await AssertQuery(
         AssertSql(
            @"SELECT c
 FROM root c
-WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", """"))");
+WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T""))");
     }
 
     [ConditionalTheory]
@@ -1313,15 +1313,19 @@ FROM root c
 WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""ix""))");
     }
 
-    [Fact]
-    public virtual void Regex_IsMatch_MethodCall_With_Unsupported_Option()
-       => Assert.Throws<InvalidOperationException>(() =>
-           Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.RightToLeft)).ToList());
+    [ConditionalTheory]
+    [MemberData(nameof(IsAsyncData))]
+    public virtual Task Regex_IsMatch_MethodCall_With_Unsupported_Option(bool async)
+        => AssertTranslationFailed(() => AssertQuery(
+            async,
+            ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.RightToLeft))));
 
-    [Fact]
-    public virtual void Regex_IsMatch_MethodCall_With_Any_Unsupported_Option()
-       => Assert.Throws<InvalidOperationException>(() =>
-           Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.RightToLeft)).ToList());
+    [ConditionalTheory]
+    [MemberData(nameof(IsAsyncData))]
+    public virtual Task Regex_IsMatch_MethodCall_With_Any_Unsupported_Option(bool async)
+        => AssertTranslationFailed(() => AssertQuery(
+            async,
+            ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.RightToLeft))));
 
     [ConditionalTheory]
     [MemberData(nameof(IsAsyncData))]