Skip to content

Commit

Permalink
Add option for specifying stored/virtual on computed columns
Browse files Browse the repository at this point in the history
Closes #6682
  • Loading branch information
roji committed Apr 22, 2020
1 parent f7e2cd1 commit d64e341
Show file tree
Hide file tree
Showing 25 changed files with 410 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ protected virtual void Generate([NotNull] AddColumnOperation operation, [NotNull
.AppendLine(",")
.Append("computedColumnSql: ")
.Append(Code.Literal(operation.ComputedColumnSql));

if (operation.ComputedColumnIsStored != null)
{
builder
.AppendLine(",")
.Append("computedColumnIsStored: ")
.Append(Code.Literal(operation.ComputedColumnIsStored));
}
}
else if (operation.DefaultValue != null)
{
Expand Down Expand Up @@ -552,6 +560,14 @@ protected virtual void Generate([NotNull] AlterColumnOperation operation, [NotNu
.AppendLine(",")
.Append("computedColumnSql: ")
.Append(Code.Literal(operation.ComputedColumnSql));

if (operation.ComputedColumnIsStored != null)
{
builder
.AppendLine(",")
.Append("computedColumnIsStored: ")
.Append(Code.Literal(operation.ComputedColumnIsStored));
}
}
else if (operation.DefaultValue != null)
{
Expand Down Expand Up @@ -653,6 +669,14 @@ protected virtual void Generate([NotNull] AlterColumnOperation operation, [NotNu
.AppendLine(",")
.Append("oldComputedColumnSql: ")
.Append(Code.Literal(operation.OldColumn.ComputedColumnSql));

if (operation.ComputedColumnIsStored != null)
{
builder
.AppendLine(",")
.Append("oldComputedColumnIsStored: ")
.Append(Code.Literal(operation.OldColumn.ComputedColumnIsStored));
}
}
else if (operation.OldColumn.DefaultValue != null)
{
Expand Down Expand Up @@ -1152,6 +1176,13 @@ protected virtual void Generate([NotNull] CreateTableOperation operation, [NotNu
builder
.Append(", computedColumnSql: ")
.Append(Code.Literal(column.ComputedColumnSql));

if (column.ComputedColumnIsStored != null)
{
builder
.Append(", computedColumnIsStored: ")
.Append(Code.Literal(column.ComputedColumnIsStored));
}
}
else if (column.DefaultValue != null)
{
Expand Down
51 changes: 43 additions & 8 deletions src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,11 +552,7 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property,
nameof(RelationalPropertyBuilderExtensions.HasDefaultValueSql),
stringBuilder);

GenerateFluentApiForAnnotation(
ref annotations,
RelationalAnnotationNames.ComputedColumnSql,
nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql),
stringBuilder);
GenerateFluentApiForComputedColumn(ref annotations, stringBuilder);

GenerateFluentApiForAnnotation(
ref annotations,
Expand All @@ -582,9 +578,7 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property,
nameof(PropertyBuilder.HasMaxLength),
stringBuilder);

GenerateFluentApiForPrecisionAndScale(
ref annotations,
stringBuilder);
GenerateFluentApiForPrecisionAndScale(ref annotations, stringBuilder);

GenerateFluentApiForAnnotation(
ref annotations,
Expand Down Expand Up @@ -1354,6 +1348,47 @@ protected virtual void GenerateFluentApiForPrecisionAndScale(
}
}

/// <summary>
/// Generates a Fluent API call for the computed column annotations.
/// </summary>
/// <param name="annotations"> The list of annotations. </param>
/// <param name="stringBuilder"> The builder code is added to. </param>
protected virtual void GenerateFluentApiForComputedColumn(
[NotNull] ref List<IAnnotation> annotations,
[NotNull] IndentedStringBuilder stringBuilder)
{
var sql = annotations
.FirstOrDefault(a => a.Name == RelationalAnnotationNames.ComputedColumnSql);

if (sql is null)
{
return;
}

stringBuilder
.AppendLine()
.Append(".")
.Append(nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql))
.Append("(")
.Append(Code.UnknownLiteral(sql.Value));

var isStored = annotations
.FirstOrDefault(a => a.Name == RelationalAnnotationNames.ComputedColumnIsStored);

if (isStored != null)
{
stringBuilder
.Append(", ")
.Append(Code.UnknownLiteral(isStored.Value));

annotations.Remove(isStored);
}

stringBuilder.Append(")");

annotations.Remove(sql);
}

/// <summary>
/// Generates code for an annotation.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
RemoveAnnotation(ref annotations, RelationalAnnotationNames.Comment);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.Collation);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.ComputedColumnSql);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.ComputedColumnIsStored);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.IsFixedLength);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.TableColumnMappings);
RemoveAnnotation(ref annotations, RelationalAnnotationNames.ViewColumnMappings);
Expand Down Expand Up @@ -742,7 +743,10 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
{
lines.Add(
$".{nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql)}" +
$"({_code.Literal(property.GetComputedColumnSql())})");
$"({_code.Literal(property.GetComputedColumnSql())}" +
(property.GetComputedColumnIsStored() is bool computedColumnIsStored
? $", stored: {_code.Literal(computedColumnIsStored)})"
: ")"));
}

