Skip to content

Commit

Permalink
Cosmos: Add translator for Regex.IsMatch method (#28121)
Browse files Browse the repository at this point in the history
Fixes #28078
  • Loading branch information
Marusyk authored May 30, 2022
1 parent 5da4122 commit bda79d4
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class ContainsTranslator : IMethodCallTranslator
public class CosmosContainsTranslator : IMethodCallTranslator
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;

Expand All @@ -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.
/// </summary>
public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
public CosmosContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class EqualsTranslator : IMethodCallTranslator
public class CosmosEqualsTranslator : IMethodCallTranslator
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;

Expand All @@ -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.
/// </summary>
public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
public CosmosEqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class MathTranslator : IMethodCallTranslator
public class CosmosMathTranslator : IMethodCallTranslator
{
private static readonly Dictionary<MethodInfo, string> SupportedMethodTranslations = new()
{
Expand Down Expand Up @@ -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.
/// </summary>
public MathTranslator(ISqlExpressionFactory sqlExpressionFactory)
public CosmosMathTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class RandomTranslator : IMethodCallTranslator
public class CosmosRandomTranslator : IMethodCallTranslator
{
private static readonly MethodInfo MethodInfo = typeof(DbFunctionsExtensions).GetRuntimeMethod(
nameof(DbFunctionsExtensions.Random), new[] { typeof(DbFunctions) });
Expand All @@ -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.
/// </summary>
public RandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
public CosmosRandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand Down
102 changes: 102 additions & 0 deletions src/EFCore.Cosmos/Query/Internal/CosmosRegexTranslator.cs
Original file line number Diff line number Diff line change
@@ -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;

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

/// <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 CosmosRegexTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

/// <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 SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Customer>().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<Customer>().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<Customer>().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<Customer>().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<Customer>().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<Customer>().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<InvalidOperationException>(() =>
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<InvalidOperationException>(() =>
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)
Expand Down

0 comments on commit bda79d4

Please sign in to comment.