diff --git a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs
index 4b463fb2c..8390bf7db 100644
--- a/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs
+++ b/src/EFCore.PG/Extensions/DbFunctionsExtensions/NpgsqlFuzzyStringMatchDbFunctionsExtensions.cs
@@ -11,6 +11,18 @@ namespace Microsoft.EntityFrameworkCore;
///
public static class NpgsqlFuzzyStringMatchDbFunctionsExtensions
{
+ ///
+ /// Returns the Daitch-Mokotoff Soundex codes for a string.
+ ///
+ /// The soundex codes, or if the input contains no encodable characters.
+ ///
+ /// The method call is translated to daitch_mokotoff(text).
+ /// Requires PostgreSQL 16 or later and the fuzzystrmatch extension.
+ /// See https://www.postgresql.org/docs/current/fuzzystrmatch.html.
+ ///
+ public static string[]? FuzzyStringMatchDaitchMokotoff(this DbFunctions _, string text)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDaitchMokotoff)));
+
///
/// The soundex function converts a string to its Soundex code.
///
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs
index d66d596c3..ecdb7d8fe 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFuzzyStringMatchMethodTranslator.cs
@@ -6,7 +6,10 @@
/// 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 NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator
+public class NpgsqlFuzzyStringMatchMethodTranslator(
+ IRelationalTypeMappingSource typeMappingSource,
+ NpgsqlSqlExpressionFactory sqlExpressionFactory,
+ bool supportsDaitchMokotoff) : IMethodCallTranslator
{
private static readonly bool[][] TrueArrays =
[
@@ -33,6 +36,8 @@ public class NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory s
var function = method.Name switch
{
+ nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDaitchMokotoff) when supportsDaitchMokotoff
+ => "daitch_mokotoff",
nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchSoundex) => "soundex",
nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDifference) => "difference",
nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchLevenshtein) => "levenshtein",
@@ -49,7 +54,9 @@ public class NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory s
function,
arguments.Skip(1),
nullable: true,
- argumentsPropagateNullability: TrueArrays[arguments.Count - 1],
- method.ReturnType);
+ // daitch_mokotoff can return NULL for non-null inputs such as the empty string.
+ argumentsPropagateNullability: function == "daitch_mokotoff" ? [false] : TrueArrays[arguments.Count - 1],
+ method.ReturnType,
+ typeMappingSource.FindMapping(method.ReturnType));
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
index e2573d4ae..ab60c87e6 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
@@ -34,6 +34,7 @@ public NpgsqlMethodCallTranslatorProvider(
var npgsqlOptions = contextOptions.FindExtension() ?? new NpgsqlOptionsExtension();
var supportsMultiranges = npgsqlOptions.PostgresVersion.AtLeast(14);
var supportRegexCount = npgsqlOptions.PostgresVersion.AtLeast(15);
+ var supportsDaitchMokotoff = npgsqlOptions.PostgresVersion.AtLeast(16);
var sqlExpressionFactory = (NpgsqlSqlExpressionFactory)dependencies.SqlExpressionFactory;
var typeMappingSource = (NpgsqlTypeMappingSource)dependencies.RelationalTypeMappingSource;
@@ -47,7 +48,7 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlConvertTranslator(sqlExpressionFactory),
new NpgsqlDateTimeMethodTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlFullTextSearchMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
- new NpgsqlFuzzyStringMatchMethodTranslator(sqlExpressionFactory),
+ new NpgsqlFuzzyStringMatchMethodTranslator(typeMappingSource, sqlExpressionFactory, supportsDaitchMokotoff),
new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlJsonDbFunctionsTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlJsonPocoTranslator(typeMappingSource, sqlExpressionFactory, model),
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs
index 3326215fc..54bd77ae7 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlEvaluatableExpressionFilter.cs
@@ -47,6 +47,7 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model
|| declaringType == typeof(NpgsqlDbFunctionsExtensions)
|| declaringType == typeof(NpgsqlFullTextSearchDbFunctionsExtensions)
|| declaringType == typeof(NpgsqlFullTextSearchLinqExtensions)
+ || declaringType == typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions)
|| declaringType == typeof(NpgsqlNetworkDbFunctionsExtensions)
|| declaringType == typeof(NpgsqlJsonDbFunctionsExtensions)
|| declaringType == typeof(NpgsqlRangeDbFunctionsExtensions)
diff --git a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs
index 647336f0f..608658e34 100644
--- a/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/FuzzyStringMatchQueryNpgsqlTest.cs
@@ -22,6 +22,62 @@ public FuzzyStringMatchQueryNpgsqlTest(FuzzyStringMatchQueryNpgsqlFixture fixtur
#region FunctionTests
+ [Fact]
+ [MinimumPostgresVersion(16, 0)]
+ public async Task FuzzyStringMatchDaitchMokotoff()
+ {
+ await using var context = CreateContext();
+ var results = await context.FuzzyStringMatchTestEntities
+ .Select(x => EF.Functions.FuzzyStringMatchDaitchMokotoff(x.Text))
+ .ToArrayAsync();
+
+ Assert.Equal(9, results.Length);
+ Assert.All(results, result => Assert.Equal(new[] { "463543" }, result));
+
+ AssertSql(
+ """
+SELECT daitch_mokotoff(f."Text")
+FROM "FuzzyStringMatchTestEntities" AS f
+""");
+ }
+
+ [Theory]
+ [InlineData("George", new[] { "595000" })]
+ [InlineData("John", new[] { "160000", "460000" })]
+ [InlineData("", null)]
+ [InlineData(null, null)]
+ [MinimumPostgresVersion(16, 0)]
+ public async Task FuzzyStringMatchDaitchMokotoff_parameter(string? text, string[]? expected)
+ {
+ await using var context = CreateContext();
+ var results = await context.FuzzyStringMatchTestEntities
+ .Select(x => EF.Functions.FuzzyStringMatchDaitchMokotoff(text!))
+ .ToArrayAsync();
+
+ Assert.Equal(9, results.Length);
+ Assert.All(results, result => Assert.Equal(expected, result));
+
+ AssertContainsSql(text is null ? "daitch_mokotoff(NULL)" : "daitch_mokotoff(@text)");
+ }
+
+ [Fact]
+ [MinimumPostgresVersion(16, 0)]
+ public async Task FuzzyStringMatchDaitchMokotoff_null_result()
+ {
+ await using var context = CreateContext();
+ var count = await context.FuzzyStringMatchTestEntities
+ .CountAsync(x => EF.Functions.FuzzyStringMatchDaitchMokotoff(x.Text.Substring(0, 0)) == null);
+
+ Assert.Equal(9, count);
+
+ AssertSql(
+ """
+SELECT count(*)::int
+FROM "FuzzyStringMatchTestEntities" AS f
+WHERE daitch_mokotoff(substring(f."Text", 1, 0)) IS NULL
+""");
+ }
+
[Fact]
public void FuzzyStringMatchSoundex()
{
diff --git a/test/EFCore.PG.Tests/Query/NpgsqlFuzzyStringMatchMethodTranslatorTest.cs b/test/EFCore.PG.Tests/Query/NpgsqlFuzzyStringMatchMethodTranslatorTest.cs
new file mode 100644
index 000000000..e571a6319
--- /dev/null
+++ b/test/EFCore.PG.Tests/Query/NpgsqlFuzzyStringMatchMethodTranslatorTest.cs
@@ -0,0 +1,55 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class NpgsqlFuzzyStringMatchMethodTranslatorTest
+{
+ [Theory]
+ [InlineData(16)]
+ [InlineData(18)]
+ public void Daitch_mokotoff_returns_mapped_text_array(int postgresVersion)
+ {
+ using var context = new DbContext(
+ new DbContextOptionsBuilder().UseNpgsql(o => o.SetPostgresVersion(postgresVersion, 0)).Options);
+ var sqlExpressionFactory = context.GetService();
+ var translator = context.GetService();
+ var method = typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions)
+ .GetMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDaitchMokotoff))!;
+
+ var translation = Assert.IsType(
+ translator.Translate(
+ context.Model,
+ null,
+ method,
+ [sqlExpressionFactory.Constant(EF.Functions), sqlExpressionFactory.Constant("John")],
+ context.GetService>()));
+
+ Assert.Equal("daitch_mokotoff", translation.Name);
+ Assert.Equal(typeof(string[]), translation.Type);
+ Assert.Same(context.GetService().FindMapping(typeof(string[])), translation.TypeMapping);
+ Assert.Equal("text[]", translation.TypeMapping!.StoreType);
+ Assert.Single(translation.Arguments!);
+ Assert.True(translation.IsNullable);
+ Assert.Equal(new[] { false }, translation.ArgumentsPropagateNullability);
+ }
+
+ [Fact]
+ public void Daitch_mokotoff_is_not_translated_before_PostgreSQL_16()
+ {
+ using var context = new DbContext(new DbContextOptionsBuilder().UseNpgsql(o => o.SetPostgresVersion(15, 0)).Options);
+ var sqlExpressionFactory = context.GetService();
+ var translator = context.GetService();
+ var method = typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions)
+ .GetMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDaitchMokotoff))!;
+
+ Assert.Null(
+ translator.Translate(
+ context.Model,
+ null,
+ method,
+ [sqlExpressionFactory.Constant(EF.Functions), sqlExpressionFactory.Constant("John")],
+ context.GetService>()));
+ }
+
+ [Fact]
+ public void Daitch_mokotoff_throws_on_client()
+ => Assert.Throws(() => EF.Functions.FuzzyStringMatchDaitchMokotoff("John"));
+}