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
+ }
+}