diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs index dca218e62a6..f35640cacc7 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs @@ -31,6 +31,7 @@ public SqliteMethodCallTranslatorProvider([NotNull] RelationalMethodCallTranslat new SqliteByteArrayMethodTranslator(sqlExpressionFactory), new SqliteDateTimeAddTranslator(sqlExpressionFactory), new SqliteMathTranslator(sqlExpressionFactory), + new SqliteRegexTranslator(sqlExpressionFactory), new SqliteStringMethodTranslator(sqlExpressionFactory) }); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteRegexTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteRegexTranslator.cs new file mode 100644 index 00000000000..8fee6f50889 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteRegexTranslator.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal +{ + /// + /// 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 class SqliteRegexTranslator : IMethodCallTranslator + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly static MethodInfo regexIsMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new Type[] { typeof(string), typeof(string) }); + + /// + /// 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 SqliteRegexTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = 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 virtual SqlExpression Translate(SqlExpression instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + Check.NotNull(method, nameof(method)); + Check.NotNull(arguments, nameof(arguments)); + Check.NotNull(logger, nameof(logger)); + + + if (method.Equals(regexIsMatchMethod)) + { + return _sqlExpressionFactory.Function("regexp", + new[] { arguments[1], arguments[0] }, + false, + new[] { false, false }, + typeof(bool), + arguments[0].TypeMapping); + } + + return null; + } + } +} diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs index 02ded1c1886..a78fce5a056 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs @@ -5,6 +5,7 @@ using System.Data.Common; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using JetBrains.Annotations; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -114,6 +115,19 @@ private void InitializeDbConnection(DbConnection connection) sqliteConnection.DefaultTimeout = _commandTimeout.Value; } + sqliteConnection.CreateFunction( + "regexp", + (pattern, input) => + { + if (input == null || pattern == null) + { + return false; + } + + return Regex.IsMatch(input, pattern); + }, + isDeterministic: true); + sqliteConnection.CreateFunction( "ef_mod", (dividend, divisor) => diff --git a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs index 57363aa0101..52788059cd2 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -1584,5 +1585,14 @@ public virtual Task Projecting_Math_Truncate_and_ordering_by_it_twice3(bool asyn .ThenBy(r => r.A), assertOrder: true); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Regex_IsMatch_MethodCall(bool async) + { + return AssertQuery(async, + ss => ss.Set().Where(o => Regex.IsMatch(o.CustomerID, @"^T")), + entryCount: 6); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 5d7f3438329..130c6256bbc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -1548,6 +1548,10 @@ public override async Task Projecting_Math_Truncate_and_ordering_by_it_twice3(bo //WHERE [o].[OrderID] < 10250 //ORDER BY [A] DESC"); } + public override Task Regex_IsMatch_MethodCall(bool async) + { + return AssertTranslationFailed(() => base.Regex_IsMatch_MethodCall(async)); + } private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index f83b8e2cca9..f66f94ebdfc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -511,6 +511,15 @@ public override async Task Trim_with_char_array_argument_in_predicate(bool async WHERE trim(""c"".""ContactTitle"", 'Or') = 'wne'"); } + public override async Task Regex_IsMatch_MethodCall(bool async) + { + await base.Regex_IsMatch_MethodCall(async); + + AssertSql(@"SELECT ""c"".""CustomerID"", ""c"".""Address"", ""c"".""City"", ""c"".""CompanyName"", ""c"".""ContactName"", ""c"".""ContactTitle"", ""c"".""Country"", ""c"".""Fax"", ""c"".""Phone"", ""c"".""PostalCode"", ""c"".""Region"" +FROM ""Customers"" AS ""c"" +WHERE regexp('^T', ""c"".""CustomerID"")"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }