diff --git a/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchDbFunctionsExtensions.cs b/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchDbFunctionsExtensions.cs index 6ebcc207d..62d0bde0a 100644 --- a/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchDbFunctionsExtensions.cs +++ b/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchDbFunctionsExtensions.cs @@ -31,6 +31,13 @@ namespace Microsoft.EntityFrameworkCore [SuppressMessage("ReSharper", "UnusedParameter.Global")] public static class NpgsqlFullTextSearchDbFunctionsExtensions { + /// + /// Convert to a tsvector. + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static NpgsqlTsVector ArrayToTsVector(this DbFunctions _, string[] lexemes) => + throw new NotSupportedException(); + /// /// Reduce to tsvector. /// http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-DOCUMENTS diff --git a/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchLinqExtensions.cs b/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchLinqExtensions.cs index 507675734..01cba0d5d 100644 --- a/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchLinqExtensions.cs +++ b/src/EFCore.PG/FullTextSearch/NpgsqlFullTextSearchLinqExtensions.cs @@ -144,6 +144,15 @@ public static NpgsqlTsQuery ToPhrase(this NpgsqlTsQuery query1, NpgsqlTsQuery qu public static bool Matches(this NpgsqlTsVector vector, NpgsqlTsQuery query) => throw new NotSupportedException(); + /// + /// Returns a vector which combines the lexemes and positional information of + /// and using the || tsvector operator. Positions and weight labels are retained + /// during the concatenation. + /// https://www.postgresql.org/docs/10/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR + /// + public static NpgsqlTsVector Concat(this NpgsqlTsVector vector1, NpgsqlTsVector vector2) => + throw new NotSupportedException(); + /// /// Assign weight to each element of and return a new /// weighted tsvector. @@ -179,6 +188,27 @@ public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, char weight) public static NpgsqlTsVector SetWeight(this NpgsqlTsVector vector, char weight, string[] lexemes) => throw new NotSupportedException(); + /// + /// Return a new vector with removed from + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static NpgsqlTsVector Delete(this NpgsqlTsVector vector, string lexeme) => + throw new NotSupportedException(); + + /// + /// Return a new vector with removed from + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static NpgsqlTsVector Delete(this NpgsqlTsVector vector, string[] lexemes) => + throw new NotSupportedException(); + + /// + /// Returns a new vector with only lexemes having weights specified in . + /// https://www.postgresql.org/docs/current/static/functions-textsearch.html + /// + public static NpgsqlTsVector Filter(this NpgsqlTsVector vector, char[] weights) => + throw new NotSupportedException(); + /// /// Returns the number of lexemes in . /// http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-MANIPULATE-TSVECTOR diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs index d057d1f5d..44ecba056 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlFullTextSearchMethodTranslator.cs @@ -47,6 +47,7 @@ public class NpgsqlFullTextSearchMethodTranslator : IMethodCallTranslator static readonly IReadOnlyDictionary _sqlNameByMethodName = new Dictionary { + [nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ArrayToTsVector)] = "array_to_tsvector", [nameof(NpgsqlFullTextSearchDbFunctionsExtensions.ToTsVector)] = "to_tsvector", [nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PlainToTsQuery)] = "plainto_tsquery", [nameof(NpgsqlFullTextSearchDbFunctionsExtensions.PhraseToTsQuery)] = "phraseto_tsquery", @@ -105,6 +106,11 @@ static Expression TryTranslateOperator(MethodCallExpression methodCallExpression methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]); + case nameof(NpgsqlFullTextSearchLinqExtensions.Concat): + return FullTextSearchExpression.TsVectorConcat( + methodCallExpression.Arguments[0], + methodCallExpression.Arguments[1]); + default: return null; } @@ -196,6 +202,22 @@ static Expression TryTranslateFunction(MethodCallExpression methodCallExpression methodCallExpression.Method.ReturnType, arguments); + case nameof(NpgsqlFullTextSearchLinqExtensions.Delete): + return new SqlFunctionExpression( + "ts_delete", + methodCallExpression.Method.ReturnType, + methodCallExpression.Arguments); + + case nameof(NpgsqlFullTextSearchLinqExtensions.Filter): + return new SqlFunctionExpression( + "ts_filter", + methodCallExpression.Method.ReturnType, + new[] + { + methodCallExpression.Arguments[0], + new ExplicitStoreTypeCastExpression(methodCallExpression.Arguments[1], typeof(char[]), "\"char\"[]") + }); + case nameof(NpgsqlFullTextSearchLinqExtensions.GetLength): return new SqlFunctionExpression( "length", diff --git a/src/EFCore.PG/Query/Expressions/Internal/FullTextSearchExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/FullTextSearchExpression.cs index 650fedf23..0b4666d11 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/FullTextSearchExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/FullTextSearchExpression.cs @@ -92,6 +92,12 @@ public static FullTextSearchExpression TsVectorMatches([NotNull] Expression left return new FullTextSearchExpression("@@", left, right, typeof(bool)); } + public static FullTextSearchExpression TsVectorConcat([NotNull] Expression left, [NotNull] Expression right) + { + ValidateArguments(left, typeof(NpgsqlTsVector), right, typeof(NpgsqlTsVector)); + return new FullTextSearchExpression("||", left, right, typeof(NpgsqlTsVector)); + } + private static void ValidateArguments( [NotNull] Expression left, [NotNull] Type validLeftType, diff --git a/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs index 5438e9045..3fd4f0f8c 100644 --- a/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/FullTextSearchDbFunctionsNpgsqlTest.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -32,6 +33,34 @@ public void TsVectorParse_converted_to_cast() LIMIT 1"); } + [Fact] + public void ArrayToTsVector() + { + using (var context = CreateContext()) + { + var tsvector = context.Customers.Select(c => EF.Functions.ArrayToTsVector(new[] { "b", "c", "d" })) + .First(); + Assert.Equal(NpgsqlTsVector.Parse("b c d").ToString(), tsvector.ToString()); + } + + AssertSql( + @"SELECT array_to_tsvector(ARRAY['b','c','d']) +FROM ""Customers"" AS c +LIMIT 1"); + } + + [Fact] + public void ArrayToTsVector_From_Columns_Throws_NotSupportedException() + { + using (var context = CreateContext()) + { + Assert.Throws( + () => context.Customers + .Select(c => EF.Functions.ArrayToTsVector(new[] { c.CompanyName, c.Address })) + .First()); + } + } + [Fact] public void ToTsVector() { @@ -429,6 +458,23 @@ public void Matches_With_Tsquery() LIMIT 1"); } + [Fact] + public void TsVectorConcat() + { + using (var context = CreateContext()) + { + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b").Concat(EF.Functions.ToTsVector("c"))) + .First(); + Assert.Equal(NpgsqlTsVector.Parse("b:1 c:2").ToString(), tsVector.ToString()); + } + + AssertSql( + @"SELECT (to_tsvector('b') || to_tsvector('c')) +FROM ""Customers"" AS c +LIMIT 1"); + } + [Fact] public void Setweight_With_Enum() { @@ -497,6 +543,57 @@ public void Setweight_With_Char_And_Lexemes() LIMIT 1"); } + [Fact] + public void Delete_With_Single_Lexeme() + { + using (var context = CreateContext()) + { + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b c").Delete("c")) + .First(); + Assert.Equal(NpgsqlTsVector.Parse("b:1").ToString(), tsVector.ToString()); + } + + AssertSql( + @"SELECT ts_delete(to_tsvector('b c'), 'c') +FROM ""Customers"" AS c +LIMIT 1"); + } + + [Fact] + public void Delete_With_Multiple_Lexemes() + { + using (var context = CreateContext()) + { + var tsVector = context.Customers + .Select(c => EF.Functions.ToTsVector("b c d").Delete(new[] { "c", "d" })) + .First(); + Assert.Equal(NpgsqlTsVector.Parse("b:1").ToString(), tsVector.ToString()); + } + + AssertSql( + @"SELECT ts_delete(to_tsvector('b c d'), ARRAY['c','d']) +FROM ""Customers"" AS c +LIMIT 1"); + } + + [Fact] + public void Filter() + { + using (var context = CreateContext()) + { + var tsVector = context.Customers + .Select(c => NpgsqlTsVector.Parse("b:1A c:2B d:3C").Filter(new[] { 'B', 'C' })) + .First(); + Assert.Equal(NpgsqlTsVector.Parse("c:2B d:3C").ToString(), tsVector.ToString()); + } + + AssertSql( + @"SELECT ts_filter(CAST('b:1A c:2B d:3C' AS tsvector), CAST(ARRAY['B','C'] AS ""char""[])) +FROM ""Customers"" AS c +LIMIT 1"); + } + [Fact] public void GetLength() {