Skip to content

Commit ca0ba38

Browse files
WhatzGamesroji
andauthored
Regex replace and count (#3062)
Closes #3059 Co-authored-by: Shay Rojansky <roji@roji.org>
1 parent 2397dd8 commit ca0ba38

File tree

6 files changed

+485
-77
lines changed

6 files changed

+485
-77
lines changed

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public NpgsqlMethodCallTranslatorProvider(
3333
{
3434
var npgsqlOptions = contextOptions.FindExtension<NpgsqlOptionsExtension>() ?? new NpgsqlOptionsExtension();
3535
var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14);
36+
var supportRegexCount = npgsqlOptions.PostgresVersion.AtLeast(15);
3637

3738
var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory;
3839
var typeMappingSource = (NpgsqlTypeMappingSource)dependencies.RelationalTypeMappingSource;
@@ -58,7 +59,7 @@ public NpgsqlMethodCallTranslatorProvider(
5859
new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory),
5960
new NpgsqlRandomTranslator(sqlExpressionFactory),
6061
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
61-
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
62+
new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount),
6263
new NpgsqlRowValueTranslator(sqlExpressionFactory),
6364
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
6465
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs

Lines changed: 0 additions & 72 deletions
This file was deleted.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.RegularExpressions;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
4+
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
5+
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
6+
7+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
8+
9+
/// <summary>
10+
/// Translates Regex method calls into their corresponding PostgreSQL equivalent for database-side processing.
11+
/// </summary>
12+
/// <remarks>
13+
/// http://www.postgresql.org/docs/current/static/functions-matching.html
14+
/// </remarks>
15+
public class NpgsqlRegexTranslator : IMethodCallTranslator
16+
{
17+
private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript;
18+
19+
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
20+
private readonly bool _supportRegexCount;
21+
private readonly NpgsqlTypeMappingSource _typeMappingSource;
22+
23+
/// <summary>
24+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
25+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
26+
/// any release. You should only use it directly in your code with extreme caution and knowing that
27+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
28+
/// </summary>
29+
public NpgsqlRegexTranslator(
30+
NpgsqlTypeMappingSource typeMappingSource,
31+
NpgsqlSqlExpressionFactory sqlExpressionFactory,
32+
bool supportRegexCount)
33+
{
34+
_sqlExpressionFactory = sqlExpressionFactory;
35+
_supportRegexCount = supportRegexCount;
36+
_typeMappingSource = typeMappingSource;
37+
}
38+
39+
/// <inheritdoc />
40+
public SqlExpression? Translate(
41+
SqlExpression? instance,
42+
MethodInfo method,
43+
IReadOnlyList<SqlExpression> arguments,
44+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
45+
{
46+
if (method.DeclaringType != typeof(Regex) || !method.IsStatic)
47+
{
48+
return null;
49+
}
50+
51+
return method.Name switch
52+
{
53+
nameof(Regex.IsMatch) when arguments.Count == 2
54+
&& arguments[0].Type == typeof(string)
55+
&& arguments[1].Type == typeof(string)
56+
=> TranslateIsMatch(arguments),
57+
nameof(Regex.IsMatch) when arguments.Count == 3
58+
&& arguments[0].Type == typeof(string)
59+
&& arguments[1].Type == typeof(string)
60+
&& TryGetOptions(arguments[2], out var options)
61+
=> TranslateIsMatch(arguments, options),
62+
63+
nameof(Regex.Replace) when arguments.Count == 3
64+
&& arguments[0].Type == typeof(string)
65+
&& arguments[1].Type == typeof(string)
66+
&& arguments[2].Type == typeof(string)
67+
=> TranslateReplace(arguments),
68+
nameof(Regex.Replace) when arguments.Count == 4
69+
&& arguments[0].Type == typeof(string)
70+
&& arguments[1].Type == typeof(string)
71+
&& arguments[2].Type == typeof(string)
72+
&& TryGetOptions(arguments[3], out var options)
73+
=> TranslateReplace(arguments, options),
74+
75+
nameof(Regex.Count) when _supportRegexCount
76+
&& arguments.Count == 2
77+
&& arguments[0].Type == typeof(string)
78+
&& arguments[1].Type == typeof(string)
79+
=> TranslateCount(arguments),
80+
nameof(Regex.Count) when _supportRegexCount
81+
&& arguments.Count == 3
82+
&& arguments[0].Type == typeof(string)
83+
&& arguments[1].Type == typeof(string)
84+
&& TryGetOptions(arguments[2], out var options)
85+
=> TranslateCount(arguments, options),
86+
87+
_ => null
88+
};
89+
90+
static bool TryGetOptions(SqlExpression argument, out RegexOptions options)
91+
{
92+
if (argument is SqlConstantExpression { Value: RegexOptions o } && (o & UnsupportedRegexOptions) is 0)
93+
{
94+
options = o;
95+
return true;
96+
}
97+
98+
options = default;
99+
return false;
100+
}
101+
}
102+
103+
private PgRegexMatchExpression TranslateIsMatch(IReadOnlyList<SqlExpression> arguments, RegexOptions regexOptions = RegexOptions.None)
104+
=> _sqlExpressionFactory.RegexMatch(
105+
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]),
106+
_sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]),
107+
regexOptions);
108+
109+
private SqlExpression TranslateReplace(IReadOnlyList<SqlExpression> arguments, RegexOptions regexOptions = RegexOptions.None)
110+
{
111+
var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]);
112+
113+
List<SqlExpression> passingArguments =
114+
[
115+
_sqlExpressionFactory.ApplyDefaultTypeMapping(input),
116+
_sqlExpressionFactory.ApplyDefaultTypeMapping(pattern),
117+
_sqlExpressionFactory.ApplyDefaultTypeMapping(replacement)
118+
];
119+
120+
if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions)
121+
{
122+
passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions));
123+
}
124+
125+
return _sqlExpressionFactory.Function(
126+
"regexp_replace",
127+
passingArguments,
128+
nullable: true,
129+
TrueArrays[passingArguments.Count],
130+
typeof(string),
131+
_typeMappingSource.FindMapping(typeof(string)));
132+
}
133+
134+
private SqlExpression TranslateCount(IReadOnlyList<SqlExpression> arguments, RegexOptions regexOptions = RegexOptions.None)
135+
{
136+
var (input, pattern) = (arguments[0], arguments[1]);
137+
138+
List<SqlExpression> passingArguments =
139+
[
140+
_sqlExpressionFactory.ApplyDefaultTypeMapping(input),
141+
_sqlExpressionFactory.ApplyDefaultTypeMapping(pattern)
142+
];
143+
144+
if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions)
145+
{
146+
passingArguments.AddRange(
147+
[
148+
_sqlExpressionFactory.Constant(1), // The starting position has to be set to use the options in PostgreSQL
149+
_sqlExpressionFactory.Constant(translatedOptions)
150+
]);
151+
}
152+
153+
return _sqlExpressionFactory.Function(
154+
"regexp_count",
155+
passingArguments,
156+
nullable: true,
157+
TrueArrays[passingArguments.Count],
158+
typeof(int),
159+
_typeMappingSource.FindMapping(typeof(int)));
160+
}
161+
162+
private static string TranslateOptions(RegexOptions options)
163+
{
164+
string? result;
165+
166+
switch (options)
167+
{
168+
case RegexOptions.Singleline:
169+
return string.Empty;
170+
case var _ when options.HasFlag(RegexOptions.Multiline):
171+
result = "n";
172+
break;
173+
case var _ when !options.HasFlag(RegexOptions.Singleline):
174+
result = "p";
175+
break;
176+
default:
177+
result = string.Empty;
178+
break;
179+
}
180+
181+
if (options.HasFlag(RegexOptions.IgnoreCase))
182+
{
183+
result += "i";
184+
}
185+
186+
if (options.HasFlag(RegexOptions.IgnorePatternWhitespace))
187+
{
188+
result += "x";
189+
}
190+
191+
return result;
192+
}
193+
}

src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -991,10 +991,6 @@ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression,
991991
Sql.Append("'");
992992
}
993993

994-
// Sql.Append(")' || ");
995-
// Visit(expression.Pattern);
996-
// Sql.Append(")");
997-
998994
return expression;
999995
}
1000996

src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Data;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Text;
4+
using System.Text.RegularExpressions;
35

46
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
57

0 commit comments

Comments
 (0)