if (property.GetComment() != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ protected virtual PropertyBuilder VisitColumn([NotNull] EntityTypeBuilder builde

if (column.ComputedColumnSql != null)
{
property.HasComputedColumnSql(column.ComputedColumnSql);
property.HasComputedColumnSql(column.ComputedColumnSql, column.ComputedColumnIsStored);
}

if (column.Comment != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -360,16 +361,27 @@ public static bool CanSetDefaultValueSql(
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="sql"> The SQL expression that computes values for the column. </param>
/// <param name="stored">
/// If <c>true</c>, the computed value is calculated on row modification and stored in the database like a regular column.
/// If <c>false</c>, the value is computed when the value is read, and does not occupy any actual storage.
/// <c>null</c> selects the database provider default.
/// </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder HasComputedColumnSql(
[NotNull] this PropertyBuilder propertyBuilder,
[CanBeNull] string sql)
[CanBeNull] string sql,
bool? stored = null)
{
Check.NotNull(propertyBuilder, nameof(propertyBuilder));
Check.NullButNotEmpty(sql, nameof(sql));

propertyBuilder.Metadata.SetComputedColumnSql(sql);

if (stored != null)
{
propertyBuilder.Metadata.SetComputedColumnIsStored(stored);
}

return propertyBuilder;
}

Expand All @@ -379,11 +391,17 @@ public static PropertyBuilder HasComputedColumnSql(
/// <typeparam name="TProperty"> The type of the property being configured. </typeparam>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="sql"> The SQL expression that computes values for the column. </param>
/// <param name="stored">
/// If <c>true</c>, the computed value is calculated on row modification and stored in the database like a regular column.
/// If <c>false</c>, the value is computed when the value is read, and does not occupy any actual storage.
/// <c>null</c> selects the database provider default.
/// </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static PropertyBuilder<TProperty> HasComputedColumnSql<TProperty>(
[NotNull] this PropertyBuilder<TProperty> propertyBuilder,
[CanBeNull] string sql)
=> (PropertyBuilder<TProperty>)HasComputedColumnSql((PropertyBuilder)propertyBuilder, sql);
[CanBeNull] string sql,
bool? stored = null)
=> (PropertyBuilder<TProperty>)HasComputedColumnSql((PropertyBuilder)propertyBuilder, sql, stored);

/// <summary>
/// Configures the property to map to a computed column when targeting a relational database.
Expand All @@ -409,6 +427,33 @@ public static IConventionPropertyBuilder HasComputedColumnSql(
return propertyBuilder;
}

/// <summary>
/// Configures the property to map to a computed column of the given type when targeting a relational database.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="stored">
/// If <c>true</c>, the computed value is calculated on row modification and stored in the database like a regular column.
/// If <c>false</c>, the value is computed when the value is read, and does not occupy any actual storage.
/// <c>null</c> selects the database provider default.
/// </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 IsStoredComputedColumn(
[NotNull] this IConventionPropertyBuilder propertyBuilder,
bool? stored,
bool fromDataAnnotation = false)
{
if (!propertyBuilder.CanSetIsStoredComputedColumn(stored, fromDataAnnotation))
{
return null;
}

propertyBuilder.Metadata.SetComputedColumnIsStored(stored, fromDataAnnotation);
return propertyBuilder;
}

/// <summary>
/// Returns a value indicating whether the given computed value SQL expression can be set for the column.
/// </summary>
Expand All @@ -425,6 +470,26 @@ public static bool CanSetComputedColumnSql(
Check.NullButNotEmpty(sql, nameof(sql)),
fromDataAnnotation);

