Skip to content

Commit bda79d4

Browse files
authored
Cosmos: Add translator for Regex.IsMatch method (#28121)
Fixes #28078
1 parent 5da4122 commit bda79d4

File tree

7 files changed

+226
-18
lines changed

7 files changed

+226
-18
lines changed

src/EFCore.Cosmos/Query/Internal/ContainsTranslator.cs renamed to src/EFCore.Cosmos/Query/Internal/CosmosContainsTranslator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
99
/// any release. You should only use it directly in your code with extreme caution and knowing that
1010
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1111
/// </summary>
12-
public class ContainsTranslator : IMethodCallTranslator
12+
public class CosmosContainsTranslator : IMethodCallTranslator
1313
{
1414
private readonly ISqlExpressionFactory _sqlExpressionFactory;
1515

@@ -19,7 +19,7 @@ public class ContainsTranslator : IMethodCallTranslator
1919
/// any release. You should only use it directly in your code with extreme caution and knowing that
2020
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2121
/// </summary>
22-
public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
22+
public CosmosContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
2323
{
2424
_sqlExpressionFactory = sqlExpressionFactory;
2525
}

src/EFCore.Cosmos/Query/Internal/EqualsTranslator.cs renamed to src/EFCore.Cosmos/Query/Internal/CosmosEqualsTranslator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
99
/// any release. You should only use it directly in your code with extreme caution and knowing that
1010
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1111
/// </summary>
12-
public class EqualsTranslator : IMethodCallTranslator
12+
public class CosmosEqualsTranslator : IMethodCallTranslator
1313
{
1414
private readonly ISqlExpressionFactory _sqlExpressionFactory;
1515

@@ -19,7 +19,7 @@ public class EqualsTranslator : IMethodCallTranslator
1919
/// any release. You should only use it directly in your code with extreme caution and knowing that
2020
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2121
/// </summary>
22-
public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
22+
public CosmosEqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
2323
{
2424
_sqlExpressionFactory = sqlExpressionFactory;
2525
}

src/EFCore.Cosmos/Query/Internal/MathTranslator.cs renamed to src/EFCore.Cosmos/Query/Internal/CosmosMathTranslator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
99
/// any release. You should only use it directly in your code with extreme caution and knowing that
1010
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1111
/// </summary>
12-
public class MathTranslator : IMethodCallTranslator
12+
public class CosmosMathTranslator : IMethodCallTranslator
1313
{
1414
private static readonly Dictionary<MethodInfo, string> SupportedMethodTranslations = new()
1515
{
@@ -77,7 +77,7 @@ public class MathTranslator : IMethodCallTranslator
7777
/// any release. You should only use it directly in your code with extreme caution and knowing that
7878
/// doing so can result in application failures when updating to a new Entity Framework Core release.
7979
/// </summary>
80-
public MathTranslator(ISqlExpressionFactory sqlExpressionFactory)
80+
public CosmosMathTranslator(ISqlExpressionFactory sqlExpressionFactory)
8181
{
8282
_sqlExpressionFactory = sqlExpressionFactory;
8383
}

src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ public CosmosMethodCallTranslatorProvider(
2929
_translators.AddRange(
3030
new IMethodCallTranslator[]
3131
{
32-
new EqualsTranslator(sqlExpressionFactory),
32+
new CosmosEqualsTranslator(sqlExpressionFactory),
3333
new CosmosStringMethodTranslator(sqlExpressionFactory),
34-
new ContainsTranslator(sqlExpressionFactory),
35-
new RandomTranslator(sqlExpressionFactory),
36-
new MathTranslator(sqlExpressionFactory)
34+
new CosmosContainsTranslator(sqlExpressionFactory),
35+
new CosmosRandomTranslator(sqlExpressionFactory),
36+
new CosmosMathTranslator(sqlExpressionFactory),
37+
new CosmosRegexTranslator(sqlExpressionFactory)
3738
//new LikeTranslator(sqlExpressionFactory),
3839
//new EnumHasFlagTranslator(sqlExpressionFactory),
3940
//new GetValueOrDefaultTranslator(sqlExpressionFactory),

src/EFCore.Cosmos/Query/Internal/RandomTranslator.cs renamed to src/EFCore.Cosmos/Query/Internal/CosmosRandomTranslator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
1111
/// any release. You should only use it directly in your code with extreme caution and knowing that
1212
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1313
/// </summary>
14-
public class RandomTranslator : IMethodCallTranslator
14+
public class CosmosRandomTranslator : IMethodCallTranslator
1515
{
1616
private static readonly MethodInfo MethodInfo = typeof(DbFunctionsExtensions).GetRuntimeMethod(
1717
nameof(DbFunctionsExtensions.Random), new[] { typeof(DbFunctions) });
@@ -24,7 +24,7 @@ public class RandomTranslator : IMethodCallTranslator
2424
/// any release. You should only use it directly in your code with extreme caution and knowing that
2525
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2626
/// </summary>
27-
public RandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
27+
public CosmosRandomTranslator(ISqlExpressionFactory sqlExpressionFactory)
2828
{
2929
_sqlExpressionFactory = sqlExpressionFactory;
3030
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.RegularExpressions;
5+
6+
namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
7+
8+
/// <summary>
9+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
10+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
11+
/// any release. You should only use it directly in your code with extreme caution and knowing that
12+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
13+
/// </summary>
14+
public class CosmosRegexTranslator : IMethodCallTranslator
15+
{
16+
private static readonly MethodInfo IsMatch =
17+
typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string) })!;
18+
19+
private static readonly MethodInfo IsMatchWithRegexOptions =
20+
typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), new[] { typeof(string), typeof(string), typeof(RegexOptions) })!;
21+
22+
private const RegexOptions SupportedOptions = RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace;
23+
24+
private readonly ISqlExpressionFactory _sqlExpressionFactory;
25+
26+
/// <summary>
27+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
28+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
29+
/// any release. You should only use it directly in your code with extreme caution and knowing that
30+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
31+
/// </summary>
32+
public CosmosRegexTranslator(ISqlExpressionFactory sqlExpressionFactory)
33+
{
34+
_sqlExpressionFactory = sqlExpressionFactory;
35+
}
36+
37+
/// <summary>
38+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
39+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
40+
/// any release. You should only use it directly in your code with extreme caution and knowing that
41+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
42+
/// </summary>
43+
public virtual SqlExpression? Translate(
44+
SqlExpression? instance,
45+
MethodInfo method,
46+
IReadOnlyList<SqlExpression> arguments,
47+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
48+
{
49+
if (method != IsMatch && method != IsMatchWithRegexOptions)
50+
{
51+
return null;
52+
}
53+
54+
var (input, pattern) = (arguments[0], arguments[1]);
55+
var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern);
56+
57+
if (method == IsMatch)
58+
{
59+
return _sqlExpressionFactory.Function(
60+
"RegexMatch",
61+
new[] {
62+
_sqlExpressionFactory.ApplyTypeMapping(input, typeMapping),
63+
_sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping)
64+
},
65+
typeof(bool));
66+
}
67+
else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions })
68+
{
69+
string modifier = "";
70+
if (regexOptions.HasFlag(RegexOptions.Multiline))
71+
{
72+
modifier += "m";
73+
}
74+
if (regexOptions.HasFlag(RegexOptions.Singleline))
75+
{
76+
modifier += "s";
77+
}
78+
if (regexOptions.HasFlag(RegexOptions.IgnoreCase))
79+
{
80+
modifier += "i";
81+
}
82+
if (regexOptions.HasFlag(RegexOptions.IgnorePatternWhitespace))
83+
{
84+
modifier += "x";
85+
}
86+
87+
return (regexOptions & ~SupportedOptions) == 0
88+
? _sqlExpressionFactory.Function(
89+
"RegexMatch",
90+
new[]
91+
{
92+
_sqlExpressionFactory.ApplyTypeMapping(input, typeMapping),
93+
_sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping),
94+
_sqlExpressionFactory.Constant(modifier)
95+
},
96+
typeof(bool))
97+
: null;
98+
}
99+
100+
return null;
101+
}
102+
}

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.RegularExpressions;
45
using Microsoft.Azure.Cosmos;
56
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
67

@@ -1204,20 +1205,124 @@ public override async Task Int_Compare_to_simple_zero(bool async)
12041205

12051206
public override async Task Regex_IsMatch_MethodCall(bool async)
12061207
{
1207-
// Cosmos client evaluation. Issue #17246.
1208-
await AssertTranslationFailed(() => base.Regex_IsMatch_MethodCall(async));
1208+
await base.Regex_IsMatch_MethodCall(async);
12091209

1210-
AssertSql();
1210+
AssertSql(
1211+
@"SELECT c
1212+
FROM root c
1213+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T""))");
12111214
}
12121215

