From 4bfdf0d369ada0a3045984a01d4ea8a8fadd9421 Mon Sep 17 00:00:00 2001 From: WhatzGames Date: Mon, 15 Jan 2024 21:27:40 +0100 Subject: [PATCH 1/6] 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 46a19b4b5..fbd43e59b 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 659d27b73..000000000 --- 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 000000000..c994cbc71 --- /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 f74474e82..e5f5993f4 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 2/6] 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 fbd43e59b..6cec82a95 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 c994cbc71..400b0bc6a 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 e5f5993f4..ff18af8d8 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 3/6] 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 400b0bc6a..2732a609e 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 4/6] 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 2732a609e..e8d3deb89 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 5/6] 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 e8d3deb89..2661145b9 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 6/6] 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 ff18af8d8..b75a5a210 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(