From bda79d4986ebd3b03670a733e1921a4ccf70b1c4 Mon Sep 17 00:00:00 2001 From: Roman Marusyk Date: Mon, 30 May 2022 11:17:37 +0300 Subject: [PATCH] Cosmos: Add translator for Regex.IsMatch method (#28121) Fixes #28078 --- ...nslator.cs => CosmosContainsTranslator.cs} | 4 +- ...ranslator.cs => CosmosEqualsTranslator.cs} | 4 +- ...hTranslator.cs => CosmosMathTranslator.cs} | 4 +- .../CosmosMethodCallTranslatorProvider.cs | 9 +- ...ranslator.cs => CosmosRandomTranslator.cs} | 4 +- .../Query/Internal/CosmosRegexTranslator.cs | 102 +++++++++++++++ .../NorthwindFunctionsQueryCosmosTest.cs | 117 +++++++++++++++++- 7 files changed, 226 insertions(+), 18 deletions(-) rename src/EFCore.Cosmos/Query/Internal/{ContainsTranslator.cs => CosmosContainsTranslator.cs} (94%) rename src/EFCore.Cosmos/Query/Internal/{EqualsTranslator.cs => CosmosEqualsTranslator.cs} (95%) rename src/EFCore.Cosmos/Query/Internal/{MathTranslator.cs => CosmosMathTranslator.cs} (98%) rename src/EFCore.Cosmos/Query/Internal/{RandomTranslator.cs => CosmosRandomTranslator.cs} (94%) create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs diff --git a/src/EFCore.Cosmos/Query/Internal/ContainsTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosContainsTranslator.cs similarity index 94% rename from src/EFCore.Cosmos/Query/Internal/ContainsTranslator.cs rename to src/EFCore.Cosmos/Query/Internal/CosmosContainsTranslator.cs index 1df1d4a2b6c..2f247a92847 100644 --- a/src/EFCore.Cosmos/Query/Internal/ContainsTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosContainsTranslator.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class ContainsTranslator : IMethodCallTranslator +public class CosmosContainsTranslator : IMethodCallTranslator { private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -19,7 +19,7 @@ public class ContainsTranslator : IMethodCallTranslator /// 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 ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory) + public CosmosContainsTranslator(ISqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } diff --git a/src/EFCore.Cosmos/Query/Internal/EqualsTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosEqualsTranslator.cs similarity index 95% rename from src/EFCore.Cosmos/Query/Internal/EqualsTranslator.cs rename to src/EFCore.Cosmos/Query/Internal/CosmosEqualsTranslator.cs index 35789ffd696..17439669e9f 100644 --- a/src/EFCore.Cosmos/Query/Internal/EqualsTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosEqualsTranslator.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class EqualsTranslator : IMethodCallTranslator +public class CosmosEqualsTranslator : IMethodCallTranslator { private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -19,7 +19,7 @@ public class EqualsTranslator : IMethodCallTranslator /// 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 EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory) + public CosmosEqualsTranslator(ISqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } diff --git a/src/EFCore.Cosmos/Query/Internal/MathTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMathTranslator.cs similarity index 98% rename from src/EFCore.Cosmos/Query/Internal/MathTranslator.cs rename to src/EFCore.Cosmos/Query/Internal/CosmosMathTranslator.cs index 2460a39fc94..d6d01c1efda 100644 --- a/src/EFCore.Cosmos/Query/Internal/MathTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMathTranslator.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class MathTranslator : IMethodCallTranslator +public class CosmosMathTranslator : IMethodCallTranslator { private static readonly Dictionary SupportedMethodTranslations = new() { @@ -77,7 +77,7 @@ public class MathTranslator : IMethodCallTranslator /// 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 MathTranslator(ISqlExpressionFactory sqlExpressionFactory) + public CosmosMathTranslator(ISqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index 701b12f622f..a3ced0b0da5 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -29,11 +29,12 @@ public CosmosMethodCallTranslatorProvider( _translators.AddRange( new IMethodCallTranslator[] { - new EqualsTranslator(sqlExpressionFactory), + new CosmosEqualsTranslator(sqlExpressionFactory), new CosmosStringMethodTranslator(sqlExpressionFactory), - new ContainsTranslator(sqlExpressionFactory), - new RandomTranslator(sqlExpressionFactory), - new MathTranslator(sqlExpressionFactory) + new CosmosContainsTranslator(sqlExpressionFactory), + new CosmosRandomTranslator(sqlExpressionFactory), + new CosmosMathTranslator(sqlExpressionFactory), + new CosmosRegexTranslator(sqlExpressionFactory) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), //new GetValueOrDefaultTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosRandomTranslator.cs similarity index 94% rename from src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs rename to src/EFCore.Cosmos/Query/Internal/CosmosRandomTranslator.cs index fefab0714ff..3b0c4ab2c5e 100644 --- a/src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosRandomTranslator.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class RandomTranslator : IMethodCallTranslator +public class CosmosRandomTranslator : IMethodCallTranslator { private static readonly MethodInfo MethodInfo = typeof(DbFunctionsExtensions).GetRuntimeMethod( nameof(DbFunctionsExtensions.Random), new[] { typeof(DbFunctions) }); @@ -24,7 +24,7 @@ public class RandomTranslator : IMethodCallTranslator /// 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 RandomTranslator(ISqlExpressionFactory sqlExpressionFactory) + public CosmosRandomTranslator(ISqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs new file mode 100644 index 00000000000..51e735e7681 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs @@ -0,0 +1,102 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// 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 class CosmosRegexTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo IsMatch = + typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string) })!; + + 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; + + /// + /// 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 CosmosRegexTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// 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 SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method != IsMatch && method != IsMatchWithRegexOptions) + { + return null; + } + + var (input, pattern) = (arguments[0], arguments[1]); + var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); + + if (method == IsMatch) + { + return _sqlExpressionFactory.Function( + "RegexMatch", + new[] { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) + }, + typeof(bool)); + } + else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) + { + string modifier = ""; + if (regexOptions.HasFlag(RegexOptions.Multiline)) + { + modifier += "m"; + } + if (regexOptions.HasFlag(RegexOptions.Singleline)) + { + modifier += "s"; + } + if (regexOptions.HasFlag(RegexOptions.IgnoreCase)) + { + modifier += "i"; + } + if (regexOptions.HasFlag(RegexOptions.IgnorePatternWhitespace)) + { + modifier += "x"; + } + + return (regexOptions & ~SupportedOptions) == 0 + ? _sqlExpressionFactory.Function( + "RegexMatch", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + _sqlExpressionFactory.Constant(modifier) + }, + typeof(bool)) + : null; + } + + return null; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index 2e3d267856e..7529b91fa3d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -1,6 +1,7 @@ // 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.Azure.Cosmos; using Microsoft.EntityFrameworkCore.TestModels.Northwind; @@ -1204,20 +1205,124 @@ public override async Task Int_Compare_to_simple_zero(bool async) public override async Task Regex_IsMatch_MethodCall(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Regex_IsMatch_MethodCall(async)); + await base.Regex_IsMatch_MethodCall(async); - AssertSql(); + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T""))"); } public override async Task Regex_IsMatch_MethodCall_constant_input(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Regex_IsMatch_MethodCall_constant_input(async)); + await base.Regex_IsMatch_MethodCall_constant_input(async); - AssertSql(); + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(""ALFKI"", c[""CustomerID""]))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Option_None(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.None)), + entryCount: 6); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", """"))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Option_IgnoreCase(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase)), + entryCount: 6); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""i""))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Option_Multiline(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.Multiline)), + entryCount: 6); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""m""))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Option_Singleline(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.Singleline)), + entryCount: 6); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""s""))"); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Option_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnorePatternWhitespace)), + entryCount: 6); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""x""))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Regex_IsMatch_MethodCall_With_Options_IgnoreCase_And_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)), + entryCount: 6); + + AssertSql( + @"SELECT c +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(() => + Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.RightToLeft)).ToList()); + + [Fact] + public virtual void Regex_IsMatch_MethodCall_With_Any_Unsupported_Option() + => Assert.Throws(() => + Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.RightToLeft)).ToList()); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Case_insensitive_string_comparison_instance(bool async)