Skip to content

Commit

Permalink
Add unaccent extension (#1501)
Browse files Browse the repository at this point in the history
Closes #1530
  • Loading branch information
cloudlucky committed Oct 11, 2020
1 parent bc23399 commit 795cb6b
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 5 deletions.
2 changes: 1 addition & 1 deletion EFCore.PG.sln
Expand Up @@ -144,4 +144,4 @@ Global
GlobalSection(MonoDevelopProperties) = preSolution
StartupItem = Npgsql.csproj
EndGlobalSection
EndGlobal
EndGlobal
@@ -1,4 +1,4 @@
using System;
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using NpgsqlTypes;
Expand Down Expand Up @@ -131,5 +131,34 @@ public static NpgsqlTsQuery WebSearchToTsQuery([CanBeNull] this DbFunctions _, [
[NotNull] string config,
[NotNull] string query)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(WebSearchToTsQuery)));

/// <summary>
/// Returns a new string that removes diacritics from characters in the given <paramref name="text" />.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="regDictionary">A specific text search dictionary.</param>
/// <param name="text">The text to remove the diacritics.</param>
/// <remarks>
/// <para>The method call is translated to <c>unaccent(regdictionary, text)</c>.</para>
///
/// See https://www.postgresql.org/docs/current/unaccent.html.
/// </remarks>
/// <returns>A string without diacritics.</returns>
public static string Unaccent([CanBeNull] this DbFunctions _, [NotNull] string regDictionary, [CanBeNull] string text)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent)));

/// <summary>
/// Returns a new string that removes diacritics from characters in the given <paramref name="text" />.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="text">The text to remove the diacritics.</param>
/// <remarks>
/// <para>The method call is translated to <c>unaccent(text)</c>.</para>
///
/// See https://www.postgresql.org/docs/current/unaccent.html.
/// </remarks>
/// <returns>A string without diacritics.</returns>
public static string Unaccent([CanBeNull] this DbFunctions _, [CanBeNull] string text)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Unaccent)));
}
}
Expand Up @@ -31,6 +31,7 @@ public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator
readonly RelationalTypeMapping _tsQueryMapping;
readonly RelationalTypeMapping _tsVectorMapping;
readonly RelationalTypeMapping _regconfigMapping;
readonly RelationalTypeMapping _regdictionaryMapping;

public NpgsqlFullTextSearchMethodTranslator(
[NotNull] IRelationalTypeMappingSource typeMappingSource,
Expand All @@ -42,6 +43,7 @@ public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator
_tsQueryMapping = typeMappingSource.FindMapping("tsquery");
_tsVectorMapping = typeMappingSource.FindMapping("tsvector");
_regconfigMapping = typeMappingSource.FindMapping("regconfig");
_regdictionaryMapping = typeMappingSource.FindMapping("regdictionary");
}

/// <inheritdoc />
Expand All @@ -64,6 +66,7 @@ public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) when arguments.Count == 3 => ConfigAccepting("phraseto_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsQuery) when arguments.Count == 3 => ConfigAccepting("to_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) when arguments.Count == 3 => ConfigAccepting("websearch_to_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.Unaccent) when arguments.Count == 3 => DictionaryAccepting("unaccent"),

// Methods not accepting a configuration
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ArrayToTsVector) => NonConfigAccepting("array_to_tsvector"),
Expand All @@ -72,6 +75,7 @@ public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PhraseToTsQuery) => NonConfigAccepting("phraseto_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsQuery) => NonConfigAccepting("to_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.WebSearchToTsQuery) => NonConfigAccepting("websearch_to_tsquery"),
nameof(NpgsqlFullTextSearchDbFunctionsExtensions.Unaccent) => NonConfigAccepting("unaccent"),

_ => null
};
Expand Down Expand Up @@ -296,6 +300,22 @@ SqlExpression ConfigAccepting(string functionName)
method.ReturnType,
_typeMappingSource.FindMapping(method.ReturnType));

SqlExpression DictionaryAccepting(string functionName)
=> _sqlExpressionFactory.Function(functionName, new[]
{
// For the regdictionary parameter, if a constant string was provided, just pass it as a string - regdictionary-accepting functions
// will implicitly cast to regdictionary. For (string!) parameters, we add an explicit cast, since regdictionary actually is an OID
// behind the scenes, and for parameter binary transfer no type coercion occurs.
arguments[1] is SqlConstantExpression constant
? _sqlExpressionFactory.ApplyDefaultTypeMapping(constant)
: _sqlExpressionFactory.Convert(arguments[1], typeof(string), _regdictionaryMapping),
arguments[2]
},
nullable: true,
argumentsPropagateNullability: TrueArrays[arguments.Count],
method.ReturnType,
_typeMappingSource.FindMapping(method.ReturnType));

