From d64e341a2fa1e01bcf89809de39bc7226bf9d678 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 6 Apr 2020 01:29:46 +0200 Subject: [PATCH] Add option for specifying stored/virtual on computed columns Closes #6682 --- .../CSharpMigrationOperationGenerator.cs | 31 ++++++++ .../Design/CSharpSnapshotGenerator.cs | 51 ++++++++++--- .../Internal/CSharpDbContextGenerator.cs | 6 +- .../RelationalScaffoldingModelFactory.cs | 2 +- .../RelationalPropertyBuilderExtensions.cs | 71 ++++++++++++++++++- .../RelationalPropertyExtensions.cs | 61 ++++++++++++++++ .../RelationalModelValidator.cs | 16 +++++ src/EFCore.Relational/Metadata/IColumn.cs | 7 ++ .../Metadata/RelationalAnnotationNames.cs | 5 ++ .../Internal/MigrationsModelDiffer.cs | 3 + .../Migrations/MigrationBuilder.cs | 9 +++ .../Operations/Builders/ColumnsBuilder.cs | 3 + .../Migrations/Operations/ColumnOperation.cs | 9 ++- .../Properties/RelationalStrings.Designer.cs | 8 +++ .../Properties/RelationalStrings.resx | 3 + .../Scaffolding/Metadata/DatabaseColumn.cs | 10 ++- .../Internal/SqlServerLoggerExtensions.cs | 6 +- .../SqlServerMigrationsSqlGenerator.cs | 7 ++ .../Properties/SqlServerStrings.resx | 4 +- .../Internal/SqlServerDatabaseModelFactory.cs | 7 +- .../CSharpMigrationOperationGeneratorTest.cs | 23 ++++-- .../MigrationsTestBase.cs | 46 ++++++++++-- .../MigrationsSqlServerTest.cs | 34 ++++++--- .../SqlServerDatabaseModelFactoryTest.cs | 24 +++++-- .../MigrationsSqliteTest.cs | 15 ++-- 25 files changed, 410 insertions(+), 51 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs index 8eb1912c7db..e9b0ad2c888 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs @@ -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) { @@ -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) { @@ -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) { @@ -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) { diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 1a357290caf..97f92781596 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -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, @@ -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, @@ -1354,6 +1348,47 @@ protected virtual void GenerateFluentApiForPrecisionAndScale( } } + /// + /// Generates a Fluent API call for the computed column annotations. + /// + /// The list of annotations. + /// The builder code is added to. + protected virtual void GenerateFluentApiForComputedColumn( + [NotNull] ref List 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); + } + /// /// Generates code for an annotation. /// diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 6ac3b3343d8..6de287bb1c1 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -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); @@ -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) diff --git a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs index 34aeea81d95..8b10e69dead 100644 --- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs +++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs @@ -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) diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs index 162a73e11b9..62112447e4d 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs @@ -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 @@ -360,16 +361,27 @@ public static bool CanSetDefaultValueSql( /// /// The builder for the property being configured. /// The SQL expression that computes values for the column. + /// + /// If true, the computed value is calculated on row modification and stored in the database like a regular column. + /// If false, the value is computed when the value is read, and does not occupy any actual storage. + /// null selects the database provider default. + /// /// The same builder instance so that multiple calls can be chained. 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; } @@ -379,11 +391,17 @@ public static PropertyBuilder HasComputedColumnSql( /// The type of the property being configured. /// The builder for the property being configured. /// The SQL expression that computes values for the column. + /// + /// If true, the computed value is calculated on row modification and stored in the database like a regular column. + /// If false, the value is computed when the value is read, and does not occupy any actual storage. + /// null selects the database provider default. + /// /// The same builder instance so that multiple calls can be chained. public static PropertyBuilder HasComputedColumnSql( [NotNull] this PropertyBuilder propertyBuilder, - [CanBeNull] string sql) - => (PropertyBuilder)HasComputedColumnSql((PropertyBuilder)propertyBuilder, sql); + [CanBeNull] string sql, + bool? stored = null) + => (PropertyBuilder)HasComputedColumnSql((PropertyBuilder)propertyBuilder, sql, stored); /// /// Configures the property to map to a computed column when targeting a relational database. @@ -409,6 +427,33 @@ public static IConventionPropertyBuilder HasComputedColumnSql( return propertyBuilder; } + /// + /// Configures the property to map to a computed column of the given type when targeting a relational database. + /// + /// The builder for the property being configured. + /// + /// If true, the computed value is calculated on row modification and stored in the database like a regular column. + /// If false, the value is computed when the value is read, and does not occupy any actual storage. + /// null selects the database provider default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, null otherwise. + /// + 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; + } + /// /// Returns a value indicating whether the given computed value SQL expression can be set for the column. /// @@ -425,6 +470,26 @@ public static bool CanSetComputedColumnSql( Check.NullButNotEmpty(sql, nameof(sql)), fromDataAnnotation); + /// + /// Returns a value indicating whether the given computed column type can be set for the column. + /// + /// The builder for the property being configured. + /// + /// If true, the computed value is calculated on row modification and stored in the database like a regular column. + /// If false, the value is computed when the value is read, and does not occupy any actual storage. + /// null selects the database provider default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given computed column type can be set for the column. + public static bool CanSetIsStoredComputedColumn( + [NotNull] this IConventionPropertyBuilder propertyBuilder, + bool? stored, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.ComputedColumnIsStored, + stored, + fromDataAnnotation); + /// /// /// Configures the default value for the column that the property maps diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index b4ff89d78a9..4a99b1b8e69 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -442,6 +442,67 @@ public static string SetComputedColumnSql( public static ConfigurationSource? GetComputedColumnSqlConfigurationSource([NotNull] this IConventionProperty property) => property.FindAnnotation(RelationalAnnotationNames.ComputedColumnSql)?.GetConfigurationSource(); + /// + /// Gets whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + /// The property. + /// + /// Whether the value of the computed column this property is mapped to is stored in the database, + /// or calculated when it is read. + /// + 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; + } + + /// + /// Sets whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + /// The property. + /// The value to set. + public static void SetComputedColumnIsStored([NotNull] this IMutableProperty property, bool? value) + => property.SetOrRemoveAnnotation( + RelationalAnnotationNames.ComputedColumnIsStored, + value); + + /// + /// Sets whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + /// The property. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetComputedColumnIsStored( + [NotNull] this IConventionProperty property, bool? value, bool fromDataAnnotation = false) + { + property.SetOrRemoveAnnotation(RelationalAnnotationNames.ComputedColumnIsStored, value, fromDataAnnotation); + + return value; + } + + /// + /// Gets the for the computed value SQL expression. + /// + /// The property. + /// The for the computed value SQL expression. + public static ConfigurationSource? GetComputedColumnIsStoredConfigurationSource([NotNull] this IConventionProperty property) + => property.FindAnnotation(RelationalAnnotationNames.ComputedColumnIsStored)?.GetConfigurationSource(); + /// /// Returns the object that is used as the default value for the column this property is mapped to. /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 7abda702622..549982fee58 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -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)) diff --git a/src/EFCore.Relational/Metadata/IColumn.cs b/src/EFCore.Relational/Metadata/IColumn.cs index a4b3bd89017..cfb402d8614 100644 --- a/src/EFCore.Relational/Metadata/IColumn.cs +++ b/src/EFCore.Relational/Metadata/IColumn.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata { @@ -83,6 +84,12 @@ public virtual object GetDefaultValue /// public virtual string ComputedColumnSql => PropertyMappings.First().Property.GetComputedColumnSql(); + /// + /// Returns whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + public virtual bool? ComputedColumnIsStored => PropertyMappings.First().Property.GetComputedColumnIsStored(); + /// /// Comment for this column /// diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index da5038f0045..dcbebb14183 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -41,6 +41,11 @@ public static class RelationalAnnotationNames /// public const string ComputedColumnSql = Prefix + "ComputedColumnSql"; + /// + /// The name for computed column type annotations. + /// + public const string ComputedColumnIsStored = Prefix + "ComputedColumnIsStored"; + /// /// The name for default value annotations. /// diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 3cc9c89232a..031c284c126 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -899,6 +899,7 @@ private bool PropertyStructureEquals(IProperty source, IProperty target) && source.IsFixedLength() == target.IsFixedLength() && source.GetConfiguredColumnType() == target.GetConfiguredColumnType() && source.GetComputedColumnSql() == target.GetComputedColumnSql() + && source.GetComputedColumnIsStored() == target.GetComputedColumnIsStored() && Equals(GetDefaultValue(source), GetDefaultValue(target)) && source.GetDefaultValueSql() == target.GetDefaultValueSql(); @@ -995,6 +996,7 @@ protected virtual IEnumerable Diff( || columnTypeChanged || source.DefaultValueSql != target.DefaultValueSql || source.ComputedColumnSql != target.ComputedColumnSql + || source.ComputedColumnIsStored != target.ComputedColumnIsStored || !Equals(GetDefaultValue(sourceProperty, GetValueConverter(sourceProperty, sourceTypeMapping)), GetDefaultValue(targetProperty, GetValueConverter(sourceProperty, targetTypeMapping))) || source.Comment != target.Comment @@ -1107,6 +1109,7 @@ private void Initialize( columnOperation.DefaultValueSql = column.DefaultValueSql; columnOperation.ComputedColumnSql = column.ComputedColumnSql; + columnOperation.ComputedColumnIsStored = column.ComputedColumnIsStored; columnOperation.Comment = column.Comment; columnOperation.Collation = column.Collation; columnOperation.AddAnnotations(migrationsAnnotations); diff --git a/src/EFCore.Relational/Migrations/MigrationBuilder.cs b/src/EFCore.Relational/Migrations/MigrationBuilder.cs index 323678d19d0..34fc0362777 100644 --- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs +++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs @@ -59,6 +59,7 @@ public MigrationBuilder([CanBeNull] string activeProvider) /// The default value for the column. /// The SQL expression to use for the column's default constraint. /// The SQL expression to use to compute the column value. + /// Whether the value of the computed column is stored in the database or not. /// Indicates whether or not the column is constrained to fixed-length data. /// A comment to associate with the column. /// A collation to apply to the column. @@ -81,6 +82,7 @@ public virtual OperationBuilder AddColumn( [CanBeNull] object defaultValue = null, [CanBeNull] string defaultValueSql = null, [CanBeNull] string computedColumnSql = null, + bool? computedColumnIsStored = null, bool? fixedLength = null, [CanBeNull] string comment = null, [CanBeNull] string collation = null, @@ -104,6 +106,7 @@ public virtual OperationBuilder AddColumn( DefaultValue = defaultValue, DefaultValueSql = defaultValueSql, ComputedColumnSql = computedColumnSql, + ComputedColumnIsStored = computedColumnIsStored, IsFixedLength = fixedLength, Comment = comment, Collation = collation, @@ -325,6 +328,7 @@ public virtual OperationBuilder AddUniqueConstrain /// The default value for the column. /// The SQL expression to use for the column's default constraint. /// The SQL expression to use to compute the column value. + /// Whether the value of the computed column is stored in the database or not. /// /// The CLR type that the column was previously mapped to. Can be null, in which case previous value is considered unknown. /// @@ -355,6 +359,7 @@ public virtual OperationBuilder AddUniqueConstrain /// /// The previous SQL expression used to compute the column value. Can be null, in which case previous value is considered unknown. /// + /// Whether the value of the previous computed column was stored in the database or not. /// Indicates whether or not the column is constrained to fixed-length data. /// Indicates whether or not the column was previously constrained to fixed-length data. /// A comment to associate with the column. @@ -386,6 +391,7 @@ public virtual AlterOperationBuilder AlterColumn( [CanBeNull] object defaultValue = null, [CanBeNull] string defaultValueSql = null, [CanBeNull] string computedColumnSql = null, + bool? computedColumnIsStored = null, [CanBeNull] Type oldClrType = null, [CanBeNull] string oldType = null, bool? oldUnicode = null, @@ -395,6 +401,7 @@ public virtual AlterOperationBuilder AlterColumn( [CanBeNull] object oldDefaultValue = null, [CanBeNull] string oldDefaultValueSql = null, [CanBeNull] string oldComputedColumnSql = null, + bool? oldComputedColumnIsStored = null, bool? fixedLength = null, bool? oldFixedLength = null, [CanBeNull] string comment = null, @@ -423,6 +430,7 @@ public virtual AlterOperationBuilder AlterColumn( DefaultValue = defaultValue, DefaultValueSql = defaultValueSql, ComputedColumnSql = computedColumnSql, + ComputedColumnIsStored = computedColumnIsStored, IsFixedLength = fixedLength, Comment = comment, Collation = collation, @@ -439,6 +447,7 @@ public virtual AlterOperationBuilder AlterColumn( DefaultValue = oldDefaultValue, DefaultValueSql = oldDefaultValueSql, ComputedColumnSql = oldComputedColumnSql, + ComputedColumnIsStored = oldComputedColumnIsStored, IsFixedLength = oldFixedLength, Comment = oldComment, Collation = oldCollation, diff --git a/src/EFCore.Relational/Migrations/Operations/Builders/ColumnsBuilder.cs b/src/EFCore.Relational/Migrations/Operations/Builders/ColumnsBuilder.cs index 52548a9b5bd..8be2b93c936 100644 --- a/src/EFCore.Relational/Migrations/Operations/Builders/ColumnsBuilder.cs +++ b/src/EFCore.Relational/Migrations/Operations/Builders/ColumnsBuilder.cs @@ -43,6 +43,7 @@ public ColumnsBuilder([NotNull] CreateTableOperation createTableOperation) /// The default value for the column. /// The SQL expression to use for the column's default constraint. /// The SQL expression to use to compute the column value. + /// Whether the value of the computed column is stored in the database or not. /// Indicates whether or not the column is constrained to fixed-length data. /// A comment to be applied to the column. /// A collation to be applied to the column. @@ -59,6 +60,7 @@ public virtual OperationBuilder Column( [CanBeNull] object defaultValue = null, [CanBeNull] string defaultValueSql = null, [CanBeNull] string computedColumnSql = null, + bool? computedColumnIsStored = null, bool? fixedLength = null, [CanBeNull] string comment = null, [CanBeNull] string collation = null, @@ -79,6 +81,7 @@ public virtual OperationBuilder Column( DefaultValue = defaultValue, DefaultValueSql = defaultValueSql, ComputedColumnSql = computedColumnSql, + ComputedColumnIsStored = computedColumnIsStored, IsFixedLength = fixedLength, Comment = comment, Collation = collation, diff --git a/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs b/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs index cc03d96e4d3..a4067cf1a9d 100644 --- a/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs @@ -3,6 +3,7 @@ using System; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Migrations.Operations { @@ -81,7 +82,13 @@ public class ColumnOperation : MigrationOperation public virtual string ComputedColumnSql { get; [param: CanBeNull] set; } /// - /// Comment for this column. + /// Whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + public virtual bool? ComputedColumnIsStored { get; set; } + + /// + /// Comment for this column /// public virtual string Comment { get; [param: CanBeNull] set; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 84253b0296a..63cd2e8728e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -284,6 +284,14 @@ public static string DuplicateColumnNameComputedSqlMismatch([CanBeNull] object e GetString("DuplicateColumnNameComputedSqlMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(value1), nameof(value2)), entityType1, property1, entityType2, property2, columnName, table, value1, value2); + /// + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different stored computed column settings ('{value1}' and '{value2}'). + /// + public static string DuplicateColumnNameComputedIsStoredMismatch([CanBeNull] object entityType1, [CanBeNull] object property1, [CanBeNull] object entityType2, [CanBeNull] object property2, [CanBeNull] object columnName, [CanBeNull] object table, [CanBeNull] object value1, [CanBeNull] object value2) + => string.Format( + GetString("DuplicateColumnNameComputedIsStoredMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(value1), nameof(value2)), + entityType1, property1, entityType2, property2, columnName, table, value1, value2); + /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different default values ('{value1}' and '{value2}'). /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 9d7e535ded0..b0fd389ec80 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -353,6 +353,9 @@ '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different computed values ('{value1}' and '{value2}'). + + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different stored computed column settings ('{value1}' and '{value2}'). + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different default values ('{value1}' and '{value2}'). diff --git a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs index b088ddc920b..47ff96a103d 100644 --- a/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs +++ b/src/EFCore.Relational/Scaffolding/Metadata/DatabaseColumn.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; #nullable enable @@ -47,10 +48,17 @@ public DatabaseColumn([NotNull] DatabaseTable table, [NotNull] string name, [Not public virtual string? DefaultValueSql { get; [param: CanBeNull] set; } /// - /// The SQL expression that computes the value of the column, or null if this is not a computed column. + /// Whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. /// public virtual string? ComputedColumnSql { get; [param: CanBeNull] set; } + /// + /// Whether the value of the computed column this property is mapped to is stored in the database, or calculated when + /// it is read. + /// + public virtual bool? ComputedColumnIsStored { get; set; } + /// /// The column comment, or null if none is set. /// diff --git a/src/EFCore.SqlServer/Internal/SqlServerLoggerExtensions.cs b/src/EFCore.SqlServer/Internal/SqlServerLoggerExtensions.cs index 8fc5b9c63e3..a04ae24ae5b 100644 --- a/src/EFCore.SqlServer/Internal/SqlServerLoggerExtensions.cs +++ b/src/EFCore.SqlServer/Internal/SqlServerLoggerExtensions.cs @@ -152,7 +152,8 @@ public static void ColumnFound( bool nullable, bool identity, [CanBeNull] string defaultValue, - [CanBeNull] string computedValue) + [CanBeNull] string computedValue, + bool? computedValueIsStored) { var definition = SqlServerResources.LogFoundColumn(diagnostics); @@ -174,7 +175,8 @@ public static void ColumnFound( nullable, identity, defaultValue, - computedValue)); + computedValue, + computedValueIsStored)); } // No DiagnosticsSource events because these are purely design-time messages diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 7f75332d437..3e8ef3e7e9d 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -11,6 +11,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; @@ -243,6 +244,7 @@ protected override void Generate( DefaultValue = operation.DefaultValue, DefaultValueSql = operation.DefaultValueSql, ComputedColumnSql = operation.ComputedColumnSql, + ComputedColumnIsStored = operation.ComputedColumnIsStored, IsFixedLength = operation.IsFixedLength }; addColumnOperation.AddAnnotations(operation.GetAnnotations()); @@ -1412,6 +1414,11 @@ protected override void ComputedColumnDefinition( .Append(" AS ") .Append(operation.ComputedColumnSql); + if (operation.ComputedColumnIsStored == true) + { + builder.Append(" PERSISTED"); + } + if (operation.Collation != null) { builder diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 054b057c58f..d57bff836ba 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -167,8 +167,8 @@ Debug SqlServerEventId.TypeAliasFound string string - Found column with table: {tableName}, column name: {columnName}, ordinal: {ordinal}, data type: {dataType}, maximum length: {maxLength}, precision: {precision}, scale: {scale}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue} - Debug SqlServerEventId.ColumnFound string string int string int int int bool bool string string + Found column with table: {tableName}, column name: {columnName}, ordinal: {ordinal}, data type: {dataType}, maximum length: {maxLength}, precision: {precision}, scale: {scale}, nullable: {isNullable}, identity: {isIdentity}, default value: {defaultValue}, computed value: {computedValue}, computed value is stored: {computedValueIsStored} + Debug SqlServerEventId.ColumnFound string string int string int int int bool bool string string bool? Found foreign key on table: {tableName}, name: {foreignKeyName}, principal table: {principalTableName}, delete action: {deleteAction}. diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index df7caeeb86d..c36014baf66 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -13,6 +13,7 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; @@ -613,6 +614,7 @@ private void GetColumns( [c].[is_identity], [dc].[definition] AS [default_sql], [cc].[definition] AS [computed_sql], + [cc].[is_persisted] AS [computed_is_persisted], CAST([e].[value] AS nvarchar(MAX)) AS [comment], [c].[collation_name] FROM @@ -673,6 +675,7 @@ UNION ALL var isIdentity = dataRecord.GetValueOrDefault("is_identity"); var defaultValue = dataRecord.GetValueOrDefault("default_sql"); var computedValue = dataRecord.GetValueOrDefault("computed_sql"); + var computedIsPersisted = dataRecord.GetValueOrDefault("computed_is_persisted"); var comment = dataRecord.GetValueOrDefault("comment"); var collation = dataRecord.GetValueOrDefault("collation_name"); @@ -687,7 +690,8 @@ UNION ALL nullable, isIdentity, defaultValue, - computedValue); + computedValue, + computedIsPersisted); string storeType; string systemTypeName; @@ -711,6 +715,7 @@ UNION ALL IsNullable = nullable, DefaultValueSql = defaultValue, ComputedColumnSql = computedValue, + ComputedColumnIsStored = computedIsPersisted, Comment = comment, Collation = collation == databaseCollation ? null : collation, ValueGenerated = isIdentity diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs index 4bc3da4d6c8..01a10a1f8f6 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs @@ -167,7 +167,8 @@ public void AddColumnOperation_ComputedExpression() Name = "Id", Table = "Post", ClrType = typeof(int), - ComputedColumnSql = "1" + ComputedColumnSql = "1", + ComputedColumnIsStored = true }, "mb.AddColumn(" + _eol @@ -177,13 +178,16 @@ public void AddColumnOperation_ComputedExpression() + _eol + " nullable: false," + _eol - + " computedColumnSql: \"1\");", + + " computedColumnSql: \"1\"," + + _eol + + " computedColumnIsStored: true);", o => { Assert.Equal("Id", o.Name); Assert.Equal("Post", o.Table); Assert.Equal(typeof(int), o.ClrType); Assert.Equal("1", o.ComputedColumnSql); + Assert.True(o.ComputedColumnIsStored); }); } @@ -734,7 +738,8 @@ public void AlterColumnOperation_computedColumnSql() Name = "Id", Table = "Post", ClrType = typeof(int), - ComputedColumnSql = "1" + ComputedColumnSql = "1", + ComputedColumnIsStored = true }, "mb.AlterColumn(" + _eol @@ -744,12 +749,15 @@ public void AlterColumnOperation_computedColumnSql() + _eol + " nullable: false," + _eol - + " computedColumnSql: \"1\");", + + " computedColumnSql: \"1\"," + + _eol + + " computedColumnIsStored: true);", o => { Assert.Equal("Id", o.Name); Assert.Equal("Post", o.Table); Assert.Equal("1", o.ComputedColumnSql); + Assert.True(o.ComputedColumnIsStored); Assert.Equal(typeof(int), o.ClrType); Assert.Null(o.ColumnType); Assert.Null(o.IsUnicode); @@ -771,6 +779,7 @@ public void AlterColumnOperation_computedColumnSql() Assert.Null(o.OldColumn.DefaultValue); Assert.Null(o.OldColumn.DefaultValueSql); Assert.Null(o.OldColumn.ComputedColumnSql); + Assert.Null(o.OldColumn.ComputedColumnIsStored); }); } @@ -1303,7 +1312,8 @@ public void CreateTableOperation_Columns_computedColumnSql() Name = "Id", Table = "Post", ClrType = typeof(int), - ComputedColumnSql = "1" + ComputedColumnSql = "1", + ComputedColumnIsStored = true } } }, @@ -1315,7 +1325,7 @@ public void CreateTableOperation_Columns_computedColumnSql() + _eol + " {" + _eol - + " Id = table.Column(nullable: false, computedColumnSql: \"1\")" + + " Id = table.Column(nullable: false, computedColumnSql: \"1\", computedColumnIsStored: true)" + _eol + " }," + _eol @@ -1332,6 +1342,7 @@ public void CreateTableOperation_Columns_computedColumnSql() Assert.Equal("Post", o.Columns[0].Table); Assert.Equal(typeof(int), o.Columns[0].ClrType); Assert.Equal("1", o.Columns[0].ComputedColumnSql); + Assert.True(o.Columns[0].ComputedColumnIsStored); }); } diff --git a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs index 7e42cab13a8..dd759190fea 100644 --- a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs @@ -345,8 +345,11 @@ public virtual Task Add_column_with_defaultValueSql() Assert.Contains("2", sumColumn.DefaultValueSql); }); - [ConditionalFact] - public virtual Task Add_column_with_computedSql() + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public virtual Task Add_column_with_computedSql(bool? computedColumnStored) => Test( builder => builder.Entity( "People", e => @@ -356,13 +359,16 @@ public virtual Task Add_column_with_computedSql() e.Property("Y"); }), builder => { }, - builder => builder.Entity("People").Property("Sum").HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}"), + builder => builder.Entity("People").Property("Sum") + .HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}", computedColumnStored), model => { var table = Assert.Single(model.Tables); var sumColumn = Assert.Single(table.Columns, c => c.Name == "Sum"); Assert.Contains("X", sumColumn.ComputedColumnSql); Assert.Contains("Y", sumColumn.ComputedColumnSql); + if (computedColumnStored != null) + Assert.Equal(computedColumnStored, sumColumn.ComputedColumnIsStored); }); // TODO: Check this out @@ -600,8 +606,11 @@ public virtual Task Alter_column_make_required_with_composite_index() Assert.Contains(table.Columns.Single(c => c.Name == "LastName"), index.Columns); }); - [ConditionalFact] - public virtual Task Alter_column_make_computed() + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public virtual Task Alter_column_make_computed(bool? computedColumnStored) => Test( builder => builder.Entity( "People", e => @@ -611,7 +620,8 @@ public virtual Task Alter_column_make_computed() e.Property("Y"); }), builder => builder.Entity("People").Property("Sum"), - builder => builder.Entity("People").Property("Sum").HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}"), + builder => builder.Entity("People").Property("Sum") + .HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}", computedColumnStored), model => { var table = Assert.Single(model.Tables); @@ -619,6 +629,8 @@ public virtual Task Alter_column_make_computed() Assert.Contains("X", sumColumn.ComputedColumnSql); Assert.Contains("Y", sumColumn.ComputedColumnSql); Assert.Contains("+", sumColumn.ComputedColumnSql); + if (computedColumnStored != null) + Assert.Equal(computedColumnStored, sumColumn.ComputedColumnIsStored); }); [ConditionalFact] @@ -643,6 +655,28 @@ public virtual Task Alter_column_change_computed() Assert.Contains("-", sumColumn.ComputedColumnSql); }); + [ConditionalFact] + public virtual Task Alter_column_change_computed_type() + => Test( + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.Property("X"); + e.Property("Y"); + e.Property("Sum"); + }), + builder => builder.Entity("People").Property("Sum") + .HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}", stored: false), + builder => builder.Entity("People").Property("Sum") + .HasComputedColumnSql($"{DelimitIdentifier("X")} + {DelimitIdentifier("Y")}", stored: true), + model => + { + var table = Assert.Single(model.Tables); + var sumColumn = Assert.Single(table.Columns, c => c.Name == "Sum"); + Assert.True(sumColumn.ComputedColumnIsStored); + }); + [ConditionalFact] public virtual Task Alter_column_add_comment() => Test( diff --git a/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs index 3ac2d0678ca..8318bee4e62 100644 --- a/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/MigrationsSqlServerTest.cs @@ -328,12 +328,14 @@ public override async Task Add_column_with_defaultValueSql() @"ALTER TABLE [People] ADD [Sum] int NOT NULL DEFAULT (1 + 2);"); } - public override async Task Add_column_with_computedSql() + public override async Task Add_column_with_computedSql(bool? computedColumnStored) { - await base.Add_column_with_computedSql(); + await base.Add_column_with_computedSql(computedColumnStored); + + var computedColumnTypeSql = computedColumnStored == true ? " PERSISTED" : ""; AssertSql( - @"ALTER TABLE [People] ADD [Sum] AS [X] + [Y];"); + @$"ALTER TABLE [People] ADD [Sum] AS [X] + [Y]{computedColumnTypeSql};"); } public override async Task Add_column_with_required() @@ -513,20 +515,21 @@ FROM [sys].[default_constraints] [d] CREATE INDEX [IX_People_FirstName_LastName] ON [People] ([FirstName], [LastName]);"); } - [ConditionalFact] - public override async Task Alter_column_make_computed() + public override async Task Alter_column_make_computed(bool? computedColumnStored) { - await base.Alter_column_make_computed(); + await base.Alter_column_make_computed(computedColumnStored); + + var computedColumnTypeSql = computedColumnStored == true ? " PERSISTED" : ""; AssertSql( - @"DECLARE @var0 sysname; + $@"DECLARE @var0 sysname; SELECT @var0 = [d].[name] FROM [sys].[default_constraints] [d] INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Sum'); IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];'); ALTER TABLE [People] DROP COLUMN [Sum]; -ALTER TABLE [People] ADD [Sum] AS [X] + [Y];"); +ALTER TABLE [People] ADD [Sum] AS [X] + [Y]{computedColumnTypeSql};"); } public override async Task Alter_column_change_computed() @@ -544,6 +547,21 @@ FROM [sys].[default_constraints] [d] ALTER TABLE [People] ADD [Sum] AS [X] - [Y];"); } + public override async Task Alter_column_change_computed_type() + { + await base.Alter_column_change_computed_type(); + + AssertSql( + @"DECLARE @var0 sysname; +SELECT @var0 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'Sum'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [People] DROP COLUMN [Sum]; +ALTER TABLE [People] ADD [Sum] AS [X] + [Y] PERSISTED;"); + } + [ConditionalFact] public override async Task Alter_column_add_comment() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index ab2e9bee668..69b5c98b7c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -1334,7 +1334,8 @@ FixedDefaultValue datetime2 NOT NULL DEFAULT ('October 20, 2015 11am'), ComputedValue AS GETDATE(), A int NOT NULL, B int NOT NULL, - SumOfAAndB AS A + B PERSISTED, + SumOfAAndB AS A + B, + SumOfAAndBPersisted AS A + B PERSISTED, );", Enumerable.Empty(), Enumerable.Empty(), @@ -1342,14 +1343,23 @@ ComputedValue AS GETDATE(), { var columns = dbModel.Tables.Single().Columns; - Assert.Equal("('October 20, 2015 11am')", columns.Single(c => c.Name == "FixedDefaultValue").DefaultValueSql); - Assert.Null(columns.Single(c => c.Name == "FixedDefaultValue").ComputedColumnSql); + var fixedDefaultValue = columns.Single(c => c.Name == "FixedDefaultValue"); + Assert.Equal("('October 20, 2015 11am')", fixedDefaultValue.DefaultValueSql); + Assert.Null(fixedDefaultValue.ComputedColumnSql); - Assert.Null(columns.Single(c => c.Name == "ComputedValue").DefaultValueSql); - Assert.Equal("(getdate())", columns.Single(c => c.Name == "ComputedValue").ComputedColumnSql); + var computedValue = columns.Single(c => c.Name == "ComputedValue"); + Assert.Null(computedValue.DefaultValueSql); + Assert.Equal("(getdate())", computedValue.ComputedColumnSql); - Assert.Null(columns.Single(c => c.Name == "SumOfAAndB").DefaultValueSql); - Assert.Equal("([A]+[B])", columns.Single(c => c.Name == "SumOfAAndB").ComputedColumnSql); + var sumOfAAndB = columns.Single(c => c.Name == "SumOfAAndB"); + Assert.Null(sumOfAAndB.DefaultValueSql); + Assert.Equal("([A]+[B])", sumOfAAndB.ComputedColumnSql); + Assert.False(sumOfAAndB.ComputedColumnIsStored); + + var sumOfAAndBPersisted = columns.Single(c => c.Name == "SumOfAAndBPersisted"); + Assert.Null(sumOfAAndBPersisted.DefaultValueSql); + Assert.Equal("([A]+[B])", sumOfAAndBPersisted.ComputedColumnSql); + Assert.True(sumOfAAndBPersisted.ComputedColumnIsStored); }, "DROP TABLE DefaultComputedValues;"); } diff --git a/test/EFCore.Sqlite.FunctionalTests/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/MigrationsSqliteTest.cs index 995c584abb0..adbc99d98bc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/MigrationsSqliteTest.cs @@ -212,8 +212,10 @@ public override async Task Add_column_with_defaultValueSql() Assert.Contains("Cannot add a column with non-constant default", ex.Message); } - public override Task Add_column_with_computedSql() - => AssertNotSupportedAsync(base.Add_column_with_computedSql, SqliteStrings.ComputedColumnsNotSupported); + public override Task Add_column_with_computedSql(bool? computedColumnStored) + => AssertNotSupportedAsync( + () => base.Add_column_with_computedSql(computedColumnStored), + SqliteStrings.ComputedColumnsNotSupported); public override async Task Add_column_with_max_length() { @@ -272,12 +274,17 @@ public override Task Alter_column_make_required_with_composite_index() => AssertNotSupportedAsync( base.Alter_column_make_required_with_composite_index, SqliteStrings.InvalidMigrationOperation("AlterColumnOperation")); - public override Task Alter_column_make_computed() - => AssertNotSupportedAsync(base.Alter_column_make_computed, SqliteStrings.InvalidMigrationOperation("AlterColumnOperation")); + public override Task Alter_column_make_computed(bool? computedColumnStored) + => AssertNotSupportedAsync( + () => base.Alter_column_make_computed(computedColumnStored), + SqliteStrings.InvalidMigrationOperation("AlterColumnOperation")); public override Task Alter_column_change_computed() => AssertNotSupportedAsync(base.Alter_column_change_computed, SqliteStrings.ComputedColumnsNotSupported); + public override Task Alter_column_change_computed_type() + => AssertNotSupportedAsync(base.Alter_column_change_computed, SqliteStrings.ComputedColumnsNotSupported); + public override Task Alter_column_add_comment() => AssertNotSupportedAsync(base.Alter_column_add_comment, SqliteStrings.InvalidMigrationOperation("AlterColumnOperation"));