diff --git a/EFCore.PG.sln b/EFCore.PG.sln index af0dbae8ce6ff071a03263990a3aa38a0f52738b..ff1df1d934822d27242d5ee07b2614f4c4c40629 100644 --- a/EFCore.PG.sln +++ b/EFCore.PG.sln @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.NTS", "src\EFCore EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.Trigrams", "src\EFCore.PG.Trigrams\EFCore.PG.Trigrams.csproj", "{4DEDE46C-FABB-4AD3-A7DF-BF4A3AF00B06}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.FuzzyStringMatch", "src\EFCore.PG.FuzzyStringMatch\EFCore.PG.FuzzyStringMatch.csproj", "{86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +112,18 @@ Global {4DEDE46C-FABB-4AD3-A7DF-BF4A3AF00B06}.Release|x64.Build.0 = Release|Any CPU {4DEDE46C-FABB-4AD3-A7DF-BF4A3AF00B06}.Release|x86.ActiveCfg = Release|Any CPU {4DEDE46C-FABB-4AD3-A7DF-BF4A3AF00B06}.Release|x86.Build.0 = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|x64.ActiveCfg = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|x64.Build.0 = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|x86.ActiveCfg = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Debug|x86.Build.0 = Debug|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|x64.ActiveCfg = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|x64.Build.0 = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|x86.ActiveCfg = Release|Any CPU + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -122,6 +136,7 @@ Global {77F0608F-6D0C-481C-9108-D5176E2EAD69} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {D7106D61-C7CA-4005-B31F-43281BB397AD} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} {4DEDE46C-FABB-4AD3-A7DF-BF4A3AF00B06} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} + {86525F23-3EB0-4B0D-9BBB-55FF247D4DD6} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F4EAAE6D-758C-4184-9D8C-7113384B61A8} diff --git a/src/EFCore.PG.FuzzyStringMatch/EFCore.PG.FuzzyStringMatch.csproj b/src/EFCore.PG.FuzzyStringMatch/EFCore.PG.FuzzyStringMatch.csproj new file mode 100644 index 0000000000000000000000000000000000000000..c5f6ae838dadac8e811e5698277b778b9a316f56 --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/EFCore.PG.FuzzyStringMatch.csproj @@ -0,0 +1,26 @@ + + + Jean-Philippe Leconte + fuzzystrmatch module support plugin for PostgreSQL/Npgsql Entity Framework Core provider. + npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;fuzzystrmatch + + Npgsql.EntityFrameworkCore.PostgreSQL.FuzzyStringMatch + Npgsql.EntityFrameworkCore.PostgreSQL.FuzzyStringMatch + + + + + + + + + + + + True + build + + + + + diff --git a/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..5594be790fdec9a0d9208a4e31e4af506193cf5e --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// fuzzystrmatch module specific extension methods for . + /// + public static class NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions + { + /// + /// Enable fuzzystrmatch module methods. + /// + /// The build being used to configure Postgres. + /// The options builder so that further configuration can be chained. + public static NpgsqlDbContextOptionsBuilder UseFuzzyStringMatch( + this NpgsqlDbContextOptionsBuilder optionsBuilder) + { + var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; + var extension = coreOptionsBuilder.Options.FindExtension() + ?? new NpgsqlFuzzyStringMatchOptionsExtension(); + + ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + } +} diff --git a/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5f1840615c708b1e34d5eab57b09e106d3d407c --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs @@ -0,0 +1,113 @@ +using System; + +namespace Microsoft.EntityFrameworkCore +{ + public static class NpgsqlFuzzyStringMatchDbFunctionsExtensions + { + /// + /// The soundex function converts a string to its Soundex code. + /// + /// + /// The method call is translated to soundex(text). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchSoundex(this DbFunctions _, string text) => + throw new NotSupportedException(); + + /// + /// The difference function converts two strings to their Soundex codes and + /// then returns the number of matching code positions. Since Soundex codes + /// have four characters, the result ranges from zero to four, with zero being + /// no match and four being an exact match. + /// + /// + /// The method call is translated to difference(source, target). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchDifference(this DbFunctions _, string source, string target) => + throw new NotSupportedException(); + + /// + /// Returns the Levenshtein distance between two strings. + /// + /// + /// The method call is translated to levenshtein(source, target). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshtein(this DbFunctions _, string source, string target) => + throw new NotSupportedException(); + + /// + /// Returns the Levenshtein distance between two strings. + /// + /// + /// The method call is translated to levenshtein(source, target, insertionCost, deletionCost, substitutionCost). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshtein(this DbFunctions _, string source, string target, int insertionCost, int deletionCost, int substitutionCost) => + throw new NotSupportedException(); + + /// + /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. + /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; + /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as levenshtein. + /// + /// + /// The method call is translated to levenshtein_less_equal(source, target, maximumDistance). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshteinLessEqual(this DbFunctions _, string source, string target, int maximumDistance) => + throw new NotSupportedException(); + + /// + /// levenshtein_less_equal is an accelerated version of the Levenshtein function for use when only small distances are of interest. + /// If the actual distance is less than or equal to maximum distance, then levenshtein_less_equal returns the correct distance; + /// otherwise it returns some value greater than maximum distance. If maximum distance is negative then the behavior is the same as levenshtein. + /// + /// + /// The method call is translated to levenshtein_less_equal(source, target, insertionCost, deletionCost, substitutionCost, maximumDistance). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static int FuzzyStringMatchLevenshteinLessEqual(this DbFunctions _, string source, string target, int insertionCost, int deletionCost, int substitutionCost, int maximumDistance) => + throw new NotSupportedException(); + + /// + /// The metaphone function converts a string to its Metaphone code. + /// + /// + /// The method call is translated to metaphone(text, maximumOutputLength). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchMetaphone(this DbFunctions _, string text, int maximumOutputLength) => + throw new NotSupportedException(); + + /// + /// The dmetaphone function converts a string to its primary Double Metaphone code. + /// + /// + /// The method call is translated to dmetaphone(text). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchDoubleMetaphone(this DbFunctions _, string text) => + throw new NotSupportedException(); + + /// + /// The dmetaphone_alt function converts a string to its alternate Double Metaphone code. + /// + /// + /// The method call is translated to dmetaphone_alt(text). + /// + /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html. + /// + public static string FuzzyStringMatchDoubleMetaphoneAlt(this DbFunctions _, string text) => + throw new NotSupportedException(); + } +} diff --git a/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchServiceCollectionExtensions.cs b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..c26f6f3fd9eb21a317132f908502640e5d73b966 --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Extensions/NpgsqlFuzzyStringMatchServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// fuzzystrmatch module extension methods for . + /// + public static class NpgsqlFuzzyStringMatchServiceCollectionExtensions + { + /// + /// Adds the services required for fuzzystrmatch support in the Npgsql provider for Entity Framework. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddEntityFrameworkNpgsqlFuzzyStringMatch( + this IServiceCollection serviceCollection) + { + new EntityFrameworkRelationalServicesBuilder(serviceCollection) + .TryAddProviderSpecificServices( + x => x.TryAddSingletonEnumerable()); + + return serviceCollection; + } + } +} diff --git a/src/EFCore.PG.FuzzyStringMatch/Infrastructure/Internal/NpgsqlFuzzyStringMatchOptionsExtension.cs b/src/EFCore.PG.FuzzyStringMatch/Infrastructure/Internal/NpgsqlFuzzyStringMatchOptionsExtension.cs new file mode 100644 index 0000000000000000000000000000000000000000..68ec2c544f1f9170bcbd61784a4203b9d1831062 --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Infrastructure/Internal/NpgsqlFuzzyStringMatchOptionsExtension.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal +{ + public class NpgsqlFuzzyStringMatchOptionsExtension : IDbContextOptionsExtension + { + public DbContextOptionsExtensionInfo Info => new ExtensionInfo(this); + + public virtual void ApplyServices(IServiceCollection services) => services.AddEntityFrameworkNpgsqlFuzzyStringMatch(); + + public virtual void Validate(IDbContextOptions options) + { + var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; + if (internalServiceProvider != null) + { + using var scope = internalServiceProvider.CreateScope(); + var plugins = scope.ServiceProvider.GetService>(); + if (plugins is null || !plugins.Any(s => s is NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin)) + { + throw new InvalidOperationException($"{nameof(NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions.UseFuzzyStringMatch)} requires {nameof(NpgsqlFuzzyStringMatchServiceCollectionExtensions.AddEntityFrameworkNpgsqlFuzzyStringMatch)} to be called on the internal service provider used."); + } + } + } + + sealed class ExtensionInfo : DbContextOptionsExtensionInfo + { + public ExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) + { + } + + public override bool IsDatabaseProvider => false; + + public override long GetServiceProviderHashCode() => 0; + + public override void PopulateDebugInfo(IDictionary debugInfo) + => debugInfo["Npgsql:" + nameof(NpgsqlFuzzyStringMatchDbContextOptionsBuilderExtensions.UseFuzzyStringMatch)] = "1"; + + public override string LogFragment => "using FuzzyStringMatch "; + } + } +} diff --git a/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin.cs b/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin.cs new file mode 100644 index 0000000000000000000000000000000000000000..75044a1425d15b6dc744551b4bdcd82b987ffda0 --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + public class NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin + { + public NpgsqlFuzzyStringMatchMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory) + => Translators = new IMethodCallTranslator[] + { + new NpgsqlFuzzyStringMatchMethodTranslator((NpgsqlSqlExpressionFactory) sqlExpressionFactory, typeMappingSource), + }; + + public virtual IEnumerable Translators { get; } + } +} diff --git a/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs b/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs new file mode 100644 index 0000000000000000000000000000000000000000..42288e9d5b9f671f347e108dbb9cc9e42e20dfc2 --- /dev/null +++ b/src/EFCore.PG.FuzzyStringMatch/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal +{ + public class NpgsqlFuzzyStringMatchMethodTranslator : IMethodCallTranslator + { + static readonly Dictionary Functions = new Dictionary + { + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchSoundex), new[] { typeof(DbFunctions), typeof(string) })] = "soundex", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDifference), new[] { typeof(DbFunctions), typeof(string), typeof(string) })] = "difference", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), new[] { typeof(DbFunctions), typeof(string), typeof(string) })] = "levenshtein", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int) })] = "levenshtein", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(int) })] = "levenshtein_less_equal", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshteinLessEqual), new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(int), typeof(int), typeof(int), typeof(int) })] = "levenshtein_less_equal", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchMetaphone), new[] { typeof(DbFunctions), typeof(string), typeof(int) })] = "metaphone", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphone), new[] { typeof(DbFunctions), typeof(string) })] = "dmetaphone", + [GetRuntimeMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDoubleMetaphoneAlt), new[] { typeof(DbFunctions), typeof(string) })] = "dmetaphone_alt" + }; + + static MethodInfo GetRuntimeMethod(string name, params Type[] parameters) + => typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions).GetRuntimeMethod(name, parameters); + + readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + + public NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory, IRelationalTypeMappingSource typeMappingSource) + => _sqlExpressionFactory = sqlExpressionFactory; + + /// + public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList arguments) + => Functions.TryGetValue(method, out var function) + ? _sqlExpressionFactory.Function(function, arguments.Skip(1), method.ReturnType) + : null; + } +} diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index f9c621f1115f2204030ce142ef78e647c32a1e28..7b992bea4a0f45525f10e679aca973524d3b45d7 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..12aeb76f0fac0b3aad3d3ab66aefb78267114416 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs @@ -0,0 +1,234 @@ +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities.Xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + /// + /// Provides unit tests for the fuzzystrmatch module function translations. + /// + /// + /// See: https://www.postgresql.org/docs/current/fuzzystrmatch.html + /// + public class FuzzyStringMatchQueryNpgsqlTest : IClassFixture + { + FuzzyStringMatchQueryNpgsqlFixture Fixture { get; } + + public FuzzyStringMatchQueryNpgsqlTest(FuzzyStringMatchQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + #region FunctionTests + + [Fact] + public void FuzzyStringMatchSoundex() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchSoundex(x.Text)) + .ToArray(); + + AssertContainsSql(@"soundex(f.""Text"")"); + } + + [Fact] + public void FuzzyStringMatchDifference() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDifference(x.Text, "target")) + .ToArray(); + + AssertContainsSql(@"difference(f.""Text"", 'target')"); + } + + [Fact] + public void FuzzyStringMatchLevenshtein() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target")) + .ToArray(); + + AssertContainsSql(@"levenshtein(f.""Text"", 'target')"); + } + + [Fact] + public void FuzzyStringMatchLevenshtein_With_Costs() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshtein(x.Text, "target", 1, 2, 3)) + .ToArray(); + + AssertContainsSql(@"levenshtein(f.""Text"", 'target', 1, 2, 3)"); + } + + [Fact] + public void FuzzyStringMatchLevenshteinLessEqual() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 5)) + .ToArray(); + + AssertContainsSql(@"levenshtein_less_equal(f.""Text"", 'target', 5)"); + } + + [Fact] + public void FuzzyStringMatchLevenshteinLessEqual_With_Costs() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchLevenshteinLessEqual(x.Text, "target", 1, 2, 3, 5)) + .ToArray(); + + AssertContainsSql(@"levenshtein_less_equal(f.""Text"", 'target', 1, 2, 3, 5)"); + } + + [Fact] + public void FuzzyStringMatchMetaphone() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchMetaphone(x.Text, 6)) + .ToArray(); + + AssertContainsSql(@"metaphone(f.""Text"", 6)"); + } + + [Fact] + public void FuzzyStringMatchDoubleMetaphone() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphone(x.Text)) + .ToArray(); + + AssertContainsSql(@"dmetaphone(f.""Text"")"); + } + + [Fact] + public void FuzzyStringMatchDoubleMetaphoneAlt() + { + using var context = CreateContext(); + var _ = context.FuzzyStringMatchTestEntities + .Select(x => EF.Functions.FuzzyStringMatchDoubleMetaphoneAlt(x.Text)) + .ToArray(); + + AssertContainsSql(@"dmetaphone_alt(f.""Text"")"); + } + + #endregion + + #region Fixtures + + /// + /// Represents a fixture suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchQueryNpgsqlFixture : SharedStoreFixtureBase + { + protected override string StoreName => "FuzzyStringMatchQueryTest"; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlFuzzyStringMatch(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseFuzzyStringMatch(); + + return optionsBuilder; + } + + protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; + protected override void Seed(FuzzyStringMatchContext context) => FuzzyStringMatchContext.Seed(context); + } + + /// + /// Represents an entity suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchTestEntity + { + // ReSharper disable once UnusedMember.Global + /// + /// The primary key. + /// + [Key] + public int Id { get; set; } + + /// + /// Some text. + /// + public string Text { get; set; } + } + + /// + /// Represents a database suitable for testing fuzzy string match functions. + /// + public class FuzzyStringMatchContext : PoolableDbContext + { + /// + /// Represents a set of entities with properties. + /// + public DbSet FuzzyStringMatchTestEntities { get; set; } + + /// + /// Initializes a . + /// + /// + /// The options to be used for configuration. + /// + public FuzzyStringMatchContext(DbContextOptions options) : base(options) {} + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresExtension("fuzzystrmatch"); + + base.OnModelCreating(modelBuilder); + } + + public static void Seed(FuzzyStringMatchContext context) + { + for (var i = 1; i <= 9; i++) + { + var text = "Some text " + i; + context.FuzzyStringMatchTestEntities.Add( + new FuzzyStringMatchTestEntity + { + Id = i, + Text = text + }); + } + context.SaveChanges(); + } + } + + #endregion + + #region Helpers + + protected FuzzyStringMatchContext CreateContext() => Fixture.CreateContext(); + + void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + /// + /// Asserts that the SQL fragment appears in the logs. + /// + /// The SQL statement or fragment to search for in the logs. + void AssertContainsSql(string sql) => Assert.Contains(sql, Fixture.TestSqlLoggerFactory.Sql); + + #endregion + } +}