From 4bfdf0d369ada0a3045984a01d4ea8a8fadd9421 Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Mon, 15 Jan 2024 21:27:40 +0100 Subject: [PATCH 01/11] Added support for Regex.Replace --- .../NpgsqlMethodCallTranslatorProvider.cs | 2 +- .../Internal/NpgsqlRegexIsMatchTranslator.cs | 72 ------- .../Internal/NpgsqlRegexTranslator.cs | 183 ++++++++++++++++++ .../NorthwindFunctionsQueryNpgsqlTest.cs | 140 ++++++++++++++ 4 files changed, 324 insertions(+), 73 deletions(-) delete mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs create mode 100644 src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 46a19b4b5d..fbd43e59be 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -59,7 +59,7 @@ public NpgsqlMethodCallTranslatorProvider( new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlRandomTranslator(sqlExpressionFactory), new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), - new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory), + new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlRowValueTranslator(sqlExpressionFactory), new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs deleted file mode 100644 index 659d27b737..0000000000 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexIsMatchTranslator.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Text.RegularExpressions; -using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; - -/// -/// Translates Regex.IsMatch calls into PostgreSQL regex expressions for database-side processing. -/// -/// -/// http://www.postgresql.org/docs/current/static/functions-matching.html -/// -public class NpgsqlRegexIsMatchTranslator : IMethodCallTranslator -{ - private static readonly MethodInfo IsMatch = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string)])!; - - private static readonly MethodInfo IsMatchWithRegexOptions = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string), typeof(RegexOptions)])!; - - private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; - - private readonly NpgsqlSqlExpressionFactory _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 NpgsqlRegexIsMatchTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - 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); - - RegexOptions options; - - if (method == IsMatch) - { - options = RegexOptions.None; - } - else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) - { - options = regexOptions; - } - else - { - return null; // We don't support non-constant regex options - } - - return (options & UnsupportedRegexOptions) == 0 - ? _sqlExpressionFactory.RegexMatch( - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - options) - : null; - } -} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs new file mode 100644 index 0000000000..c994cbc71c --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -0,0 +1,183 @@ +using System.Text.RegularExpressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +/// +/// Translates Regex.IsMatch calls into PostgreSQL regex expressions for database-side processing. +/// +/// +/// http://www.postgresql.org/docs/current/static/functions-matching.html +/// +public class NpgsqlRegexTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo IsMatch = + typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string)])!; + + private static readonly MethodInfo IsMatchWithRegexOptions = + typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string), typeof(RegexOptions)])!; + + private static readonly MethodInfo Replace = + typeof(Regex).GetRuntimeMethod(nameof(Regex.Replace), [typeof(string), typeof(string), typeof(string)])!; + + private static readonly MethodInfo ReplaceWithRegexOptions = + typeof(Regex).GetRuntimeMethod(nameof(Regex.Replace), [typeof(string), typeof(string), typeof(string), typeof(RegexOptions)])!; + + private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; + + private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + private readonly NpgsqlTypeMappingSource _typeMappingSource; + + /// + /// 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 NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => TranslateIsMatch(instance, method, arguments, logger) + ?? TranslateIsRegexMatch(method, arguments, logger); + + /// + public virtual SqlExpression? TranslateIsMatch( + 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); + + RegexOptions options; + + if (method == IsMatch) + { + options = RegexOptions.None; + } + else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) + { + options = regexOptions; + } + else + { + return null; // We don't support non-constant regex options + } + + return (options & UnsupportedRegexOptions) == 0 + ? _sqlExpressionFactory.RegexMatch( + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + options) + : null; + } + + private SqlExpression? TranslateIsRegexMatch(MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) + { + if (method != Replace && method != ReplaceWithRegexOptions) + { + return null; + } + + var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]); + var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern, replacement); + + RegexOptions options; + + if (method == Replace) + { + options = RegexOptions.None; + } + else if (arguments[3] is SqlConstantExpression { Value: RegexOptions regexOptions }) + { + options = regexOptions; + } + else + { + return null; // We don't support non-constant regex options + } + + if ((options & UnsupportedRegexOptions) != 0) + { + return null; + } + + var translatedOptions = TranslateOptions(options); + + if (translatedOptions.Length > 0) + { + return _sqlExpressionFactory.Function( + "regexp_replace", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), + _sqlExpressionFactory.Constant(TranslateOptions(options)) + }, + nullable: true, + new[] { true, false, false, false }, + typeof(string), + _typeMappingSource.FindMapping(typeof(string))); + } + + return _sqlExpressionFactory.Function("regexp_replace", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) + }, + nullable: true, + new[] { true, false, false}, + typeof(string), + _typeMappingSource.FindMapping(typeof(string))); + } + + private static string TranslateOptions(RegexOptions options) + { + if (options is RegexOptions.Singleline) + { + return string.Empty; + } + + var result = string.Empty; + + if (options.HasFlag(RegexOptions.Multiline)) + { + result += "n"; + }else if(!options.HasFlag(RegexOptions.Singleline)) + { + result += "p"; + } + + if (options.HasFlag(RegexOptions.IgnoreCase)) + { + result += "i"; + } + + if (options.HasFlag(RegexOptions.IgnorePatternWhitespace)) + { + result += "x"; + } + + return result; + } + +} diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs index f74474e828..e5f5993f4f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs @@ -240,6 +240,146 @@ public void Regex_IsMatch_with_unsupported_option() () => Fixture.CreateContext().Customers.Where(c => Regex.IsMatch(c.CompanyName, "^A", RegexOptions.RightToLeft)).ToList()); + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_constant_pattern_and_replacement(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B"))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_parameter_pattern_and_replacement(bool async) + { + var pattern = "^A"; + var replacement = "B"; + + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, pattern, replacement))); + + AssertSql( + """ + @__pattern_0='^A' + @__replacement_1='B' + + SELECT regexp_replace(c."CompanyName", @__pattern_0, @__replacement_1, 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_OptionsNone(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.None))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_IgnoreCase(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.IgnoreCase))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^a', 'B', 'pi') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Multiline(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Multiline))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^A', 'B', 'pn') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Singleline(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^A", "B", RegexOptions.Singleline))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^A', 'B') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_Singleline_and_IgnoreCase(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.Singleline | RegexOptions.IgnoreCase))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^a', 'B', 'i') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Regex_Replace_with_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^ A", "B", RegexOptions.IgnorePatternWhitespace))); + + AssertSql( + """ + SELECT regexp_replace(c."CompanyName", '^ A', 'B', 'px') + FROM "Customers" AS c + """ + ); + } + + [Fact] + public void Regex_Replace_with_unsupported_option() + => Assert.Throws( + () => Fixture.CreateContext().Customers + .FirstOrDefault(x => Regex.Replace(x.CompanyName, "^A", "foo", RegexOptions.RightToLeft) != null)); + #endregion Regex #region Guid From ff41eda2228536a6049842b73bf848e80f05a67d Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Mon, 15 Jan 2024 22:45:56 +0100 Subject: [PATCH 02/11] Added support for regex Regex.Count --- .../NpgsqlMethodCallTranslatorProvider.cs | 3 +- .../Internal/NpgsqlRegexTranslator.cs | 88 ++++++++++- .../NorthwindFunctionsQueryNpgsqlTest.cs | 149 +++++++++++++++++- 3 files changed, 234 insertions(+), 6 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index fbd43e59be..6cec82a95c 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -33,6 +33,7 @@ public NpgsqlMethodCallTranslatorProvider( { var npgsqlOptions = contextOptions.FindExtension() ?? new NpgsqlOptionsExtension(); var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14); + var supportRegexCount = npgsqlOptions.PostgresVersion.AtLeast(15); var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory; var typeMappingSource = (NpgsqlTypeMappingSource)dependencies.RelationalTypeMappingSource; @@ -59,7 +60,7 @@ public NpgsqlMethodCallTranslatorProvider( new NpgsqlObjectToStringTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlRandomTranslator(sqlExpressionFactory), new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges), - new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory), + new NpgsqlRegexTranslator(typeMappingSource, sqlExpressionFactory, supportRegexCount), new NpgsqlRowValueTranslator(sqlExpressionFactory), new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory), new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model), diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index c994cbc71c..400b0bc6a7 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -24,9 +24,16 @@ public class NpgsqlRegexTranslator : IMethodCallTranslator private static readonly MethodInfo ReplaceWithRegexOptions = typeof(Regex).GetRuntimeMethod(nameof(Regex.Replace), [typeof(string), typeof(string), typeof(string), typeof(RegexOptions)])!; + private static readonly MethodInfo Count = + typeof(Regex).GetRuntimeMethod(nameof(Regex.Count), [typeof(string), typeof(string)])!; + + private static readonly MethodInfo CountWithRegexOptions = + typeof(Regex).GetRuntimeMethod(nameof(Regex.Count), [typeof(string), typeof(string), typeof(RegexOptions)])!; + private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + private readonly bool _supportRegexCount; private readonly NpgsqlTypeMappingSource _typeMappingSource; /// @@ -35,9 +42,13 @@ public class NpgsqlRegexTranslator : 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 NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory) + public NpgsqlRegexTranslator( + NpgsqlTypeMappingSource typeMappingSource, + NpgsqlSqlExpressionFactory sqlExpressionFactory, + bool supportRegexCount) { _sqlExpressionFactory = sqlExpressionFactory; + _supportRegexCount = supportRegexCount; _typeMappingSource = typeMappingSource; } @@ -48,7 +59,8 @@ public NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSq IReadOnlyList arguments, IDiagnosticsLogger logger) => TranslateIsMatch(instance, method, arguments, logger) - ?? TranslateIsRegexMatch(method, arguments, logger); + ?? TranslateRegexReplace(method, arguments, logger) + ?? TranslateCount(method, arguments, logger); /// public virtual SqlExpression? TranslateIsMatch( @@ -88,7 +100,10 @@ public NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSq : null; } - private SqlExpression? TranslateIsRegexMatch(MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) + private SqlExpression? TranslateRegexReplace( + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) { if (method != Replace && method != ReplaceWithRegexOptions) { @@ -129,7 +144,7 @@ public NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSq _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), - _sqlExpressionFactory.Constant(TranslateOptions(options)) + _sqlExpressionFactory.Constant(translatedOptions) }, nullable: true, new[] { true, false, false, false }, @@ -150,6 +165,71 @@ public NpgsqlRegexTranslator(NpgsqlTypeMappingSource typeMappingSource, NpgsqlSq _typeMappingSource.FindMapping(typeof(string))); } + private SqlExpression? TranslateCount( + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (!_supportRegexCount || (method != Count && method != CountWithRegexOptions)) + { + return null; + } + + var (input, pattern) = (arguments[0], arguments[1]); + var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); + + RegexOptions options; + + if (method == Count) + { + options = RegexOptions.None; + } + else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) + { + options = regexOptions; + } + else + { + return null; // We don't support non-constant regex options + } + + if ((options & UnsupportedRegexOptions) != 0) + { + return null; + } + + var translatedOptions = TranslateOptions(options); + + if (translatedOptions.Length is 0) + { + return _sqlExpressionFactory.Function( + "regexp_count", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) + }, + nullable: true, + new[] { true, false }, + typeof(int), + _typeMappingSource.FindMapping(typeof(int))); + } + + return _sqlExpressionFactory.Function( + "regexp_count", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + _sqlExpressionFactory.Constant(1), + _sqlExpressionFactory.Constant(translatedOptions) + }, + nullable: true, + new[] { true, false, false, false }, + typeof(int), + _typeMappingSource.FindMapping(typeof(int))); + } + private static string TranslateOptions(RegexOptions options) { if (options is RegexOptions.Singleline) diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs index e5f5993f4f..ff18af8d8a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs @@ -320,7 +320,7 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^A', 'B', 'pn') + SELECT regexp_replace(c."CompanyName", '^A', 'B', 'n') FROM "Customers" AS c """ ); @@ -380,6 +380,153 @@ public void Regex_Replace_with_unsupported_option() () => Fixture.CreateContext().Customers .FirstOrDefault(x => Regex.Replace(x.CompanyName, "^A", "foo", RegexOptions.RightToLeft) != null)); + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_constant_pattern(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A"))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^A', 1, 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_parameter_pattern(bool async) + { + var pattern = "^A"; + + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, pattern))); + + AssertSql( + """ + @__pattern_0='^A' + + SELECT regexp_count(c."CompanyName", @__pattern_0, 1, 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_OptionsNone(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.None))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^A', 1, 'p') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_IgnoreCase(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.IgnoreCase))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Multiline(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Multiline))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^A', 1, 'n') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Singleline(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^A", RegexOptions.Singleline))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^A') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_Singleline_and_IgnoreCase(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^a", RegexOptions.Singleline | RegexOptions.IgnoreCase))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^a', 1, 'i') + FROM "Customers" AS c + """ + ); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + [MinimumPostgresVersion(15, 0)] + public async Task Regex_Count_with_IgnorePatternWhitespace(bool async) + { + await AssertQuery( + async, + cs => cs.Set().Select(c => Regex.Count(c.CompanyName, "^ A", RegexOptions.IgnorePatternWhitespace))); + + AssertSql( + """ + SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') + FROM "Customers" AS c + """ + ); + } + + [Fact] + [MinimumPostgresVersion(15, 0)] + public void Regex_Count_with_unsupported_option() + => Assert.Throws( + () => Fixture.CreateContext().Customers + .FirstOrDefault(x => Regex.Count(x.CompanyName, "^A", RegexOptions.RightToLeft) != 0)); + #endregion Regex #region Guid From ac9b36d23808e446c55b17598167fba8a4623ece Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Tue, 16 Jan 2024 00:10:15 +0100 Subject: [PATCH 03/11] some minor cleanup and comment adjustments --- .../ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 400b0bc6a7..2732a609e7 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; /// -/// Translates Regex.IsMatch calls into PostgreSQL regex expressions for database-side processing. +/// Translates Regex method calls into their corresponding PostgreSQL equivalent for database-side processing. /// /// /// http://www.postgresql.org/docs/current/static/functions-matching.html @@ -62,8 +62,7 @@ public NpgsqlRegexTranslator( ?? TranslateRegexReplace(method, arguments, logger) ?? TranslateCount(method, arguments, logger); - /// - public virtual SqlExpression? TranslateIsMatch( + private SqlExpression? TranslateIsMatch( SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, @@ -221,6 +220,7 @@ public NpgsqlRegexTranslator( { _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + //starting position has to be set to use the options in postgres _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(translatedOptions) }, From fc4e3bbb28919edabeb2fa8a3b540e198ba7633f Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Wed, 3 Apr 2024 00:30:44 +0200 Subject: [PATCH 04/11] corrected nullability for translation --- .../Internal/NpgsqlRegexTranslator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 2732a609e7..e8d3deb890 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -146,7 +146,7 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.Constant(translatedOptions) }, nullable: true, - new[] { true, false, false, false }, + new[] { true, true, true, true }, typeof(string), _typeMappingSource.FindMapping(typeof(string))); } @@ -159,7 +159,7 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) }, nullable: true, - new[] { true, false, false}, + new[] { true, true, true}, typeof(string), _typeMappingSource.FindMapping(typeof(string))); } @@ -209,8 +209,8 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) }, nullable: true, - new[] { true, false }, - typeof(int), + new[] { true, true }, + typeof(int?), _typeMappingSource.FindMapping(typeof(int))); } @@ -225,8 +225,8 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.Constant(translatedOptions) }, nullable: true, - new[] { true, false, false, false }, - typeof(int), + new[] { true, true, true, true }, + typeof(int?), _typeMappingSource.FindMapping(typeof(int))); } From 0a2debd63f233a8252a3899e56172f192a127c36 Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Wed, 3 Apr 2024 01:04:18 +0200 Subject: [PATCH 05/11] reordered condition --- .../Internal/NpgsqlRegexTranslator.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index e8d3deb890..2661145b9f 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -134,32 +134,32 @@ public NpgsqlRegexTranslator( var translatedOptions = TranslateOptions(options); - if (translatedOptions.Length > 0) + if (translatedOptions.Length is 0) { - return _sqlExpressionFactory.Function( - "regexp_replace", + return _sqlExpressionFactory.Function("regexp_replace", new[] { _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), - _sqlExpressionFactory.Constant(translatedOptions) + _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) }, nullable: true, - new[] { true, true, true, true }, + new[] { true, true, true}, typeof(string), _typeMappingSource.FindMapping(typeof(string))); } - return _sqlExpressionFactory.Function("regexp_replace", + return _sqlExpressionFactory.Function( + "regexp_replace", new[] { _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) + _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), + _sqlExpressionFactory.Constant(translatedOptions) }, nullable: true, - new[] { true, true, true}, + new[] { true, true, true, true }, typeof(string), _typeMappingSource.FindMapping(typeof(string))); } From ff79e362d70f221edfc81219eca4926e7cf60e2c Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Wed, 3 Apr 2024 01:24:16 +0200 Subject: [PATCH 06/11] switched to conditional execution for regexp__count tests --- .../NorthwindFunctionsQueryNpgsqlTest.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs index ff18af8d8a..b75a5a2101 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs @@ -253,7 +253,7 @@ await AssertQuery( SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') FROM "Customers" AS c """ - ); + ); } [Theory] @@ -275,7 +275,7 @@ await AssertQuery( SELECT regexp_replace(c."CompanyName", @__pattern_0, @__replacement_1, 'p') FROM "Customers" AS c """ - ); + ); } [Theory] @@ -348,7 +348,8 @@ public async Task Regex_Replace_with_Singleline_and_IgnoreCase(bool async) { await AssertQuery( async, - source => source.Set().Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.Singleline | RegexOptions.IgnoreCase))); + source => source.Set() + .Select(x => Regex.Replace(x.CompanyName, "^a", "B", RegexOptions.Singleline | RegexOptions.IgnoreCase))); AssertSql( """ @@ -380,7 +381,7 @@ public void Regex_Replace_with_unsupported_option() () => Fixture.CreateContext().Customers .FirstOrDefault(x => Regex.Replace(x.CompanyName, "^A", "foo", RegexOptions.RightToLeft) != null)); - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_constant_pattern(bool async) @@ -397,7 +398,7 @@ SELECT regexp_count(c."CompanyName", '^A', 1, 'p') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_parameter_pattern(bool async) @@ -418,7 +419,7 @@ SELECT regexp_count(c."CompanyName", @__pattern_0, 1, 'p') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_OptionsNone(bool async) @@ -435,7 +436,7 @@ SELECT regexp_count(c."CompanyName", '^A', 1, 'p') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_IgnoreCase(bool async) @@ -452,7 +453,7 @@ SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_Multiline(bool async) @@ -469,7 +470,7 @@ SELECT regexp_count(c."CompanyName", '^A', 1, 'n') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_Singleline(bool async) @@ -486,7 +487,7 @@ SELECT regexp_count(c."CompanyName", '^A') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_Singleline_and_IgnoreCase(bool async) @@ -503,7 +504,7 @@ SELECT regexp_count(c."CompanyName", '^a', 1, 'i') ); } - [Theory] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] [MinimumPostgresVersion(15, 0)] public async Task Regex_Count_with_IgnorePatternWhitespace(bool async) @@ -520,7 +521,7 @@ SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') ); } - [Fact] + [ConditionalFact] [MinimumPostgresVersion(15, 0)] public void Regex_Count_with_unsupported_option() => Assert.Throws( From 98113c317e95c0ccbb57e2372e7cc6560714b929 Mon Sep 17 00:00:00 2001 From: Obed Kooijman Date: Sat, 14 Dec 2024 22:00:05 +0100 Subject: [PATCH 07/11] use collection expression --- .../Internal/NpgsqlRegexTranslator.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 2661145b9f..6614823a2e 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -137,29 +137,27 @@ public NpgsqlRegexTranslator( if (translatedOptions.Length is 0) { return _sqlExpressionFactory.Function("regexp_replace", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) - }, + ], nullable: true, - new[] { true, true, true}, + [true, true, true], typeof(string), _typeMappingSource.FindMapping(typeof(string))); } return _sqlExpressionFactory.Function( "regexp_replace", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), _sqlExpressionFactory.Constant(translatedOptions) - }, + ], nullable: true, - new[] { true, true, true, true }, + [true, true, true, true], typeof(string), _typeMappingSource.FindMapping(typeof(string))); } @@ -203,29 +201,27 @@ public NpgsqlRegexTranslator( { return _sqlExpressionFactory.Function( "regexp_count", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) - }, + ], nullable: true, - new[] { true, true }, + [true, true], typeof(int?), _typeMappingSource.FindMapping(typeof(int))); } return _sqlExpressionFactory.Function( "regexp_count", - new[] - { + [ _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), //starting position has to be set to use the options in postgres _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(translatedOptions) - }, + ], nullable: true, - new[] { true, true, true, true }, + [true, true, true, true], typeof(int?), _typeMappingSource.FindMapping(typeof(int))); } From f854f30e22e06b0cb13c0199e555d219528c01b0 Mon Sep 17 00:00:00 2001 From: Obed Kooijman Date: Sat, 14 Dec 2024 23:18:22 +0100 Subject: [PATCH 08/11] some minor cleanup --- .../Internal/NpgsqlRegexTranslator.cs | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 6614823a2e..fdddc2a785 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -132,32 +132,26 @@ public NpgsqlRegexTranslator( return null; } + + List passingArguments = [ + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) + ]; + + var translatedOptions = TranslateOptions(options); - if (translatedOptions.Length is 0) + if (translatedOptions.Length is not 0) { - return _sqlExpressionFactory.Function("regexp_replace", - [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) - ], - nullable: true, - [true, true, true], - typeof(string), - _typeMappingSource.FindMapping(typeof(string))); + passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions)); } return _sqlExpressionFactory.Function( "regexp_replace", - [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping), - _sqlExpressionFactory.Constant(translatedOptions) - ], + passingArguments, nullable: true, - [true, true, true, true], + Enumerable.Repeat(true, passingArguments.Count), typeof(string), _typeMappingSource.FindMapping(typeof(string))); } @@ -195,33 +189,27 @@ public NpgsqlRegexTranslator( return null; } + List passingArguments = [ + _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), + _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) + ]; + var translatedOptions = TranslateOptions(options); - if (translatedOptions.Length is 0) + if (translatedOptions.Length is not 0) { - return _sqlExpressionFactory.Function( - "regexp_count", - [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) - ], - nullable: true, - [true, true], - typeof(int?), - _typeMappingSource.FindMapping(typeof(int))); + passingArguments.AddRange([ + //starting position has to be set to use the options in postgres + _sqlExpressionFactory.Constant(1), + _sqlExpressionFactory.Constant(translatedOptions) + ]); } return _sqlExpressionFactory.Function( "regexp_count", - [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - //starting position has to be set to use the options in postgres - _sqlExpressionFactory.Constant(1), - _sqlExpressionFactory.Constant(translatedOptions) - ], + passingArguments, nullable: true, - [true, true, true, true], + Enumerable.Repeat(true, passingArguments.Count), typeof(int?), _typeMappingSource.FindMapping(typeof(int))); } @@ -233,14 +221,18 @@ private static string TranslateOptions(RegexOptions options) return string.Empty; } - var result = string.Empty; + string? result; if (options.HasFlag(RegexOptions.Multiline)) { - result += "n"; + result = "n"; }else if(!options.HasFlag(RegexOptions.Singleline)) { - result += "p"; + result = "p"; + } + else + { + result = string.Empty; } if (options.HasFlag(RegexOptions.IgnoreCase)) From 382d1bcd4c463cef0ffbab130d0cacee56514f2d Mon Sep 17 00:00:00 2001 From: Obed Kooijman Date: Sun, 15 Dec 2024 00:32:17 +0100 Subject: [PATCH 09/11] cleanup regarding options --- .../Internal/NpgsqlRegexTranslator.cs | 83 +++++++------------ 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index fdddc2a785..01f6c83db7 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; @@ -76,26 +77,11 @@ public NpgsqlRegexTranslator( var (input, pattern) = (arguments[0], arguments[1]); var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); - RegexOptions options; - - if (method == IsMatch) - { - options = RegexOptions.None; - } - else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) - { - options = regexOptions; - } - else - { - return null; // We don't support non-constant regex options - } - - return (options & UnsupportedRegexOptions) == 0 + return TryGetRegexOptions(arguments, 2, out var regexOptions) ? _sqlExpressionFactory.RegexMatch( _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - options) + regexOptions.Value) : null; } @@ -112,35 +98,18 @@ public NpgsqlRegexTranslator( var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]); var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern, replacement); - RegexOptions options; - - if (method == Replace) - { - options = RegexOptions.None; - } - else if (arguments[3] is SqlConstantExpression { Value: RegexOptions regexOptions }) - { - options = regexOptions; - } - else - { - return null; // We don't support non-constant regex options - } - - if ((options & UnsupportedRegexOptions) != 0) + if (!TryGetRegexOptions(arguments, 3, out var regexOptions)) { return null; } - List passingArguments = [ _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) ]; - - var translatedOptions = TranslateOptions(options); + var translatedOptions = TranslateOptions(regexOptions.Value); if (translatedOptions.Length is not 0) { @@ -169,22 +138,7 @@ public NpgsqlRegexTranslator( var (input, pattern) = (arguments[0], arguments[1]); var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); - RegexOptions options; - - if (method == Count) - { - options = RegexOptions.None; - } - else if (arguments[2] is SqlConstantExpression { Value: RegexOptions regexOptions }) - { - options = regexOptions; - } - else - { - return null; // We don't support non-constant regex options - } - - if ((options & UnsupportedRegexOptions) != 0) + if (!TryGetRegexOptions(arguments, 2, out var regexOptions)) { return null; } @@ -194,7 +148,7 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) ]; - var translatedOptions = TranslateOptions(options); + var translatedOptions = TranslateOptions(regexOptions.Value); if (translatedOptions.Length is not 0) { @@ -214,6 +168,29 @@ public NpgsqlRegexTranslator( _typeMappingSource.FindMapping(typeof(int))); } + private static bool TryGetRegexOptions( + IReadOnlyList arguments, + int minArguments, + [NotNullWhen(true)] out RegexOptions? options) + { + if (arguments.Count == minArguments) + { + options = RegexOptions.None; + } + else if (arguments[minArguments] is SqlConstantExpression { Value: RegexOptions regexOptions } + && (regexOptions & UnsupportedRegexOptions) is 0) + { + options = regexOptions; + } + else + { + options = null; + return false; + } + + return true; + } + private static string TranslateOptions(RegexOptions options) { if (options is RegexOptions.Singleline) From 4b83cdc9d4574e9742f685f37068c7301933aeb5 Mon Sep 17 00:00:00 2001 From: Obed Kooijman Date: Sun, 15 Dec 2024 00:49:41 +0100 Subject: [PATCH 10/11] more minor cleanup --- .../Internal/NpgsqlRegexTranslator.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 01f6c83db7..88f1350271 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -60,7 +60,7 @@ public NpgsqlRegexTranslator( IReadOnlyList arguments, IDiagnosticsLogger logger) => TranslateIsMatch(instance, method, arguments, logger) - ?? TranslateRegexReplace(method, arguments, logger) + ?? TranslateReplace(method, arguments, logger) ?? TranslateCount(method, arguments, logger); private SqlExpression? TranslateIsMatch( @@ -85,7 +85,7 @@ public NpgsqlRegexTranslator( : null; } - private SqlExpression? TranslateRegexReplace( + private SqlExpression? TranslateReplace( MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) @@ -109,9 +109,7 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) ]; - var translatedOptions = TranslateOptions(regexOptions.Value); - - if (translatedOptions.Length is not 0) + if (TranslateOptions(regexOptions.Value) is { Length: not 0 } translatedOptions) { passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions)); } @@ -148,9 +146,7 @@ public NpgsqlRegexTranslator( _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) ]; - var translatedOptions = TranslateOptions(regexOptions.Value); - - if (translatedOptions.Length is not 0) + if (TranslateOptions(regexOptions.Value) is { Length: not 0 } translatedOptions) { passingArguments.AddRange([ //starting position has to be set to use the options in postgres From bfc73bb2ab501814056cb45740ec3ae0f25aef94 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 15 Dec 2024 10:34:56 +0100 Subject: [PATCH 11/11] Tweaks and cleanup --- .../Internal/NpgsqlRegexTranslator.cs | 213 ++++++++---------- .../Query/Internal/NpgsqlQuerySqlGenerator.cs | 4 - .../Internal/NpgsqlSqlGenerationHelper.cs | 2 + .../NorthwindFunctionsQueryNpgsqlTest.cs | 102 ++++----- 4 files changed, 144 insertions(+), 177 deletions(-) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs index 88f1350271..3407bef163 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlRegexTranslator.cs @@ -1,7 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; +using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; @@ -13,24 +14,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte /// public class NpgsqlRegexTranslator : IMethodCallTranslator { - private static readonly MethodInfo IsMatch = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string)])!; - - private static readonly MethodInfo IsMatchWithRegexOptions = - typeof(Regex).GetRuntimeMethod(nameof(Regex.IsMatch), [typeof(string), typeof(string), typeof(RegexOptions)])!; - - private static readonly MethodInfo Replace = - typeof(Regex).GetRuntimeMethod(nameof(Regex.Replace), [typeof(string), typeof(string), typeof(string)])!; - - private static readonly MethodInfo ReplaceWithRegexOptions = - typeof(Regex).GetRuntimeMethod(nameof(Regex.Replace), [typeof(string), typeof(string), typeof(string), typeof(RegexOptions)])!; - - private static readonly MethodInfo Count = - typeof(Regex).GetRuntimeMethod(nameof(Regex.Count), [typeof(string), typeof(string)])!; - - private static readonly MethodInfo CountWithRegexOptions = - typeof(Regex).GetRuntimeMethod(nameof(Regex.Count), [typeof(string), typeof(string), typeof(RegexOptions)])!; - private const RegexOptions UnsupportedRegexOptions = RegexOptions.RightToLeft | RegexOptions.ECMAScript; private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; @@ -59,57 +42,82 @@ public NpgsqlRegexTranslator( MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) - => TranslateIsMatch(instance, method, arguments, logger) - ?? TranslateReplace(method, arguments, logger) - ?? TranslateCount(method, arguments, logger); - - private SqlExpression? TranslateIsMatch( - SqlExpression? instance, - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) { - if (method != IsMatch && method != IsMatchWithRegexOptions) + if (method.DeclaringType != typeof(Regex) || !method.IsStatic) { return null; } - var (input, pattern) = (arguments[0], arguments[1]); - var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); - - return TryGetRegexOptions(arguments, 2, out var regexOptions) - ? _sqlExpressionFactory.RegexMatch( - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - regexOptions.Value) - : null; + return method.Name switch + { + nameof(Regex.IsMatch) when arguments.Count == 2 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + => TranslateIsMatch(arguments), + nameof(Regex.IsMatch) when arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && TryGetOptions(arguments[2], out var options) + => TranslateIsMatch(arguments, options), + + nameof(Regex.Replace) when arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && arguments[2].Type == typeof(string) + => TranslateReplace(arguments), + nameof(Regex.Replace) when arguments.Count == 4 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && arguments[2].Type == typeof(string) + && TryGetOptions(arguments[3], out var options) + => TranslateReplace(arguments, options), + + nameof(Regex.Count) when _supportRegexCount + && arguments.Count == 2 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + => TranslateCount(arguments), + nameof(Regex.Count) when _supportRegexCount + && arguments.Count == 3 + && arguments[0].Type == typeof(string) + && arguments[1].Type == typeof(string) + && TryGetOptions(arguments[2], out var options) + => TranslateCount(arguments, options), + + _ => null + }; + + static bool TryGetOptions(SqlExpression argument, out RegexOptions options) + { + if (argument is SqlConstantExpression { Value: RegexOptions o } && (o & UnsupportedRegexOptions) is 0) + { + options = o; + return true; + } + + options = default; + return false; + } } - private SqlExpression? TranslateReplace( - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) - { - if (method != Replace && method != ReplaceWithRegexOptions) - { - return null; - } + private PgRegexMatchExpression TranslateIsMatch(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) + => _sqlExpressionFactory.RegexMatch( + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[0]), + _sqlExpressionFactory.ApplyDefaultTypeMapping(arguments[1]), + regexOptions); + private SqlExpression TranslateReplace(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) + { var (input, pattern, replacement) = (arguments[0], arguments[1], arguments[2]); - var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern, replacement); - if (!TryGetRegexOptions(arguments, 3, out var regexOptions)) - { - return null; - } - - List passingArguments = [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(replacement, typeMapping) + List passingArguments = + [ + _sqlExpressionFactory.ApplyDefaultTypeMapping(input), + _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern), + _sqlExpressionFactory.ApplyDefaultTypeMapping(replacement) ]; - if (TranslateOptions(regexOptions.Value) is { Length: not 0 } translatedOptions) + if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) { passingArguments.Add(_sqlExpressionFactory.Constant(translatedOptions)); } @@ -118,39 +126,26 @@ public NpgsqlRegexTranslator( "regexp_replace", passingArguments, nullable: true, - Enumerable.Repeat(true, passingArguments.Count), + TrueArrays[passingArguments.Count], typeof(string), _typeMappingSource.FindMapping(typeof(string))); } - private SqlExpression? TranslateCount( - MethodInfo method, - IReadOnlyList arguments, - IDiagnosticsLogger logger) + private SqlExpression TranslateCount(IReadOnlyList arguments, RegexOptions regexOptions = RegexOptions.None) { - if (!_supportRegexCount || (method != Count && method != CountWithRegexOptions)) - { - return null; - } - var (input, pattern) = (arguments[0], arguments[1]); - var typeMapping = ExpressionExtensions.InferTypeMapping(input, pattern); - - if (!TryGetRegexOptions(arguments, 2, out var regexOptions)) - { - return null; - } - List passingArguments = [ - _sqlExpressionFactory.ApplyTypeMapping(input, typeMapping), - _sqlExpressionFactory.ApplyTypeMapping(pattern, typeMapping) + List passingArguments = + [ + _sqlExpressionFactory.ApplyDefaultTypeMapping(input), + _sqlExpressionFactory.ApplyDefaultTypeMapping(pattern) ]; - if (TranslateOptions(regexOptions.Value) is { Length: not 0 } translatedOptions) + if (TranslateOptions(regexOptions) is { Length: not 0 } translatedOptions) { - passingArguments.AddRange([ - //starting position has to be set to use the options in postgres - _sqlExpressionFactory.Constant(1), + passingArguments.AddRange( + [ + _sqlExpressionFactory.Constant(1), // The starting position has to be set to use the options in PostgreSQL _sqlExpressionFactory.Constant(translatedOptions) ]); } @@ -159,53 +154,28 @@ public NpgsqlRegexTranslator( "regexp_count", passingArguments, nullable: true, - Enumerable.Repeat(true, passingArguments.Count), - typeof(int?), + TrueArrays[passingArguments.Count], + typeof(int), _typeMappingSource.FindMapping(typeof(int))); } - private static bool TryGetRegexOptions( - IReadOnlyList arguments, - int minArguments, - [NotNullWhen(true)] out RegexOptions? options) - { - if (arguments.Count == minArguments) - { - options = RegexOptions.None; - } - else if (arguments[minArguments] is SqlConstantExpression { Value: RegexOptions regexOptions } - && (regexOptions & UnsupportedRegexOptions) is 0) - { - options = regexOptions; - } - else - { - options = null; - return false; - } - - return true; - } - private static string TranslateOptions(RegexOptions options) { - if (options is RegexOptions.Singleline) - { - return string.Empty; - } - string? result; - if (options.HasFlag(RegexOptions.Multiline)) - { - result = "n"; - }else if(!options.HasFlag(RegexOptions.Singleline)) - { - result = "p"; - } - else - { - result = string.Empty; + switch (options) + { + case RegexOptions.Singleline: + return string.Empty; + case var _ when options.HasFlag(RegexOptions.Multiline): + result = "n"; + break; + case var _ when !options.HasFlag(RegexOptions.Singleline): + result = "p"; + break; + default: + result = string.Empty; + break; } if (options.HasFlag(RegexOptions.IgnoreCase)) @@ -220,5 +190,4 @@ private static string TranslateOptions(RegexOptions options) return result; } - } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 5c1ed24f12..5de8f23be2 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -991,10 +991,6 @@ protected virtual Expression VisitRegexMatch(PgRegexMatchExpression expression, Sql.Append("'"); } - // Sql.Append(")' || "); - // Visit(expression.Pattern); - // Sql.Append(")"); - return expression; } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs b/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs index bbff1a515c..89eddbdc0c 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlSqlGenerationHelper.cs @@ -1,5 +1,7 @@ using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.RegularExpressions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs index 85b4bd8c7c..5c422ffed6 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindFunctionsQueryNpgsqlTest.cs @@ -292,9 +292,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') +FROM "Customers" AS c +""" ); } @@ -311,12 +311,12 @@ await AssertQuery( AssertSql( """ - @__pattern_0='^A' - @__replacement_1='B' +@__pattern_0='^A' +@__replacement_1='B' - SELECT regexp_replace(c."CompanyName", @__pattern_0, @__replacement_1, 'p') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", @__pattern_0, @__replacement_1, 'p') +FROM "Customers" AS c +""" ); } @@ -330,9 +330,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'p') +FROM "Customers" AS c +""" ); } @@ -346,9 +346,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^a', 'B', 'pi') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^a', 'B', 'pi') +FROM "Customers" AS c +""" ); } @@ -362,9 +362,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^A', 'B', 'n') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^A', 'B', 'n') +FROM "Customers" AS c +""" ); } @@ -378,9 +378,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^A', 'B') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^A', 'B') +FROM "Customers" AS c +""" ); } @@ -395,9 +395,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^a', 'B', 'i') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^a', 'B', 'i') +FROM "Customers" AS c +""" ); } @@ -411,9 +411,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_replace(c."CompanyName", '^ A', 'B', 'px') - FROM "Customers" AS c - """ +SELECT regexp_replace(c."CompanyName", '^ A', 'B', 'px') +FROM "Customers" AS c +""" ); } @@ -434,9 +434,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^A', 1, 'p') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'p') +FROM "Customers" AS c +""" ); } @@ -453,11 +453,11 @@ await AssertQuery( AssertSql( """ - @__pattern_0='^A' +@__pattern_0='^A' - SELECT regexp_count(c."CompanyName", @__pattern_0, 1, 'p') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", @__pattern_0, 1, 'p') +FROM "Customers" AS c +""" ); } @@ -472,9 +472,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^A', 1, 'p') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'p') +FROM "Customers" AS c +""" ); } @@ -489,9 +489,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^a', 1, 'pi') +FROM "Customers" AS c +""" ); } @@ -506,9 +506,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^A', 1, 'n') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^A', 1, 'n') +FROM "Customers" AS c +""" ); } @@ -523,9 +523,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^A') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^A') +FROM "Customers" AS c +""" ); } @@ -540,9 +540,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^a', 1, 'i') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^a', 1, 'i') +FROM "Customers" AS c +""" ); } @@ -557,9 +557,9 @@ await AssertQuery( AssertSql( """ - SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') - FROM "Customers" AS c - """ +SELECT regexp_count(c."CompanyName", '^ A', 1, 'px') +FROM "Customers" AS c +""" ); }