SqlExpression NonConfigAccepting(string functionName)
=> _sqlExpressionFactory.Function(
functionName,
Expand Down
@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities;
using NpgsqlTypes;
Expand Down
@@ -0,0 +1,24 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities;
using NpgsqlTypes;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping
{
public class NpgsqlRegdictionaryTypeMapping : NpgsqlTypeMapping
{
public NpgsqlRegdictionaryTypeMapping() : base("regdictionary", typeof(uint), NpgsqlDbType.Oid) { }

protected NpgsqlRegdictionaryTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Oid) { }

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlRegdictionaryTypeMapping(parameters);

protected override string GenerateNonNullSqlLiteral(object value)
=> $"'{EscapeSqlLiteral((string)value)}'";

string EscapeSqlLiteral([NotNull] string literal)
=> Check.NotNull(literal, nameof(literal)).Replace("'", "''");
}
}
7 changes: 6 additions & 1 deletion src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Expand Up @@ -98,6 +98,9 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource
readonly NpgsqlRegconfigTypeMapping _regconfig = new NpgsqlRegconfigTypeMapping();
readonly NpgsqlTsRankingNormalizationTypeMapping _rankingNormalization = new NpgsqlTsRankingNormalizationTypeMapping();

// Unaccent mapping
readonly NpgsqlRegdictionaryTypeMapping _regdictionary = new NpgsqlRegdictionaryTypeMapping();

// Built-in ranges
readonly NpgsqlRangeTypeMapping _int4range;
readonly NpgsqlRangeTypeMapping _int8range;
Expand Down Expand Up @@ -212,7 +215,9 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource

{ "tsquery", new[] { _tsquery } },
{ "tsvector", new[] { _tsvector } },
{ "regconfig", new[] { _regconfig } }
{ "regconfig", new[] { _regconfig } },

{ "regdictionary", new[] { _regdictionary } }
};

var clrTypeMappings = new Dictionary<Type, RelationalTypeMapping>
Expand Down
1 change: 1 addition & 0 deletions test/EFCore.PG.FunctionalTests/Northwind.sql
Expand Up @@ -6,6 +6,7 @@ SET client_min_messages = warning;
GO

CREATE EXTENSION "uuid-ossp";
CREATE EXTENSION "unaccent";

CREATE TABLE "Employees" (
"EmployeeID" SERIAL,
Expand Down
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
Expand Down Expand Up @@ -859,6 +859,49 @@ public void Complex_query()
Assert.Equal("<b>Accounting</b> <b>Manager</b>", headline);
}

[Fact]
public void Unaccent()
{
using var context = CreateContext();
var _ = context.Customers
.Select(x => EF.Functions.Unaccent(x.ContactName))
.FirstOrDefault();

AssertSql(@"SELECT unaccent(c.""ContactName"")
FROM ""Customers"" AS c
LIMIT 1");
}

[Fact]
public void Unaccent_with_constant_regdictionary()
{
using var context = CreateContext();
var _ = context.Customers
.Select(x => EF.Functions.Unaccent("unaccent", x.ContactName))
.FirstOrDefault();

AssertSql(@"SELECT unaccent('unaccent', c.""ContactName"")
FROM ""Customers"" AS c
LIMIT 1");
}

[Fact]
public void Unaccent_with_parameter_regdictionary()
{
using var context = CreateContext();
var regDictionary = "unaccent";
var _ = context.Customers
.Select(x => EF.Functions.Unaccent(regDictionary, x.ContactName))
.FirstOrDefault();

AssertSql(
@"@__regDictionary_1='unaccent'
SELECT unaccent(@__regDictionary_1::regdictionary, c.""ContactName"")
FROM ""Customers"" AS c
LIMIT 1");
}

void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Expand Up @@ -12,6 +12,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
base.OnModelCreating(modelBuilder);

modelBuilder.HasPostgresExtension("uuid-ossp");
modelBuilder.HasPostgresExtension("unaccent");

modelBuilder.Entity<CustomerQuery>().ToSqlQuery(@"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""");
}
Expand Down

0 comments on commit 795cb6b

Please sign in to comment.