From b1be679cbf218ff11123f1535f07ea482cdf0a38 Mon Sep 17 00:00:00 2001 From: Neil Bostrom Date: Sat, 18 May 2019 10:40:18 +0100 Subject: [PATCH] Allow table/column comments to be specified in the model --- .../CSharpMigrationOperationGenerator.cs | 44 +++++- .../Design/CSharpSnapshotGenerator.cs | 17 +++ .../RelationalEntityTypeBuilderExtensions.cs | 73 ++++++++++ .../RelationalEntityTypeExtensions.cs | 27 ++++ .../RelationalPropertyBuilderExtensions.cs | 69 +++++++++ .../RelationalPropertyExtensions.cs | 41 ++++++ .../RelationalModelValidator.cs | 29 ++++ .../Metadata/Internal/TableMapping.cs | 10 ++ .../Metadata/RelationalAnnotationNames.cs | 5 + .../Internal/MigrationsModelDiffer.cs | 10 +- .../Migrations/MigrationBuilder.cs | 31 +++-- .../Migrations/MigrationsSqlGenerator.cs | 41 ++++++ .../Operations/AlterTableOperation.cs | 5 + .../Migrations/Operations/ColumnOperation.cs | 5 + .../Operations/CreateTableOperation.cs | 5 + .../Properties/RelationalStrings.Designer.cs | 16 +++ .../Properties/RelationalStrings.resx | 6 + .../SqlServerMigrationsSqlGenerator.cs | 129 +++++++++++++++++ .../CSharpMigrationOperationGeneratorTest.cs | 77 +++++++++- .../Design/CSharpMigrationsGeneratorTest.cs | 12 +- .../RelationalModelValidatorTest.cs | 29 ++++ ...RelationalMetadataBuilderExtensionsTest.cs | 16 +++ .../SqlServerMigrationSqlGeneratorTest.cs | 131 ++++++++++++++++++ 23 files changed, 808 insertions(+), 20 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs index 3503e25345c..5d8bc4518be 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpMigrationOperationGenerator.cs @@ -182,6 +182,14 @@ protected virtual void Generate([NotNull] AddColumnOperation operation, [NotNull .Append(Code.UnknownLiteral(operation.DefaultValue)); } + if (operation.Comment != null) + { + builder + .AppendLine(",") + .Append("comment: ") + .Append(Code.Literal(operation.Comment)); + } + builder.Append(")"); Annotations(operation.GetAnnotations(), builder); @@ -518,6 +526,14 @@ protected virtual void Generate([NotNull] AlterColumnOperation operation, [NotNu .Append(Code.UnknownLiteral(operation.DefaultValue)); } + if (operation.Comment != null) + { + builder + .AppendLine(",") + .Append("comment: ") + .Append(Code.Literal(operation.Comment)); + } + if (operation.OldColumn.ClrType != null) { builder.AppendLine(",") @@ -589,6 +605,14 @@ protected virtual void Generate([NotNull] AlterColumnOperation operation, [NotNu .Append(Code.UnknownLiteral(operation.OldColumn.DefaultValue)); } + if (operation.OldColumn.Comment != null) + { + builder + .AppendLine(",") + .Append("oldComment: ") + .Append(Code.Literal(operation.OldColumn.Comment)); + } + builder.Append(")"); Annotations(operation.GetAnnotations(), builder); @@ -736,6 +760,14 @@ protected virtual void Generate([NotNull] AlterTableOperation operation, [NotNul .Append(Code.Literal(operation.Schema)); } + if (operation.Comment != null) + { + builder + .AppendLine(",") + .Append("comment: ") + .Append(Code.Literal(operation.Comment)); + } + builder.Append(")"); Annotations(operation.GetAnnotations(), builder); @@ -1163,7 +1195,17 @@ protected virtual void Generate([NotNull] CreateTableOperation operation, [NotNu } } - builder.Append("})"); + builder.Append("}"); + + if (operation.Comment != null) + { + builder + .AppendLine(",") + .Append("comment: ") + .Append(Code.Literal(operation.Comment)); + } + + builder.Append(")"); Annotations(operation.GetAnnotations(), builder); } diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 18841e3d0ab..af1807b2663 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -492,6 +492,7 @@ protected virtual void GeneratePropertyAnnotations([NotNull] IProperty property, GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.DefaultValueSql, nameof(RelationalPropertyBuilderExtensions.HasDefaultValueSql), stringBuilder); GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.ComputedColumnSql, nameof(RelationalPropertyBuilderExtensions.HasComputedColumnSql), stringBuilder); GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.IsFixedLength, nameof(RelationalPropertyBuilderExtensions.IsFixedLength), stringBuilder); + GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.Comment, nameof(RelationalPropertyBuilderExtensions.HasComment), stringBuilder); GenerateFluentApiForAnnotation(ref annotations, CoreAnnotationNames.MaxLength, nameof(PropertyBuilder.HasMaxLength), stringBuilder); GenerateFluentApiForAnnotation(ref annotations, CoreAnnotationNames.Unicode, nameof(PropertyBuilder.IsUnicode), stringBuilder); @@ -758,6 +759,22 @@ protected virtual void GenerateEntityTypeAnnotations( annotations.Remove(discriminatorValueAnnotation); } + var commentAnnotation = annotations.FirstOrDefault(a => a.Name == RelationalAnnotationNames.Comment); + + if (commentAnnotation != null) + { + stringBuilder + .AppendLine() + .Append(builderName) + .Append(".") + .Append(nameof(RelationalPropertyBuilderExtensions.HasComment)) + .Append("(") + .Append(Code.UnknownLiteral(commentAnnotation.Value)) + .AppendLine(");"); + + annotations.Remove(commentAnnotation); + } + IgnoreAnnotations( annotations, CoreAnnotationNames.NavigationCandidates, diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index dc1733f6114..42e640e5d3e 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -462,5 +462,78 @@ public static bool CanSetCheckConstraint( || (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) .Overrides(constraint.GetConfigurationSource()); } + + /// + /// Configures a comment to be applied to the table + /// + /// The builder for the entity type being configured. + /// The comment for the table. + /// A builder to further configure the entity type. + public static EntityTypeBuilder HasComment( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string comment) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + entityTypeBuilder.Metadata.SetComment(comment); + return entityTypeBuilder; + } + + /// + /// Configures a comment to be applied to the table + /// + /// The entity type being configured. + /// The entity type builder. + /// The comment for the table. + /// A builder to further configure the entity type. + public static EntityTypeBuilder HasComment( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string comment) + where TEntity : class + => (EntityTypeBuilder)HasComment((EntityTypeBuilder)entityTypeBuilder, comment); + + /// + /// Configures a comment to be applied to the table + /// + /// The builder for the entity type being configured. + /// The comment for the table. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionEntityTypeBuilder HasComment( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + [CanBeNull] string comment, + bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + if (!entityTypeBuilder.CanSetComment(comment, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetComment(comment, fromDataAnnotation); + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether a comment can be set for this entity type + /// from the current configuration source + /// + /// The builder for the entity type being configured. + /// The comment for the table. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the configuration can be applied. + public static bool CanSetComment( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + [CanBeNull] string comment, + bool fromDataAnnotation = false) + => entityTypeBuilder.CanSetAnnotation( + RelationalAnnotationNames.Comment, + comment, + fromDataAnnotation); + } } diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 35b37e71a13..632d38a8bd6 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -250,5 +250,32 @@ public static bool RemoveCheckConstraint( /// The entity type to get the check constraints for. public static IEnumerable GetCheckConstraints([NotNull] this IEntityType entityType) => CheckConstraint.GetCheckConstraints(entityType); + + /// + /// Returns the comment for the column this property is mapped to. + /// + /// The entity type. + /// The comment for the column this property is mapped to. + public static string GetComment([NotNull] this IEntityType entityType) + => (string)entityType[RelationalAnnotationNames.Comment]; + + /// + /// Configures a comment to be applied to the column this property is mapped to. + /// + /// The entity type. + /// The comment for the column. + public static void SetComment([NotNull] this IMutableEntityType entityType, [CanBeNull] string comment) + => entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.Comment, comment); + + /// + /// Configures a comment to be applied to the column this property is mapped to. + /// + /// The entity type. + /// The comment for the column. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetComment( + [NotNull] this IConventionEntityType entityType, [CanBeNull] string comment, bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation(RelationalAnnotationNames.Comment, comment, fromDataAnnotation); + } } diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs index c7bf99d0ab8..2a9890242a3 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyBuilderExtensions.cs @@ -442,5 +442,74 @@ public static bool CanSetDefaultValue( RelationalAnnotationNames.DefaultValue, value, fromDataAnnotation); + + /// + /// Configures a comment to be applied to the column + /// + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasComment( + [NotNull] this PropertyBuilder propertyBuilder, + [CanBeNull] string comment) + { + Check.NotNull(propertyBuilder, nameof(propertyBuilder)); + + propertyBuilder.Metadata.SetComment(comment); + + return propertyBuilder; + } + + /// + /// Configures a comment to be applied to the column + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasComment( + [NotNull] this PropertyBuilder propertyBuilder, + [CanBeNull] string comment) + => (PropertyBuilder)HasComment((PropertyBuilder)propertyBuilder, comment); + + /// + /// Configures a comment to be applied to the column + /// + /// The builder for the property being configured. + /// The comment for the column. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionPropertyBuilder HasComment( + [NotNull] this IConventionPropertyBuilder propertyBuilder, + [CanBeNull] string comment, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetComment(comment, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetComment(comment, fromDataAnnotation); + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the given value can be set as comment for the column. + /// + /// The builder for the property being configured. + /// The comment for the column. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the given value can be set as default for the column. + public static bool CanSetComment( + [NotNull] this IConventionPropertyBuilder propertyBuilder, + [CanBeNull] object comment, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.Comment, + comment, + fromDataAnnotation); } } diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 4df1afc4c5e..dd8e6b7200c 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -470,5 +470,46 @@ public static IProperty FindSharedTableRootPrimaryKeyProperty([NotNull] this IPr return principalProperty == property ? null : principalProperty; } + + /// + /// Returns the comment for the column this property is mapped to. + /// + /// The property. + /// The comment for the column this property is mapped to. + public static string GetComment([NotNull] this IProperty property) + { + var value = (string)property[RelationalAnnotationNames.Comment]; + if (value != null) + { + return value; + } + + var sharedTablePrincipalPrimaryKeyProperty = property.FindSharedTableRootPrimaryKeyProperty(); + if (sharedTablePrincipalPrimaryKeyProperty != null) + { + return GetComment(sharedTablePrincipalPrimaryKeyProperty); + } + + return null; + } + + /// + /// Configures a comment to be applied to the column this property is mapped to. + /// + /// The property. + /// The comment for the column. + public static void SetComment([NotNull] this IMutableProperty property, [CanBeNull] string comment) + => property.SetOrRemoveAnnotation(RelationalAnnotationNames.Comment, comment); + + /// + /// Configures a comment to be applied to the column this property is mapped to. + /// + /// The property. + /// The comment for the column. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetComment( + [NotNull] this IConventionProperty property, [CanBeNull] string comment, bool fromDataAnnotation = false) + => property.SetOrRemoveAnnotation(RelationalAnnotationNames.Comment, comment, fromDataAnnotation); + } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 79722a64705..47a9768bd18 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -249,6 +249,19 @@ protected virtual void ValidateSharedTableCompatibility( otherKey.Properties.Format())); } + var currentComment = entityType.GetComment() ?? ""; + var previousComment = nextEntityType.GetComment() ?? ""; + if (!currentComment.Equals(previousComment, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + RelationalStrings.IncompatibleTableCommentMismatch( + tableName, + entityType.DisplayName(), + nextEntityType.DisplayName(), + previousComment, + currentComment)); + } + typesToValidate.Enqueue(nextEntityType); } @@ -415,6 +428,22 @@ protected virtual void ValidateSharedColumnsCompatibility( previousDefaultValueSql, currentDefaultValueSql)); } + + var currentComment = property.GetComment() ?? ""; + var previousComment = duplicateProperty.GetComment() ?? ""; + if (!currentComment.Equals(previousComment, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + RelationalStrings.DuplicateColumnNameCommentMismatch( + duplicateProperty.DeclaringEntityType.DisplayName(), + duplicateProperty.Name, + property.DeclaringEntityType.DisplayName(), + property.Name, + columnName, + tableName, + previousComment, + currentComment)); + } } if ((missingConcurrencyTokens?.Count ?? 0) != 0) diff --git a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs index 5c096e0fbfa..11f4ab75485 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMapping.cs @@ -194,5 +194,15 @@ public static TableMapping GetTableMapping([NotNull] IModel model, [NotNull] str ? new TableMapping(schema, table, mappedEntities) : null; } + + /// + /// 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. + /// + public virtual string GetComment() + => EntityTypes.Select(e => e.GetComment()).FirstOrDefault(c => c != null); + } } diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 8e8f4a17e8a..c368aab62dc 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -49,6 +49,11 @@ public static class RelationalAnnotationNames /// public const string Schema = Prefix + "Schema"; + /// + /// The name for comment annotations. + /// + public const string Comment = Prefix + "Comment"; + /// /// The name for default schema annotations. /// diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index ca8bd4bb20b..821295e3fc2 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -582,7 +582,8 @@ private IEnumerable DiffAnnotations( var alterTableOperation = new AlterTableOperation { Name = target.Name, - Schema = target.Schema + Schema = target.Schema, + Comment = target.GetComment() }; alterTableOperation.AddAnnotations(targetMigrationsAnnotations); @@ -600,12 +601,14 @@ private IEnumerable DiffAnnotations( protected virtual IEnumerable Add( [NotNull] TableMapping target, [NotNull] DiffContext diffContext) { + var entityType = target.EntityTypes[0]; var createTableOperation = new CreateTableOperation { Schema = target.Schema, - Name = target.Name + Name = target.Name, + Comment = target.GetComment() }; - createTableOperation.AddAnnotations(MigrationsAnnotations.For(target.EntityTypes[0])); + createTableOperation.AddAnnotations(MigrationsAnnotations.For(entityType)); createTableOperation.Columns.AddRange( GetSortedProperties(target).SelectMany(p => Add(p, diffContext, inline: true)).Cast()); @@ -1037,6 +1040,7 @@ private void Initialize( columnOperation.DefaultValueSql = property.GetDefaultValueSql(); columnOperation.ComputedColumnSql = property.GetComputedColumnSql(); + columnOperation.Comment = property.GetComment(); columnOperation.AddAnnotations(migrationsAnnotations); } diff --git a/src/EFCore.Relational/Migrations/MigrationBuilder.cs b/src/EFCore.Relational/Migrations/MigrationBuilder.cs index 4fec8003a11..cbba0e06c3f 100644 --- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs +++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs @@ -60,6 +60,7 @@ public MigrationBuilder([CanBeNull] string activeProvider) /// The SQL expression to use for the column's default constraint. /// The SQL expression to use to compute the column value. /// Indicates whether or not the column is constrained to fixed-length data. + /// A comment to associate with the column. /// A builder to allow annotations to be added to the operation. public virtual OperationBuilder AddColumn( [NotNull] string name, @@ -73,7 +74,8 @@ public virtual OperationBuilder AddColumn( [CanBeNull] object defaultValue = null, [CanBeNull] string defaultValueSql = null, [CanBeNull] string computedColumnSql = null, - bool? fixedLength = null) + bool? fixedLength = null, + [CanBeNull] string comment = null) { Check.NotEmpty(name, nameof(name)); Check.NotEmpty(table, nameof(table)); @@ -92,7 +94,8 @@ public virtual OperationBuilder AddColumn( DefaultValue = defaultValue, DefaultValueSql = defaultValueSql, ComputedColumnSql = computedColumnSql, - IsFixedLength = fixedLength + IsFixedLength = fixedLength, + Comment = comment }; Operations.Add(operation); @@ -341,6 +344,8 @@ public virtual OperationBuilder AddUniqueConstrain /// /// 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. + /// The previous comment to associate with the column. /// A builder to allow annotations to be added to the operation. public virtual AlterOperationBuilder AlterColumn( [NotNull] string name, @@ -364,7 +369,9 @@ public virtual AlterOperationBuilder AlterColumn( [CanBeNull] string oldDefaultValueSql = null, [CanBeNull] string oldComputedColumnSql = null, bool? fixedLength = null, - bool? oldFixedLength = null) + bool? oldFixedLength = null, + [CanBeNull] string comment = null, + [CanBeNull] string oldComment = null) { Check.NotEmpty(name, nameof(name)); Check.NotEmpty(table, nameof(table)); @@ -384,6 +391,7 @@ public virtual AlterOperationBuilder AlterColumn( DefaultValueSql = defaultValueSql, ComputedColumnSql = computedColumnSql, IsFixedLength = fixedLength, + Comment = comment, OldColumn = new ColumnOperation { ClrType = oldClrType ?? typeof(T), @@ -395,7 +403,8 @@ public virtual AlterOperationBuilder AlterColumn( DefaultValue = oldDefaultValue, DefaultValueSql = oldDefaultValueSql, ComputedColumnSql = oldComputedColumnSql, - IsFixedLength = oldFixedLength + IsFixedLength = oldFixedLength, + Comment = oldComment } }; @@ -470,17 +479,20 @@ public virtual AlterOperationBuilder AlterSequence( /// /// The table name. /// The schema that contains the table, or null to use the default schema. + /// A comment to associate with the table. /// A builder to allow annotations to be added to the operation. public virtual AlterOperationBuilder AlterTable( [NotNull] string name, - [CanBeNull] string schema = null) + [CanBeNull] string schema = null, + [CanBeNull] string comment = null) { Check.NotEmpty(name, nameof(name)); var operation = new AlterTableOperation { Schema = schema, - Name = name + Name = name, + Comment = comment }; Operations.Add(operation); @@ -667,12 +679,14 @@ public virtual OperationBuilder CreateCheckConst /// /// A delegate allowing constraints to be applied over the columns configured by the 'columns' delegate above. /// + /// A comment to been applied to the table. /// A to allow further configuration to be chained. public virtual CreateTableBuilder CreateTable( [NotNull] string name, [NotNull] Func columns, [CanBeNull] string schema = null, - [CanBeNull] Action> constraints = null) + [CanBeNull] Action> constraints = null, + [CanBeNull] string comment = null) { Check.NotEmpty(name, nameof(name)); Check.NotNull(columns, nameof(columns)); @@ -680,7 +694,8 @@ public virtual CreateTableBuilder CreateTable( var createTableOperation = new CreateTableOperation { Schema = schema, - Name = name + Name = name, + Comment = comment }; var columnsBuilder = new ColumnsBuilder(createTableOperation); diff --git a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs index 59d561c6509..95da47d5133 100644 --- a/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs +++ b/src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs @@ -190,6 +190,13 @@ protected virtual void Generate( ColumnDefinition(operation, model, builder); + if (operation.Comment != null) + { + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + GenerateComment(operation, model, builder, operation.Comment, operation.Schema, operation.Table, operation.Name); + } + if (terminate) { builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); @@ -626,6 +633,11 @@ protected virtual void Generate( builder.Append(")"); + if (operation.Comment != null) + { + GenerateComment(operation, model, builder, operation.Comment, operation.Schema, operation.Name); + } + if (terminate) { builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); @@ -1765,6 +1777,35 @@ protected virtual void EndStatement( builder.EndCommand(suppressTransaction); } + /// + /// + /// Can be overridden by database providers to build commands for applying comments to tables and columns + /// by making calls on the given . + /// + /// + /// Note that the default implementation of this method does nothing because there is no common metadata + /// relating to this operation. Providers only need to override this method if they have some provider-specific + /// annotations that must be handled. + /// + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// The comment to be applied. + /// The schema of the table. + /// The name of the table. + /// The column name if comment is being applied to a column. + protected virtual void GenerateComment( + [NotNull] MigrationOperation operation, + [CanBeNull] IModel model, + [NotNull] MigrationCommandListBuilder builder, + [NotNull] string comment, + [NotNull] string schema, + [NotNull] string table, + [CanBeNull] string columnName = null) + { + } + /// /// Concatenates the given column names into a /// separated list. diff --git a/src/EFCore.Relational/Migrations/Operations/AlterTableOperation.cs b/src/EFCore.Relational/Migrations/Operations/AlterTableOperation.cs index 38c2b6ac2dc..53e90754d91 100644 --- a/src/EFCore.Relational/Migrations/Operations/AlterTableOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/AlterTableOperation.cs @@ -22,6 +22,11 @@ public class AlterTableOperation : MigrationOperation, IAlterMigrationOperation /// public virtual string Schema { get; [param: CanBeNull] set; } + /// + /// Comment for this table + /// + public virtual string Comment { get; [param: CanBeNull] set; } + /// /// An operation representing the table as it was before being altered. /// diff --git a/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs b/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs index 7633c5aa978..67e55b1cf0d 100644 --- a/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/ColumnOperation.cs @@ -67,5 +67,10 @@ public class ColumnOperation : MigrationOperation /// is not computed. /// public virtual string ComputedColumnSql { get; [param: CanBeNull] set; } + + /// + /// Comment for this column + /// + public virtual string Comment { get; [param: CanBeNull] set; } } } diff --git a/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs b/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs index 0b17f09789a..dd7f917b4bc 100644 --- a/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs +++ b/src/EFCore.Relational/Migrations/Operations/CreateTableOperation.cs @@ -21,6 +21,11 @@ public class CreateTableOperation : MigrationOperation /// public virtual string Schema { get; [param: CanBeNull] set; } + /// + /// Comment for this table + /// + public virtual string Comment { get; [param: CanBeNull] set; } + /// /// The representing the creation of the primary key for the table. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index c7f86467f2a..416044a867f 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -179,6 +179,14 @@ public static string IncompatibleTableKeyNameMismatch([CanBeNull] object table, GetString("IncompatibleTableKeyNameMismatch", nameof(table), nameof(entityType), nameof(otherEntityType), nameof(keyName), nameof(primaryKey), nameof(otherName), nameof(otherPrimaryKey)), table, entityType, otherEntityType, keyName, primaryKey, otherName, otherPrimaryKey); + /// + /// Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'. + /// + public static string IncompatibleTableCommentMismatch([CanBeNull] object table, [CanBeNull] object entityType, [CanBeNull] object otherEntityType, [CanBeNull] object comment, [CanBeNull] object otherComment) + => string.Format( + GetString("IncompatibleTableCommentMismatch", nameof(table), nameof(entityType), nameof(otherEntityType), nameof(comment), nameof(otherComment)), + table, entityType, otherEntityType, comment, otherComment); + /// /// Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and there is no relationship between their primary keys. /// @@ -313,6 +321,14 @@ public static string DuplicateColumnNameDefaultSqlMismatch([CanBeNull] object en GetString("DuplicateColumnNameDefaultSqlMismatch", 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 comments ('{comment1}' and '{comment2}'). + /// + public static string DuplicateColumnNameCommentMismatch([CanBeNull] object entityType1, [CanBeNull] object property1, [CanBeNull] object entityType2, [CanBeNull] object property2, [CanBeNull] object columnName, [CanBeNull] object table, [CanBeNull] object comment1, [CanBeNull] object comment2) + => string.Format( + GetString("DuplicateColumnNameCommentMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(comment1), nameof(comment2)), + entityType1, property1, entityType2, property2, columnName, table, comment1, comment2); + /// /// {conflictingConfiguration} cannot be set for '{property}' at the same time as {existingConfiguration}. Remove one of these values. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 16cf0de54c2..a241725554e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -278,6 +278,9 @@ Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the name '{keyName}' of the primary key {primaryKey} does not match the name '{otherName}' of the primary key {otherPrimaryKey}. + + Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'. + Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and there is no relationship between their primary keys. @@ -349,6 +352,9 @@ '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different default values ('{value1}' and '{value2}'). + + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different comments ('{comment1}' and '{comment2}'). + {conflictingConfiguration} cannot be set for '{property}' at the same time as {existingConfiguration}. Remove one of these values. diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 4291c0e25f5..06c154aaeab 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -338,6 +338,18 @@ protected override void Generate( .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); } + if (operation.OldColumn.Comment != operation.Comment) + { + if (operation.OldColumn.Comment != null) + { + EndStatement(builder); + + GenerateDropExtendedProperty(builder, model, "Comment", operation.Schema, operation.Table, operation.Name); + } + + GenerateComment(operation, model, builder, operation.Comment, operation.Schema, operation.Table, operation.Name); + } + if (narrowed) { CreateIndexes(indexesToRebuild, builder); @@ -1616,6 +1628,123 @@ protected virtual void CreateIndexes( } } + /// + /// Generates SQL to create comment extended properties on table and columns. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// The comment to be applied. + /// The schema of the table. + /// The name of the table. + /// The column name if comment is being applied to a column. + protected override void GenerateComment( + MigrationOperation operation, + IModel model, + MigrationCommandListBuilder builder, + string comment, + string schema, + string table, + string columnName = null) + { + Check.NotNull(operation, nameof(operation)); + Check.NotNull(builder, nameof(builder)); + Check.NotNull(table, nameof(table)); + + if (comment != null) + { + EndStatement(builder); + + GenerateCreateExtendedProperty(builder, model, "Comment", comment, schema, table, columnName); + } + + } + + /// + /// Generates SQL to create a extended property on table and columns. + /// + /// The command builder to use to build the commands. + /// The target model which may be null if the operations exist without a model. + /// The name of the extended property. + /// The value of the extended property. + /// The schema of the table. + /// The name of the table. + /// The column name if comment is being applied to a column. + protected virtual void GenerateCreateExtendedProperty( + [NotNull] MigrationCommandListBuilder builder, + [CanBeNull] IModel model, + [NotNull] string name, + [NotNull] string value, + [CanBeNull] string schema, + [NotNull] string table, + [CanBeNull] string columnName = null) + { + Check.NotNull(builder, nameof(builder)); + Check.NotNull(name, nameof(name)); + Check.NotNull(value, nameof(value)); + Check.NotNull(table, nameof(table)); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + builder + .Append("EXEC sp_addextendedproperty @name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(name)) + .Append(", @value = ") + .Append(stringTypeMapping.GenerateSqlLiteral(value)) + .Append(", @level0type = N'Schema', @level0name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(schema ?? model?.GetDefaultSchema())) + .Append(", @level1type = N'Table', @level1name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(table)); + + if (columnName != null) + { + builder + .Append(", @level2type = N'Column', @level2name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(columnName)); + } + + } + + /// + /// Generates SQL to drop a extended property on table and columns. + /// + /// The command builder to use to build the commands. + /// The target model which may be null if the operations exist without a model. + /// The name of the extended property. + /// The schema of the table. + /// The name of the table. + /// The column name if comment is being applied to a column. + protected virtual void GenerateDropExtendedProperty( + [NotNull] MigrationCommandListBuilder builder, + [CanBeNull] IModel model, + [NotNull] string name, + [CanBeNull] string schema, + [NotNull] string table, + [CanBeNull] string columnName = null) + { + Check.NotNull(builder, nameof(builder)); + Check.NotNull(name, nameof(name)); + Check.NotNull(table, nameof(table)); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + builder + .Append("EXEC sp_dropextendedproperty @name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(name)) + .Append(", @level0type = N'Schema', @level0name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(schema ?? model?.GetDefaultSchema())) + .Append(", @level1type = N'Table', @level1name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(table)); + + if (columnName != null) + { + builder + .Append(", @level2type = N'Column', @level2name = ") + .Append(stringTypeMapping.GenerateSqlLiteral(columnName)); + } + + } + /// /// Checks whether or not should have a filter generated for it by /// Migrations. diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs index c14728577f7..e75420b28e2 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationOperationGeneratorTest.cs @@ -94,7 +94,8 @@ public void AddColumnOperation_all_args() IsRowVersion = true, IsNullable = true, DefaultValue = 1, - IsFixedLength = true + IsFixedLength = true, + Comment = "My Comment" }, "mb.AddColumn(" + _eol + " name: \"Id\"," + _eol + @@ -106,7 +107,8 @@ public void AddColumnOperation_all_args() " maxLength: 30," + _eol + " rowVersion: true," + _eol + " nullable: true," + _eol + - " defaultValue: 1);", + " defaultValue: 1," + _eol + + " comment: \"My Comment\");", o => { Assert.Equal("Id", o.Name); @@ -118,6 +120,7 @@ public void AddColumnOperation_all_args() Assert.Equal(1, o.DefaultValue); Assert.False(o.IsUnicode); Assert.True(o.IsFixedLength); + Assert.Equal("My Comment", o.Comment); }); } @@ -508,6 +511,7 @@ public void AlterColumnOperation_all_args() IsNullable = true, DefaultValue = 1, IsFixedLength = true, + Comment = "My Comment 2", OldColumn = { ClrType = typeof(string), @@ -517,7 +521,8 @@ public void AlterColumnOperation_all_args() IsRowVersion = true, IsNullable = true, DefaultValue = 0, - IsFixedLength = true + IsFixedLength = true, + Comment = "My Comment", } }, "mb.AlterColumn(" + _eol + @@ -531,6 +536,7 @@ public void AlterColumnOperation_all_args() " rowVersion: true," + _eol + " nullable: true," + _eol + " defaultValue: 1," + _eol + + " comment: \"My Comment 2\"," + _eol + " oldClrType: typeof(string)," + _eol + " oldType: \"string\"," + _eol + " oldUnicode: false," + _eol + @@ -538,7 +544,8 @@ public void AlterColumnOperation_all_args() " oldMaxLength: 20," + _eol + " oldRowVersion: true," + _eol + " oldNullable: true," + _eol + - " oldDefaultValue: 0);", + " oldDefaultValue: 0," + _eol + + " oldComment: \"My Comment\");", o => { Assert.Equal("Id", o.Name); @@ -554,6 +561,7 @@ public void AlterColumnOperation_all_args() Assert.Equal(1, o.DefaultValue); Assert.Null(o.DefaultValueSql); Assert.Null(o.ComputedColumnSql); + Assert.Equal("My Comment 2", o.Comment); Assert.Equal(typeof(string), o.OldColumn.ClrType); Assert.Equal("string", o.OldColumn.ColumnType); Assert.False(o.OldColumn.IsUnicode); @@ -564,6 +572,7 @@ public void AlterColumnOperation_all_args() Assert.Equal(0, o.OldColumn.DefaultValue); Assert.Null(o.OldColumn.DefaultValueSql); Assert.Null(o.OldColumn.ComputedColumnSql); + Assert.Equal("My Comment", o.OldColumn.Comment); }); } @@ -747,21 +756,40 @@ public void AlterSequenceOperation_all_args() } [ConditionalFact] - public void AlterTableOperation() + public void AlterTableOperation_required_args() + { + Test( + new AlterTableOperation + { + Name = "Customer" + }, + "mb.AlterTable(" + _eol + + " name: \"Customer\");", + o => + { + Assert.Equal("Customer", o.Name); + }); + } + + [ConditionalFact] + public void AlterTableOperation_all_args() { Test( new AlterTableOperation { Name = "Customer", - Schema = "dbo" + Schema = "dbo", + Comment = "My Comment" }, "mb.AlterTable(" + _eol + " name: \"Customer\"," + _eol + - " schema: \"dbo\");", + " schema: \"dbo\"," + _eol + + " comment: \"My Comment\");", o => { Assert.Equal("Customer", o.Name); Assert.Equal("dbo", o.Schema); + Assert.Equal("My Comment", o.Comment); }); } @@ -1694,6 +1722,41 @@ public void CreateTableOperation_ChecksConstraints_all_args() }); } + [ConditionalFact] + public void CreateTableOperation_Comment() + { + Test( + new CreateTableOperation + { + Name = "Post", + Schema = "dbo", + Columns = + { + new AddColumnOperation + { + Name = "AltId1", + ClrType = typeof(int) + } + }, + Comment = "My Comment" + }, + "mb.CreateTable(" + _eol + + " name: \"Post\"," + _eol + + " schema: \"dbo\"," + _eol + + " columns: table => new" + _eol + + " {" + _eol + + " AltId1 = table.Column(nullable: false)" + _eol + + " }," + _eol + + " constraints: table =>" + _eol + + " {" + _eol + + " }," + _eol + + " comment: \"My Comment\");", + o => + { + Assert.Equal("My Comment", o.Comment); + }); + } + [ConditionalFact] public void DropColumnOperation_required_args() { diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 028c5dca5f6..c27c79ae99c 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -100,7 +100,13 @@ public void Test_new_annotations_handled_for_entity_types() ("MyDiscriminatorValue", _toTable + _nl + "modelBuilder.HasDiscriminator" + "()." + nameof(DiscriminatorBuilder.HasValue) + @"(""MyDiscriminatorValue"");" + _nl) - } + }, + { + RelationalAnnotationNames.Comment, + ("My Comment", + _toTable + _nl + "modelBuilder.HasComment" + + @"(""My Comment"");" + _nl) + }, }; MissingAnnotationCheck( @@ -189,6 +195,10 @@ public void Test_new_annotations_handled_for_properties() { RelationalAnnotationNames.IsFixedLength, (true, _nl + "." + nameof(RelationalPropertyBuilderExtensions.IsFixedLength) + "(true)") + }, + { + RelationalAnnotationNames.Comment, + ("My Comment", _nl + "." + nameof(RelationalPropertyBuilderExtensions.HasComment) + @"(""My Comment"")") } }; diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 7f31e08c5c1..2fcca0ece39 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -186,6 +186,21 @@ public virtual void Detects_incompatible_primary_keys_with_shared_table() modelBuilder.Model); } + [ConditionalFact] + public virtual void Detects_incompatible_comments_with_shared_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne().IsRequired().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id); + modelBuilder.Entity().ToTable("Table").HasComment("My comment"); + modelBuilder.Entity().ToTable("Table"); + + VerifyError( + RelationalStrings.IncompatibleTableCommentMismatch( + "Table", nameof(A), nameof(B), "", "My comment"), + modelBuilder.Model); + } + [ConditionalFact] public virtual void Detects_incompatible_primary_key_columns_with_shared_table() { @@ -387,6 +402,20 @@ public virtual void Detects_duplicate_column_names_within_hierarchy_with_differe modelBuilder.Model); } + [ConditionalFact] + public virtual void Detects_duplicate_column_names_within_hierarchy_with_different_comments() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity(); + modelBuilder.Entity().Property(c => c.Breed).HasColumnName("Breed").HasComment("My comment"); + modelBuilder.Entity().Property(c => c.Breed).HasColumnName("Breed"); + + VerifyError( + RelationalStrings.DuplicateColumnNameCommentMismatch( + nameof(Cat), nameof(Cat.Breed), nameof(Dog), nameof(Dog.Breed), nameof(Cat.Breed), nameof(Animal), "My comment", ""), + modelBuilder.Model); + } + [ConditionalFact] public virtual void Passes_for_compatible_duplicate_column_names_within_hierarchy() { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs index 844a4bd9883..4bb07599ebc 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs @@ -194,6 +194,22 @@ public void Can_access_check_constraint() Assert.Equal("s < p", entityType.GetCheckConstraints().Single().Sql); } + [ConditionalFact] + public void Can_access_comment() + { + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); + var entityType = typeBuilder.Metadata; + + Assert.NotNull(typeBuilder.HasComment("My Comment")); + Assert.Equal("My Comment", entityType.GetComment()); + + Assert.NotNull(typeBuilder.HasComment("My Comment 2", fromDataAnnotation: true)); + Assert.Equal("My Comment 2", entityType.GetComment()); + + Assert.Null(typeBuilder.HasComment("My Comment")); + Assert.Equal("My Comment 2", entityType.GetComment()); + } + private class Splot { public static readonly PropertyInfo SplowedProperty = typeof(Splot).GetProperty("Splowed"); diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerMigrationSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerMigrationSqlGeneratorTest.cs index 375f3eec427..070f83269d5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerMigrationSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerMigrationSqlGeneratorTest.cs @@ -223,6 +223,25 @@ public virtual void AddColumnOperation_with_rowversion_no_model() Sql); } + [ConditionalFact] + public virtual void AddColumnOperation_with_comment() + { + Generate( + new AddColumnOperation + { + Table = "People", + Name = "FullName", + ClrType = typeof(string), + Comment = "My comment" + }); + + Assert.Equal( + "ALTER TABLE [People] ADD [FullName] nvarchar(max) NOT NULL;" + EOL + + "GO" + EOL + EOL + + "EXEC sp_addextendedproperty @name = N'Comment', @value = N'My comment', @level0type = N'Schema', @level0name = NULL, @level1type = N'Table', @level1name = N'People', @level2type = N'Column', @level2name = N'FullName';" + EOL, + Sql); + } + [ConditionalFact] public virtual void AddPrimaryKeyOperation_nonclustered() { @@ -808,6 +827,118 @@ public virtual void AlterColumnOperation_remove_identity() Assert.Equal(SqlServerStrings.AlterIdentityColumn, ex.Message); } + [ConditionalFact] + public void AlterColumnOperation_with_new_comment() + { + Generate( + new AlterColumnOperation + { + Table = "People", + Schema = "dbo", + Name = "LuckyNumber", + ClrType = typeof(int), + ColumnType = "int", + IsNullable = false, + Comment = "My Comment" + }); + + Assert.Equal( + "DECLARE @var0 sysname;" + EOL + + "SELECT @var0 = [d].[name]" + EOL + + "FROM [sys].[default_constraints] [d]" + EOL + + "INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]" + + EOL + + "WHERE ([d].[parent_object_id] = OBJECT_ID(N'[dbo].[People]') AND [c].[name] = N'LuckyNumber');" + EOL + + "IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [dbo].[People] DROP CONSTRAINT [' + @var0 + '];');" + EOL + + "ALTER TABLE [dbo].[People] ALTER COLUMN [LuckyNumber] int NOT NULL;" + EOL + + "GO" + EOL + EOL + + "EXEC sp_addextendedproperty @name = N'Comment', @value = N'My Comment', @level0type = N'Schema', @level0name = N'dbo', @level1type = N'Table', @level1name = N'People', @level2type = N'Column', @level2name = N'LuckyNumber'", + Sql); + } + + [ConditionalFact] + public void AlterColumnOperation_with_different_comment_to_existing() + { + Generate( + modelBuilder => modelBuilder + .HasAnnotation(CoreAnnotationNames.ProductVersion, "1.1.0") + .Entity( + "Person", x => + { + x.Property("Name").HasComment("My Comment"); + }), + new AlterColumnOperation + { + Table = "People", + Schema = "dbo", + Name = "Name", + ClrType = typeof(string), + IsNullable = false, + Comment = "My Comment 2", + OldColumn = new ColumnOperation + { + ClrType = typeof(string), + IsNullable = true, + Comment = "My Comment" + } + }); + + Assert.Equal( + "DECLARE @var0 sysname;" + EOL + + "SELECT @var0 = [d].[name]" + EOL + + "FROM [sys].[default_constraints] [d]" + EOL + + "INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]" + + EOL + + "WHERE ([d].[parent_object_id] = OBJECT_ID(N'[dbo].[People]') AND [c].[name] = N'Name');" + EOL + + "IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [dbo].[People] DROP CONSTRAINT [' + @var0 + '];');" + EOL + + "ALTER TABLE [dbo].[People] ALTER COLUMN [Name] nvarchar(max) NOT NULL;" + EOL + + "GO" + EOL + EOL + + "EXEC sp_dropextendedproperty @name = N'Comment', @level0type = N'Schema', @level0name = N'dbo', @level1type = N'Table', @level1name = N'People', @level2type = N'Column', @level2name = N'Name'" + + "GO" + EOL + EOL + + "EXEC sp_addextendedproperty @name = N'Comment', @value = N'My Comment 2', @level0type = N'Schema', @level0name = N'dbo', @level1type = N'Table', @level1name = N'People', @level2type = N'Column', @level2name = N'Name'", + Sql); + } + + [ConditionalFact] + public void AlterColumnOperation_by_removing_comment() + { + Generate( + modelBuilder => modelBuilder + .HasAnnotation(CoreAnnotationNames.ProductVersion, "1.1.0") + .Entity( + "Person", x => + { + x.Property("Name").HasComment("My Comment"); + }), + new AlterColumnOperation + { + Table = "People", + Schema = "dbo", + Name = "Name", + ClrType = typeof(string), + IsNullable = false, + OldColumn = new ColumnOperation + { + ClrType = typeof(string), + IsNullable = true, + Comment = "My Comment" + } + }); + + Assert.Equal( + "DECLARE @var0 sysname;" + EOL + + "SELECT @var0 = [d].[name]" + EOL + + "FROM [sys].[default_constraints] [d]" + EOL + + "INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]" + + EOL + + "WHERE ([d].[parent_object_id] = OBJECT_ID(N'[dbo].[People]') AND [c].[name] = N'Name');" + EOL + + "IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [dbo].[People] DROP CONSTRAINT [' + @var0 + '];');" + EOL + + "ALTER TABLE [dbo].[People] ALTER COLUMN [Name] nvarchar(max) NOT NULL;" + EOL + + "GO" + EOL + EOL + + "EXEC sp_dropextendedproperty @name = N'Comment', @level0type = N'Schema', @level0name = N'dbo', @level1type = N'Table', @level1name = N'People', @level2type = N'Column', @level2name = N'Name'", + Sql); + } + [ConditionalFact] public virtual void CreateDatabaseOperation() {