-
Notifications
You must be signed in to change notification settings - Fork 3.4k
SQL Server: Support for full-text search TVFs (CONTAINSTABLE/FREETEXTTABLE) #37489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
9785ec0
8aeb974
5de1261
afb0bb4
db1007f
33510f6
622d95d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,48 @@ public static bool Contains( | |
| string searchCondition) | ||
| => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains))); | ||
|
|
||
| /// <summary> | ||
| /// Provides a mapping to the SQL Server CONTAINSTABLE full-text search function. | ||
| /// </summary> | ||
| /// <param name="_">The <see cref="DbFunctions" /> instance.</param> | ||
| /// <param name="property">The property to search.</param> | ||
| /// <param name="searchCondition">The search condition.</param> | ||
| /// <returns>The table-valued function result containing Key and Rank columns.</returns> | ||
| public static IQueryable<FullTextTableResult> ContainsTable( | ||
| this DbFunctions _, | ||
| object property, | ||
| string searchCondition) | ||
| => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsTable))); | ||
|
|
||
| /// <summary> | ||
| /// Provides a mapping to the SQL Server FREETEXTTABLE full-text search function. | ||
| /// </summary> | ||
| /// <param name="_">The <see cref="DbFunctions" /> instance.</param> | ||
| /// <param name="property">The property to search.</param> | ||
| /// <param name="freeText">The text that will be searched for in the property.</param> | ||
| /// <returns>The table-valued function result containing Key and Rank columns.</returns> | ||
| public static IQueryable<FullTextTableResult> FreeTextTable( | ||
| this DbFunctions _, | ||
| object property, | ||
|
||
| string freeText) | ||
| => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FreeTextTable))); | ||
|
|
||
| /// <summary> | ||
| /// The result type for SQL Server full-text table-valued functions (CONTAINSTABLE / FREETEXTTABLE). | ||
| /// </summary> | ||
| public class FullTextTableResult | ||
| { | ||
| /// <summary> | ||
| /// The key of the row matched. | ||
| /// </summary> | ||
| public object Key { get; set; } = default!; | ||
|
||
|
|
||
| /// <summary> | ||
| /// The ranking value assigned to the row. | ||
| /// </summary> | ||
| public int Rank { get; set; } | ||
| } | ||
|
|
||
| #endregion Full-text search | ||
|
|
||
| #region DateDiffYear | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||
| using Microsoft.EntityFrameworkCore.Query.SqlExpressions; | ||
| using static Microsoft.EntityFrameworkCore.SqlServerDbFunctionsExtensions; | ||
|
|
||
| // ReSharper disable once CheckNamespace | ||
| namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; | ||
|
|
||
| /// <summary> | ||
| /// An expression that represents a SQL Server full-text table-valued function (e.g., CONTAINSTABLE). | ||
| /// </summary> | ||
| public class FullTextTableExpression : TableExpressionBase | ||
|
||
| { | ||
| /// <summary> | ||
| /// Creates a new instance of the <see cref="FullTextTableExpression" /> class. | ||
| /// </summary> | ||
| /// <param name="functionName">The name of the full-text function.</param> | ||
| /// <param name="column">The column to search.</param> | ||
| /// <param name="searchCondition">The search condition expression.</param> | ||
| /// <param name="alias">The table alias.</param> | ||
| public FullTextTableExpression( | ||
| string functionName, | ||
| ColumnExpression column, | ||
| SqlExpression searchCondition, | ||
| string alias) | ||
| : base(alias) | ||
| { | ||
| FunctionName = functionName; | ||
| Column = column; | ||
| SearchCondition = searchCondition; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The name of the function (e.g. CONTAINSTABLE). | ||
| /// </summary> | ||
| public virtual string FunctionName { get; } | ||
|
|
||
| /// <summary> | ||
| /// The column being searched. | ||
| /// </summary> | ||
| public virtual ColumnExpression Column { get; } | ||
|
|
||
| /// <summary> | ||
| /// The search condition. | ||
| /// </summary> | ||
| public virtual SqlExpression SearchCondition { get; } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override TableExpressionBase WithAnnotations(IReadOnlyDictionary<string, IAnnotation> annotations) | ||
| => this; | ||
|
|
||
| /// <inheritdoc /> | ||
| public override TableExpressionBase WithAlias(string newAlias) | ||
| => new FullTextTableExpression(FunctionName, Column, SearchCondition, newAlias); | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override void Print(ExpressionPrinter expressionPrinter) | ||
| { | ||
| expressionPrinter.Append(FunctionName!).Append("(").Append(Column.TableAlias!).Append(", "); | ||
| expressionPrinter.Visit(Column); | ||
| expressionPrinter.Append(", "); | ||
| expressionPrinter.Visit(SearchCondition); | ||
| expressionPrinter.Append(") AS ").Append(Alias!); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override TableExpressionBase Quote() | ||
| => this; | ||
|
|
||
| /// <inheritdoc /> | ||
| public override TableExpressionBase Clone(string? alias, ExpressionVisitor visitor) | ||
| { | ||
| var newColumn = (ColumnExpression)visitor.Visit(Column); | ||
| var newSearchCondition = (SqlExpression)visitor.Visit(SearchCondition); | ||
|
|
||
| return new FullTextTableExpression(FunctionName, newColumn, newSearchCondition, alias ?? Alias!); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -622,6 +622,9 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy | |
|
|
||
| case SqlServerOpenJsonExpression openJsonExpression: | ||
| return VisitOpenJsonExpression(openJsonExpression); | ||
|
|
||
| case FullTextTableExpression fullTextTableExpression: | ||
| return VisitFullTextTable(fullTextTableExpression); | ||
| } | ||
|
|
||
| return base.VisitExtension(extensionExpression); | ||
|
|
@@ -914,6 +917,28 @@ protected override bool TryGetOperatorInfo(SqlExpression expression, out int pre | |
| return precedence != default; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
| /// 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> | ||
| protected virtual Expression VisitFullTextTable(FullTextTableExpression fullTextTableExpression) | ||
| { | ||
| Sql.Append(fullTextTableExpression.FunctionName) | ||
| .Append("(") | ||
| .Append(_sqlGenerationHelper.DelimitIdentifier(fullTextTableExpression.Column.TableAlias!)) | ||
|
||
| .Append(", "); | ||
| Visit(fullTextTableExpression.Column); | ||
| Sql.Append(", "); | ||
| Visit(fullTextTableExpression.SearchCondition); | ||
| Sql.Append(")") | ||
| .Append(AliasSeparator) | ||
| .Append(_sqlGenerationHelper.DelimitIdentifier(fullTextTableExpression.Alias!)); | ||
|
|
||
| return fullTextTableExpression; | ||
| } | ||
|
|
||
| private void GenerateList<T>( | ||
| IReadOnlyList<T> items, | ||
| Action<T> generationAction, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Reflection; | ||
| using Microsoft.EntityFrameworkCore.Query.SqlExpressions; | ||
| using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; | ||
| using Microsoft.EntityFrameworkCore.SqlServer.Internal; | ||
|
|
@@ -71,6 +72,79 @@ protected SqlServerQueryableMethodTranslatingExpressionVisitor( | |
| protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() | ||
| => new SqlServerQueryableMethodTranslatingExpressionVisitor(this); | ||
|
|
||
| /// <summary> | ||
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
| /// 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> | ||
| protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) | ||
| { | ||
| var method = methodCallExpression.Method; | ||
|
|
||
| // Handle ContainsTable and FreeTextTable table-valued functions | ||
| if (method.DeclaringType == typeof(SqlServerDbFunctionsExtensions) | ||
| && (method.Name == nameof(SqlServerDbFunctionsExtensions.ContainsTable) | ||
| || method.Name == nameof(SqlServerDbFunctionsExtensions.FreeTextTable))) | ||
| { | ||
| var functionName = method.Name == nameof(SqlServerDbFunctionsExtensions.ContainsTable) | ||
| ? "CONTAINSTABLE" | ||
| : "FREETEXTTABLE"; | ||
|
|
||
| // Translate the arguments | ||
| var propertyReference = TranslateExpression(methodCallExpression.Arguments[1]); | ||
| if (propertyReference is not ColumnExpression columnExpression) | ||
| { | ||
| throw new InvalidOperationException(SqlServerStrings.InvalidColumnNameForFreeText); | ||
| } | ||
|
|
||
| var searchCondition = TranslateExpression(methodCallExpression.Arguments[2]); | ||
| if (searchCondition == null) | ||
| { | ||
| AddTranslationErrorDetails( | ||
| CoreStrings.TranslationFailed(methodCallExpression.Arguments[2].Print())); | ||
| return QueryCompilationContext.NotTranslatedExpression; | ||
| } | ||
|
|
||
| // Create the FullTextTableExpression | ||
| var alias = _queryCompilationContext.SqlAliasManager.GenerateTableAlias("ct"); | ||
| var fullTextTableExpression = new FullTextTableExpression(functionName, columnExpression, searchCondition, alias); | ||
|
|
||
| // Create columns for Key and Rank (CONTAINSTABLE/FREETEXTTABLE return these columns) | ||
| var resultType = typeof(SqlServerDbFunctionsExtensions.FullTextTableResult); | ||
| var keyColumn = new ColumnExpression("Key", alias, typeof(object), null, nullable: false); | ||
| var rankColumn = new ColumnExpression("Rank", alias, typeof(int), _typeMappingSource.FindMapping(typeof(int)), nullable: false); | ||
|
|
||
| // Create SelectExpression with Key as the main projection | ||
| // Use Key as identifier since it's the primary key from CONTAINSTABLE/FREETEXTTABLE | ||
| var keyTypeMapping = _typeMappingSource.FindMapping(typeof(object)); | ||
| #pragma warning disable EF1001 // Internal EF Core API usage. | ||
| var selectExpression = new SelectExpression( | ||
| [fullTextTableExpression], | ||
| keyColumn, | ||
|
||
| [(keyColumn, keyTypeMapping?.Comparer ?? ValueComparer.CreateDefault(typeof(object), favorStructuralComparisons: false))], | ||
| _queryCompilationContext.SqlAliasManager); | ||
| #pragma warning restore EF1001 // Internal EF Core API usage. | ||
|
|
||
| // Add Rank to projection | ||
| var rankIndex = selectExpression.AddToProjection(rankColumn); | ||
|
|
||
| // Create shaper expression for FullTextTableResult | ||
| // Key is in the projection mapping (main projection), Rank is at rankIndex in the projection list | ||
| var keyBinding = new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(object)); | ||
| var rankBinding = new ProjectionBindingExpression(selectExpression, rankIndex, typeof(int)); | ||
|
|
||
| var shaperExpression = Expression.New( | ||
| resultType.GetConstructor([typeof(object), typeof(int)])!, | ||
| keyBinding, | ||
| rankBinding); | ||
|
|
||
| return new ShapedQueryExpression(selectExpression, shaperExpression); | ||
| } | ||
|
|
||
| return base.VisitMethodCall(methodCallExpression); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -817,4 +817,24 @@ protected override DbParameter CreateDbParameter(string name, object value) | |
|
|
||
| private void AssertSql(params string[] expected) | ||
| => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); | ||
|
|
||
|
||
| [ConditionalFact] | ||
| public virtual void FullText_ContainsTable_queryable_simple() | ||
|
||
| { | ||
| using var ctx = Fixture.CreateContext(); | ||
|
|
||
Abde1rahman1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| var query = from c in ctx.Set<Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer>() | ||
| join ct in EF.Functions.ContainsTable(ctx.Set<TestModels.Northwind.Customer>().Select(x => x.ContactName), "John") | ||
|
||
| on c.CustomerID equals (string)ct.Key | ||
| select new { c.ContactName, ct.Rank }; | ||
|
|
||
| var result = query.ToList(); | ||
| AssertSql( | ||
| """ | ||
| SELECT [c].[ContactName], [c0].[Rank] | ||
| FROM [Customers] AS [c] | ||
| INNER JOIN CONTAINSTABLE([Customers], [ContactName], N'John') AS [c0] ON [c].[CustomerID] = [c0].[Key] | ||
| """); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move these to SqlServerQueryableExtensions, similar to RelationalQueryableExtensions.