12131216
public override async Task Regex_IsMatch_MethodCall_constant_input(bool async)
12141217
{
1215-
// Cosmos client evaluation. Issue #17246.
1216-
await AssertTranslationFailed(() => base.Regex_IsMatch_MethodCall_constant_input(async));
1218+
await base.Regex_IsMatch_MethodCall_constant_input(async);
12171219

1218-
AssertSql();
1220+
AssertSql(
1221+
@"SELECT c
1222+
FROM root c
1223+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(""ALFKI"", c[""CustomerID""]))");
1224+
}
1225+
1226+
[ConditionalTheory]
1227+
[MemberData(nameof(IsAsyncData))]
1228+
public virtual async Task Regex_IsMatch_MethodCall_With_Option_None(bool async)
1229+
{
1230+
await AssertQuery(
1231+
async,
1232+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.None)),
1233+
entryCount: 6);
1234+
1235+
AssertSql(
1236+
@"SELECT c
1237+
FROM root c
1238+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", """"))");
1239+
}
1240+
1241+
[ConditionalTheory]
1242+
[MemberData(nameof(IsAsyncData))]
1243+
public virtual async Task Regex_IsMatch_MethodCall_With_Option_IgnoreCase(bool async)
1244+
{
1245+
await AssertQuery(
1246+
async,
1247+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase)),
1248+
entryCount: 6);
1249+
1250+
AssertSql(
1251+
@"SELECT c
1252+
FROM root c
1253+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""i""))");
1254+
}
1255+
1256+
[ConditionalTheory]
1257+
[MemberData(nameof(IsAsyncData))]
1258+
public virtual async Task Regex_IsMatch_MethodCall_With_Option_Multiline(bool async)
1259+
{
1260+
await AssertQuery(
1261+
async,
1262+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.Multiline)),
1263+
entryCount: 6);
1264+
1265+
AssertSql(
1266+
@"SELECT c
1267+
FROM root c
1268+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""m""))");
1269+
}
1270+
1271+
[ConditionalTheory]
1272+
[MemberData(nameof(IsAsyncData))]
1273+
public virtual async Task Regex_IsMatch_MethodCall_With_Option_Singleline(bool async)
1274+
{
1275+
await AssertQuery(
1276+
async,
1277+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.Singleline)),
1278+
entryCount: 6);
1279+
1280+
AssertSql(
1281+
@"SELECT c
1282+
FROM root c
1283+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""s""))");
12191284
}
12201285

1286+
[ConditionalTheory]
1287+
[MemberData(nameof(IsAsyncData))]
1288+
public virtual async Task Regex_IsMatch_MethodCall_With_Option_IgnorePatternWhitespace(bool async)
1289+
{
1290+
await AssertQuery(
1291+
async,
1292+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnorePatternWhitespace)),
1293+
entryCount: 6);
1294+
1295+
AssertSql(
1296+
@"SELECT c
1297+
FROM root c
1298+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""x""))");
1299+
}
1300+
1301+
[ConditionalTheory]
1302+
[MemberData(nameof(IsAsyncData))]
1303+
public virtual async Task Regex_IsMatch_MethodCall_With_Options_IgnoreCase_And_IgnorePatternWhitespace(bool async)
1304+
{
1305+
await AssertQuery(
1306+
async,
1307+
ss => ss.Set<Customer>().Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)),
1308+
entryCount: 6);
1309+
1310+
AssertSql(
1311+
@"SELECT c
1312+
FROM root c
1313+
WHERE ((c[""Discriminator""] = ""Customer"") AND RegexMatch(c[""CustomerID""], ""^T"", ""ix""))");
1314+
}
1315+
1316+
[Fact]
1317+
public virtual void Regex_IsMatch_MethodCall_With_Unsupported_Option()
1318+
=> Assert.Throws<InvalidOperationException>(() =>
1319+
Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.RightToLeft)).ToList());
1320+
1321+
[Fact]
1322+
public virtual void Regex_IsMatch_MethodCall_With_Any_Unsupported_Option()
1323+
=> Assert.Throws<InvalidOperationException>(() =>
1324+
Fixture.CreateContext().Customers.Where(o => Regex.IsMatch(o.CustomerID, "^T", RegexOptions.IgnoreCase | RegexOptions.RightToLeft)).ToList());
1325+
12211326
[ConditionalTheory]
12221327
[MemberData(nameof(IsAsyncData))]
12231328
public virtual async Task Case_insensitive_string_comparison_instance(bool async)

0 commit comments

Comments
 (0)