/// <summary>
/// Returns a value indicating whether the given computed column type can be set for the column.
/// </summary>
/// <param name="propertyBuilder"> The builder for the property being configured. </param>
/// <param name="stored">
/// If <c>true</c>, the computed value is calculated on row modification and stored in the database like a regular column.
/// If <c>false</c>, the value is computed when the value is read, and does not occupy any actual storage.
/// <c>null</c> selects the database provider default.
/// </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> <c>true</c> if the given computed column type can be set for the column. </returns>
public static bool CanSetIsStoredComputedColumn(
[NotNull] this IConventionPropertyBuilder propertyBuilder,
bool? stored,
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(
RelationalAnnotationNames.ComputedColumnIsStored,
stored,
fromDataAnnotation);

/// <summary>
/// <para>
/// Configures the default value for the column that the property maps
Expand Down
61 changes: 61 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,67 @@ public static string SetComputedColumnSql(
public static ConfigurationSource? GetComputedColumnSqlConfigurationSource([NotNull] this IConventionProperty property)
=> property.FindAnnotation(RelationalAnnotationNames.ComputedColumnSql)?.GetConfigurationSource();

/// <summary>
/// Gets whether the value of the computed column this property is mapped to is stored in the database, or calculated when
/// it is read.
/// </summary>
/// <param name="property"> The property. </param>
/// <returns>
/// Whether the value of the computed column this property is mapped to is stored in the database,
/// or calculated when it is read.
/// </returns>
public static bool? GetComputedColumnIsStored([NotNull] this IProperty property)
{
var computedColumnIsStored = (bool?)property[RelationalAnnotationNames.ComputedColumnIsStored];
if (computedColumnIsStored != null)
{
return computedColumnIsStored;
}

var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedRootPrimaryKeyProperty();
if (sharedTablePrincipalPrimaryKeyProperty != null)
{
return GetComputedColumnIsStored(sharedTablePrincipalPrimaryKeyProperty);
}

return null;
}

/// <summary>
/// Sets whether the value of the computed column this property is mapped to is stored in the database, or calculated when
/// it is read.
/// </summary>
/// <param name="property"> The property. </param>
/// <param name="value"> The value to set. </param>
public static void SetComputedColumnIsStored([NotNull] this IMutableProperty property, bool? value)
=> property.SetOrRemoveAnnotation(
RelationalAnnotationNames.ComputedColumnIsStored,
value);

/// <summary>
/// Sets whether the value of the computed column this property is mapped to is stored in the database, or calculated when
/// it is read.
/// </summary>
/// <param name="property"> The property. </param>
/// <param name="value"> The value to set. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> The configured value. </returns>
public static bool? SetComputedColumnIsStored(
[NotNull] this IConventionProperty property, bool? value, bool fromDataAnnotation = false)
{
property.SetOrRemoveAnnotation(RelationalAnnotationNames.ComputedColumnIsStored, value, fromDataAnnotation);

return value;
}

/// <summary>
/// Gets the <see cref="ConfigurationSource" /> for the computed value SQL expression.
/// </summary>
/// <param name="property"> The property. </param>
/// <returns> The <see cref="ConfigurationSource" /> for the computed value SQL expression. </returns>
public static ConfigurationSource? GetComputedColumnIsStoredConfigurationSource([NotNull] this IConventionProperty property)
=> property.FindAnnotation(RelationalAnnotationNames.ComputedColumnIsStored)?.GetConfigurationSource();

/// <summary>
/// Returns the object that is used as the default value for the column this property is mapped to.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,22 @@ protected virtual void ValidateCompatible(
currentComputedColumnSql));
}

var currentComputedColumnIsStored = property.GetComputedColumnIsStored();
var previousComputedColumnIsStored = duplicateProperty.GetComputedColumnIsStored();
if (currentComputedColumnIsStored != previousComputedColumnIsStored)
{
throw new InvalidOperationException(
RelationalStrings.DuplicateColumnNameComputedIsStoredMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
tableName,
previousComputedColumnIsStored,
currentComputedColumnIsStored));
}

var currentDefaultValue = property.GetDefaultValue();
var previousDefaultValue = duplicateProperty.GetDefaultValue();
if (!Equals(currentDefaultValue, previousDefaultValue))
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.Relational/Metadata/IColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata
{
Expand Down Expand Up @@ -83,6 +84,12 @@ public virtual object GetDefaultValue
/// </summary>
public virtual string ComputedColumnSql => PropertyMappings.First().Property.GetComputedColumnSql();

/// <summary>
/// Returns whether the value of the computed column this property is mapped to is stored in the database, or calculated when
/// it is read.
/// </summary>
public virtual bool? ComputedColumnIsStored => PropertyMappings.First().Property.GetComputedColumnIsStored();

/// <summary>
/// Comment for this column
/// </summary>
Expand Down
Loading

0 comments on commit d64e341

Please sign in to comment.