未验证 提交 c6b84605 编写于 作者: S Shay Rojansky 提交者: GitHub

Sugar to easily configure tsvector generated columns (#1262)

Closes #1253
上级 e6cbfd6c
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities;
......@@ -48,6 +52,59 @@ public static class NpgsqlEntityTypeBuilderExtensions
#endregion xmin
#region Generated tsvector column
// Note: actual configuration for generated TsVector properties is on the property
/// <summary>
/// Configures a property on this entity to be a full-text search tsvector column over other given properties.
/// </summary>
/// <param name="entityTypeBuilder">The builder for the entity being configured.</param>
/// <param name="tsVectorPropertyExpression">
/// A lambda expression representing the property to be configured as a tsvector column
/// (<c>blog => blog.Url</c>).
/// </param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
/// <param name="includeExpression">
/// <para>
/// A lambda expression representing the property(s) to be included in the tsvector column
/// (<c>blog => blog.Url</c>).
/// </para>
/// <para>
/// If multiple properties are to be included then specify an anonymous type including the
/// properties (<c>post => new { post.Title, post.BlogId }</c>).
/// </para>
/// </param>
/// <returns>A builder to further configure the property.</returns>
public static EntityTypeBuilder<TEntity> HasGeneratedTsVectorProperty<TEntity, TProperty>(
[NotNull] this EntityTypeBuilder<TEntity> entityTypeBuilder,
[NotNull] Expression<Func<TEntity, TProperty>> tsVectorPropertyExpression,
[NotNull] string config,
[NotNull] Expression<Func<TEntity, object>> includeExpression)
where TEntity : class
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
Check.NotNull(tsVectorPropertyExpression, nameof(tsVectorPropertyExpression));
Check.NotNull(config, nameof(config));
Check.NotNull(includeExpression, nameof(includeExpression));
entityTypeBuilder.Property(tsVectorPropertyExpression).IsGeneratedTsVector(
config,
includeExpression.GetPropertyAccessList().Select(MemberInfoExtensions.GetSimpleMemberName).ToArray());
return entityTypeBuilder;
}
#endregion Generated tsvector column
#region Storage parameters
/// <summary>
......
using System;
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
......@@ -611,5 +612,136 @@ public static IConventionPropertyBuilder UseIdentityByDefaultColumn([NotNull] th
}
#endregion Identity options
#region Generated tsvector column
// Note: tsvector properties can be configured with a generic API through the entity type builder
/// <summary>
/// Configures the property to be a full-text search tsvector column over the given properties.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
/// <param name="includedPropertyNames">An array of property names to be included in the tsvector.</param>
/// <returns>A builder to further configure the property.</returns>
public static PropertyBuilder IsGeneratedTsVector(
[NotNull] this PropertyBuilder propertyBuilder,
[NotNull] string config,
[NotNull] params string[] includedPropertyNames)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));
Check.NotNull(config, nameof(config));
Check.NotEmpty(includedPropertyNames, nameof(includedPropertyNames));
propertyBuilder.HasColumnType("tsvector");
propertyBuilder.Metadata.SetGeneratedTsVectorConfig(config);
propertyBuilder.Metadata.SetGeneratedTsVectorProperties(includedPropertyNames);
return propertyBuilder;
}
/// <summary>
/// Configures the property to be a full-text search tsvector column over the given properties.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
/// <param name="includedPropertyNames">An array of property names to be included in the tsvector.</param>
/// <returns>A builder to further configure the property.</returns>
public static PropertyBuilder<TProperty> IsGeneratedTsVector<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder,
[NotNull] string config,
[NotNull] params string[] includedPropertyNames)
=> (PropertyBuilder<TProperty>)IsGeneratedTsVector((PropertyBuilder)propertyBuilder, config, includedPropertyNames);
/// <summary>
/// Configures the property to be a full-text search tsvector column over the given properties.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
/// <param name="includedPropertyNames">An array of property names to be included in the tsvector.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <c>null</c> otherwise.
/// </returns>
public static IConventionPropertyBuilder IsGeneratedTsVector(
[NotNull] this IConventionPropertyBuilder propertyBuilder,
[CanBeNull] string config,
[CanBeNull] IReadOnlyList<string> includedPropertyNames,
bool fromDataAnnotation = false)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));
if (propertyBuilder.CanSetIsGeneratedTsVector(config, includedPropertyNames, fromDataAnnotation))
{
propertyBuilder.HasColumnType("tsvector");
propertyBuilder.Metadata.SetGeneratedTsVectorConfig(config, fromDataAnnotation);
propertyBuilder.Metadata.SetGeneratedTsVectorProperties(includedPropertyNames, fromDataAnnotation);
return propertyBuilder;
}
return null;
}
/// <summary>
/// Returns a value indicating whether the property can be configured as a full-text search tsvector column.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
/// <param name="includedPropertyNames">An array of property names to be included in the tsvector.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><c>true</c> if the property can be configured as a full-text search tsvector column.</returns>
public static bool CanSetIsGeneratedTsVector(
[NotNull] this IConventionPropertyBuilder propertyBuilder,
[CanBeNull] string config,
[CanBeNull] IReadOnlyList<string> includedPropertyNames,
bool fromDataAnnotation = false)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));
return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention)
.Overrides(propertyBuilder.Metadata.GetGeneratedTsVectorConfigConfigurationSource()) &&
(fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention)
.Overrides(propertyBuilder.Metadata.GetGeneratedTsVectorPropertiesConfigurationSource())
||
config == propertyBuilder.Metadata.GetGeneratedTsVectorConfig() &&
StructuralComparisons.StructuralEqualityComparer.Equals(
includedPropertyNames, propertyBuilder.Metadata.GetGeneratedTsVectorProperties());
}
#endregion Generated tsvector column
}
}
using System;
using System.Globalization;
using System.Text;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
......@@ -477,5 +475,120 @@ public static void RemoveIdentityOptions([NotNull] this IConventionProperty prop
=> property.RemoveAnnotation(NpgsqlAnnotationNames.IdentityOptions);
#endregion Identity sequence options
#region Generated tsvector column
/// <summary>
/// Returns the text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </returns>
public static string GetGeneratedTsVectorConfig([NotNull] this IProperty property)
=> (string)property[NpgsqlAnnotationNames.GeneratedTsVectorConfig];
/// <summary>
/// Sets the text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
public static void SetGeneratedTsVectorConfig([NotNull] this IMutableProperty property, [CanBeNull] string config)
=> property.SetOrRemoveAnnotation(NpgsqlAnnotationNames.GeneratedTsVectorConfig, config);
/// <summary>
/// Returns the text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <param name="config">
/// <para>
/// The text search configuration for this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </para>
/// <para>
/// See https://www.postgresql.org/docs/current/textsearch-controls.html for more information.
/// </para>
/// </param>
public static void SetGeneratedTsVectorConfig(
[NotNull] this IConventionProperty property, [NotNull] string config, bool fromDataAnnotation = false)
=> property.SetOrRemoveAnnotation(
NpgsqlAnnotationNames.GeneratedTsVectorConfig,
config,
fromDataAnnotation);
/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for the text search configuration for the generated tsvector
/// property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The configuration source for the text search configuration for the generated tsvector property.</returns>
public static ConfigurationSource? GetGeneratedTsVectorConfigConfigurationSource([NotNull] this IConventionProperty property)
=> property.FindAnnotation(NpgsqlAnnotationNames.GeneratedTsVectorConfig)?.GetConfigurationSource();
/// <summary>
/// Returns the properties included in this generated tsvector property, or <c>null</c> if this is not a
/// generated tsvector property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The included property names, or <c>null</c> if this is not a Generated tsvector column.</returns>
public static IReadOnlyList<string> GetGeneratedTsVectorProperties([NotNull] this IProperty property)
=> (string[])property[NpgsqlAnnotationNames.GeneratedTsVectorProperties];
/// <summary>
/// Sets the properties included in this generated tsvector property, or <c>null</c> to make this a regular,
/// non-generated property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="properties">The included property names.</param>
public static void SetGeneratedTsVectorProperties(
[NotNull] this IMutableProperty property,
[CanBeNull] IReadOnlyList<string> properties)
=> property.SetOrRemoveAnnotation(
NpgsqlAnnotationNames.GeneratedTsVectorProperties,
properties);
/// <summary>
/// Sets properties included in this generated tsvector property, or <c>null</c> to make this a regular,
/// non-generated property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <param name="properties">The included property names.</param>
public static void SetGeneratedTsVectorProperties(
[NotNull] this IConventionProperty property,
[CanBeNull] IReadOnlyList<string> properties,
bool fromDataAnnotation = false)
=> property.SetOrRemoveAnnotation(
NpgsqlAnnotationNames.GeneratedTsVectorProperties,
properties,
fromDataAnnotation);
/// <summary>
/// Returns the <see cref="ConfigurationSource" /> for the properties included in the generated tsvector property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The configuration source for the properties included in the generated tsvector property.</returns>
public static ConfigurationSource? GetGeneratedTsVectorPropertiesConfigurationSource([NotNull] this IConventionProperty property)
=> property.FindAnnotation(NpgsqlAnnotationNames.GeneratedTsVectorConfig)?.GetConfigurationSource();
#endregion Generated tsvector column
}
}
......@@ -24,6 +24,8 @@ public static class NpgsqlAnnotationNames
public const string StorageParameterPrefix = Prefix + "StorageParameter:";
public const string UnloggedTable = Prefix + "UnloggedTable";
public const string IdentityOptions = Prefix + "IdentitySequenceOptions";
public const string GeneratedTsVectorProperties = Prefix + "GeneratedTsVectorProperties";
public const string GeneratedTsVectorConfig = Prefix + "GeneratedTsVectorConfig";
// Database model annotations
......
......@@ -45,6 +45,18 @@ public override IEnumerable<IAnnotation> For(IProperty property)
}
}
}
if (property.GetGeneratedTsVectorConfig() is string tsVectorConfig)
yield return new Annotation(NpgsqlAnnotationNames.GeneratedTsVectorConfig, tsVectorConfig);
if (property.GetGeneratedTsVectorProperties() is IReadOnlyList<string> tsVectorProperties)
{
yield return new Annotation(
NpgsqlAnnotationNames.GeneratedTsVectorProperties,
tsVectorProperties
.Select(p => property.DeclaringEntityType.FindProperty(p).GetColumnName())
.ToArray());
}
}
public override IEnumerable<IAnnotation> For(IIndex index)
......@@ -61,13 +73,11 @@ public override IEnumerable<IAnnotation> For(IIndex index)
yield return new Annotation(NpgsqlAnnotationNames.IndexNullSortOrder, nullSortOrder);
if (index.GetIncludeProperties() is IReadOnlyList<string> includeProperties)
{
var includeColumns = includeProperties
.Select(p => index.DeclaringEntityType.FindProperty(p).GetColumnName())
.ToArray();
yield return new Annotation(
NpgsqlAnnotationNames.IndexInclude,
includeColumns);
includeProperties
.Select(p => index.DeclaringEntityType.FindProperty(p).GetColumnName())
.ToArray());
}
var isCreatedConcurrently = index.IsCreatedConcurrently();
......
......@@ -1159,6 +1159,34 @@ protected override void Generate(CreateSequenceOperation operation, IModel model
}
}
if (operation[NpgsqlAnnotationNames.GeneratedTsVectorConfig] is string tsVectorConfig)
{
var tsVectorIncludedColumns = operation[NpgsqlAnnotationNames.GeneratedTsVectorProperties] as string[];
if (tsVectorIncludedColumns == null)
throw new InvalidOperationException(
$"{nameof(NpgsqlAnnotationNames.GeneratedTsVectorConfig)} is present in a migration but " +
$"{nameof(NpgsqlAnnotationNames.GeneratedTsVectorProperties)} is absent or empty");
var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
operation.ComputedColumnSql = new StringBuilder()
.Append("to_tsvector(")
.Append(stringTypeMapping.GenerateSqlLiteral(tsVectorConfig))
.Append(", ")
.Append(string.Join(" || ' ' || ", tsVectorIncludedColumns.Select(GetTsVectorColumnExpression)))
.Append(")")
.ToString();
string GetTsVectorColumnExpression(string includedColumn)
{
var delimitedColumnName = Dependencies.SqlGenerationHelper.DelimitIdentifier(includedColumn);
var property = FindProperty(model, schema, table, includedColumn);
return property?.IsColumnNullable() == true
? $"coalesce({delimitedColumnName}, '')"
: delimitedColumnName;
}
}
base.ColumnDefinition(
schema,
table,
......
......@@ -10,6 +10,7 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Scaffolding.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
using NpgsqlTypes;
using Xunit;
using Xunit.Abstractions;
......@@ -759,6 +760,37 @@ public virtual async Task Add_column_with_huge_varchar()
@"ALTER TABLE ""People"" ADD ""Name"" text NULL;");
}
[Fact]
public virtual async Task Add_column_generated_tsvector()
{
if (TestEnvironment.PostgresVersion.IsUnder(12))
{
await Assert.ThrowsAsync<NotSupportedException>(() => base.Add_column_with_computedSql());
return;
}
await Test(
builder => builder.Entity(
"People", e =>
{
e.Property<string>("Title").IsRequired();
e.Property<string>("Description");
}),
builder => { },
builder => builder.Entity("People").Property<NpgsqlTsVector>("TsVector")
.IsGeneratedTsVector("english", "Title", "Description"),
model =>
{
var table = Assert.Single(model.Tables);
var column = Assert.Single(table.Columns, c => c.Name == "TsVector");
Assert.Equal("tsvector", column.StoreType);
Assert.Equal(@"to_tsvector('english'::regconfig, ((""Title"" || ' '::text) || COALESCE(""Description"", ''::text)))", column.ComputedColumnSql);
});
AssertSql(
@"ALTER TABLE ""People"" ADD ""TsVector"" tsvector GENERATED ALWAYS AS (to_tsvector('english', ""Title"" || ' ' || coalesce(""Description"", ''))) STORED;");
}
public override async Task Alter_column_change_type()
{
await base.Alter_column_change_type();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册