-
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
base: main
Are you sure you want to change the base?
Changes from 5 commits
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 |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| // ReSharper disable once CheckNamespace | ||
|
|
||
| namespace Microsoft.EntityFrameworkCore; | ||
|
|
||
| /// <summary> | ||
| /// SQL Server specific extension methods for LINQ queries. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and | ||
| /// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see> | ||
| /// for more information and examples. | ||
| /// </remarks> | ||
| public static class SqlServerQueryableExtensions | ||
| { | ||
| /// <summary> | ||
| /// Provides a mapping to the SQL Server CONTAINSTABLE full-text search function. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and | ||
| /// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see> | ||
| /// for more information and examples. | ||
| /// </remarks> | ||
| /// <typeparam name="TEntity">The type of entity being queried.</typeparam> | ||
| /// <typeparam name="TKey">The type of the entity's primary key.</typeparam> | ||
| /// <param name="source">The source query.</param> | ||
| /// <param name="propertySelector">A lambda expression selecting 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<TKey>> ContainsTable<TEntity, TKey>( | ||
| this IQueryable<TEntity> source, | ||
| Expression<Func<TEntity, object>> propertySelector, | ||
| string searchCondition) | ||
| where TEntity : class | ||
| => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ContainsTable))); | ||
|
|
||
| /// <summary> | ||
| /// Provides a mapping to the SQL Server FREETEXTTABLE full-text search function. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and | ||
| /// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and Azure SQL databases with EF Core</see> | ||
| /// for more information and examples. | ||
| /// </remarks> | ||
| /// <typeparam name="TEntity">The type of entity being queried.</typeparam> | ||
| /// <typeparam name="TKey">The type of the entity's primary key.</typeparam> | ||
| /// <param name="source">The source query.</param> | ||
| /// <param name="propertySelector">A lambda expression selecting 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<TKey>> FreeTextTable<TEntity, TKey>( | ||
| this IQueryable<TEntity> source, | ||
| Expression<Func<TEntity, object>> propertySelector, | ||
| string freeText) | ||
| where TEntity : class | ||
| => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FreeTextTable))); | ||
|
|
||
| /// <summary> | ||
| /// The result type for SQL Server full-text table-valued functions (CONTAINSTABLE / FREETEXTTABLE). | ||
| /// </summary> | ||
| /// <typeparam name="TKey">The type of the key column.</typeparam> | ||
| public class FullTextTableResult<TKey> | ||
|
||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="FullTextTableResult{TKey}" /> class. | ||
| /// </summary> | ||
| /// <param name="key">The key of the row matched.</param> | ||
| /// <param name="rank">The ranking value assigned to the row.</param> | ||
| public FullTextTableResult(TKey key, int rank) | ||
|
||
| { | ||
| Key = key; | ||
| Rank = rank; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The key of the row matched. | ||
| /// </summary> | ||
| public TKey Key { get; set; } = default!; | ||
|
|
||
| /// <summary> | ||
| /// The ranking value assigned to the row. | ||
| /// </summary> | ||
| public int Rank { get; set; } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // 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="tableFragment">The SQL fragment representing the raw table name.</param> | ||
| /// <param name="columnFragment">The SQL fragment representing the raw column name.</param> | ||
| /// <param name="searchCondition">The search condition expression.</param> | ||
| /// <param name="alias">The table alias.</param> | ||
| public FullTextTableExpression( | ||
| string functionName, | ||
| SqlFragmentExpression tableFragment, | ||
| SqlFragmentExpression columnFragment, | ||
| SqlExpression searchCondition, | ||
| string alias) | ||
| : base(alias) | ||
| { | ||
| FunctionName = functionName; | ||
| TableFragment = tableFragment; | ||
| ColumnFragment = columnFragment; | ||
| SearchCondition = searchCondition; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The name of the function (e.g. CONTAINSTABLE). | ||
| /// </summary> | ||
| public virtual string FunctionName { get; } | ||
|
|
||
| /// <summary> | ||
| /// The SQL fragment representing the raw table name. | ||
| /// </summary> | ||
| public virtual SqlFragmentExpression TableFragment { get; } | ||
|
|
||
| /// <summary> | ||
| /// The SQL fragment representing the raw column name. | ||
| /// </summary> | ||
| public virtual SqlFragmentExpression ColumnFragment { get; } | ||
|
|
||
| /// <summary> | ||
| /// The search condition. | ||
| /// </summary> | ||
| public virtual SqlExpression SearchCondition { get; } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override TableExpressionBase WithAnnotations(IReadOnlyDictionary<string, IAnnotation> annotations) | ||
| => new FullTextTableExpression(FunctionName, TableFragment, ColumnFragment, SearchCondition, Alias!); | ||
|
|
||
| /// <inheritdoc /> | ||
| public override TableExpressionBase WithAlias(string newAlias) | ||
| => new FullTextTableExpression(FunctionName, TableFragment, ColumnFragment, SearchCondition, newAlias); | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override void Print(ExpressionPrinter expressionPrinter) | ||
| { | ||
| expressionPrinter.Append(FunctionName).Append("("); | ||
| expressionPrinter.Visit(TableFragment); | ||
| expressionPrinter.Append(", "); | ||
| expressionPrinter.Visit(ColumnFragment); | ||
| 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) | ||
| { | ||
| return new FullTextTableExpression( | ||
| FunctionName, | ||
| (SqlFragmentExpression)visitor.Visit(TableFragment), | ||
| (SqlFragmentExpression)visitor.Visit(ColumnFragment), | ||
| (SqlExpression)visitor.Visit(SearchCondition), | ||
| (alias ?? Alias)!); | ||
| } | ||
| } | ||
| 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,92 @@ 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; | ||||||
| if (method.DeclaringType == typeof(SqlServerQueryableExtensions) | ||||||
| && (method.Name == nameof(SqlServerQueryableExtensions.ContainsTable) | ||||||
| || method.Name == nameof(SqlServerQueryableExtensions.FreeTextTable))) | ||||||
| { | ||||||
| var functionName = method.Name == nameof(SqlServerQueryableExtensions.ContainsTable) | ||||||
| ? "CONTAINSTABLE" | ||||||
| : "FREETEXTTABLE"; | ||||||
|
|
||||||
| var source = Visit(methodCallExpression.Arguments[0]); | ||||||
| if (source is not ShapedQueryExpression shapedQuery | ||||||
| || shapedQuery.QueryExpression is not SelectExpression sourceSelectExpression) | ||||||
| { | ||||||
| return QueryCompilationContext.NotTranslatedExpression; | ||||||
| } | ||||||
|
|
||||||
| var lambda = methodCallExpression.Arguments[1].UnwrapLambdaFromQuote(); | ||||||
|
||||||
| if (lambda is not LambdaExpression lambdaExpression) | ||||||
| { | ||||||
| return QueryCompilationContext.NotTranslatedExpression; | ||||||
| } | ||||||
|
|
||||||
| var propertyTranslation = TranslateLambdaExpression(shapedQuery, lambdaExpression); | ||||||
| if (propertyTranslation is not ColumnExpression columnExpression) | ||||||
| { | ||||||
| AddTranslationErrorDetails(SqlServerStrings.InvalidColumnNameForFreeText); | ||||||
| return QueryCompilationContext.NotTranslatedExpression; | ||||||
| } | ||||||
| if (sourceSelectExpression.Tables.Count == 0 | ||||||
| || sourceSelectExpression.Tables[0] is not TableExpression tableExpression) | ||||||
|
||||||
| { | ||||||
| return QueryCompilationContext.NotTranslatedExpression; | ||||||
| } | ||||||
| var tableName = tableExpression.Name; | ||||||
| var columnName = columnExpression.Name; | ||||||
| var searchCondition = TranslateExpression(methodCallExpression.Arguments[2]); | ||||||
| if (searchCondition == null) | ||||||
| { | ||||||
| AddTranslationErrorDetails(CoreStrings.TranslationFailed(methodCallExpression.Arguments[2].Print())); | ||||||
| return QueryCompilationContext.NotTranslatedExpression; | ||||||
| } | ||||||
| var keyType = method.GetGenericArguments()[1]; // TKey is the second generic argument | ||||||
| var alias = _queryCompilationContext.SqlAliasManager.GenerateTableAlias("ft"); | ||||||
|
||||||
| var alias = _queryCompilationContext.SqlAliasManager.GenerateTableAlias("ft"); | |
| var alias = _queryCompilationContext.SqlAliasManager.GenerateTableAlias("fts"); |
Outdated
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.
This is wrong - identifier delimitation (the brackets) needs to be applied in query generation, not here in translation, as it's a SQL syntax thing (that's where we apply this for all other table/column names. Your code notably is missing escaping logic, so if a table name contains the ] character this won't escape it.
Outdated
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.
Do not call MakeGenericType, as it's incompatible with NativeAOT, which we're starting to move towards. You should already have the correct return type on the method in the inputted MethodCallExpression.
Outdated
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.
Call the columns KEY and RANK (uppercase) as per the documentation.
Outdated
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.
This is wrong - the select projects out both the key and the column; down below you're also mixing two projection type (this is a complicated and messy part of SelectExpression currently). Instantiate a SelectExpression without any projections, then call ReplaceProjection() with a dictionary argument to provide the two projections.
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.
Note that the two SQL Server functions accept two additional parameters: language and top_n_by_rank.