Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ namespace Microsoft.EntityFrameworkCore;
/// </remarks>
public static class NpgsqlFuzzyStringMatchDbFunctionsExtensions
{
/// <summary>
/// Returns the Daitch-Mokotoff Soundex codes for a string.
/// </summary>
/// <returns>The soundex codes, or <see langword="null" /> if the input contains no encodable characters.</returns>
/// <remarks>
/// The method call is translated to <c>daitch_mokotoff(text)</c>.
/// Requires PostgreSQL 16 or later and the fuzzystrmatch extension.
/// See https://www.postgresql.org/docs/current/fuzzystrmatch.html.
/// </remarks>
public static string[]? FuzzyStringMatchDaitchMokotoff(this DbFunctions _, string text)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FuzzyStringMatchDaitchMokotoff)));

/// <summary>
/// The soundex function converts a string to its Soundex code.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class NpgsqlFuzzyStringMatchMethodTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator
public class NpgsqlFuzzyStringMatchMethodTranslator(
IRelationalTypeMappingSource typeMappingSource,
NpgsqlSqlExpressionFactory sqlExpressionFactory,
bool supportsDaitchMokotoff) : IMethodCallTranslator
{
private static readonly bool[][] TrueArrays =
[
Expand All @@ -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",
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public NpgsqlMethodCallTranslatorProvider(
var npgsqlOptions = contextOptions.FindExtension<NpgsqlOptionsExtension>() ?? 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;
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ISqlExpressionFactory>();
var translator = context.GetService<IMethodCallTranslatorProvider>();
var method = typeof(NpgsqlFuzzyStringMatchDbFunctionsExtensions)
.GetMethod(nameof(NpgsqlFuzzyStringMatchDbFunctionsExtensions.FuzzyStringMatchDaitchMokotoff))!;

var translation = Assert.IsType<SqlFunctionExpression>(
translator.Translate(
context.Model,
null,
method,
[sqlExpressionFactory.Constant(EF.Functions), sqlExpressionFactory.Constant("John")],
context.GetService<IDiagnosticsLogger<DbLoggerCategory.Query>>()));

Assert.Equal("daitch_mokotoff", translation.Name);
Assert.Equal(typeof(string[]), translation.Type);
Assert.Same(context.GetService<IRelationalTypeMappingSource>().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<ISqlExpressionFactory>();
var translator = context.GetService<IMethodCallTranslatorProvider>();
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<IDiagnosticsLogger<DbLoggerCategory.Query>>()));
}

[Fact]
public void Daitch_mokotoff_throws_on_client()
=> Assert.Throws<InvalidOperationException>(() => EF.Functions.FuzzyStringMatchDaitchMokotoff("John"));
}