Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 150 additions & 34 deletions src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ protected override void Generate(

var narrowed = false;
var oldColumnSupported = IsOldColumnSupported(model);
string? oldType = null;

// SQL Server can't ALTER COLUMN on a computed column when the expression is unchanged; see #33425.
var computedColumnIsNoOp = operation.ComputedColumnSql != null
Expand All @@ -308,7 +309,7 @@ protected override void Generate(
throw new InvalidOperationException(SqlServerStrings.AlterIdentityColumn);
}

var oldType = operation.OldColumn.ColumnType
oldType = operation.OldColumn.ColumnType
?? GetColumnType(
operation.Schema,
operation.Table,
Expand Down Expand Up @@ -429,43 +430,158 @@ protected override void Generate(

if (alterStatementNeeded)
{
builder
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ALTER COLUMN ");
// SQL Server can't ALTER COLUMN from json to a character type; use rename-add-copy-drop instead. See #38364.
if ((oldType ?? operation.OldColumn.ColumnType)
?.Equals("json", StringComparison.OrdinalIgnoreCase) == true
&& !columnType.Equals("json", StringComparison.OrdinalIgnoreCase))
Comment on lines +433 to +436
{
var tempColumnName = "ef_temp_" + operation.Name;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract the logic in this block to a method to improve readability


Rename(
Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)
+ "."
+ Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name),
tempColumnName,
"COLUMN",
builder);

builder
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ADD ");

ColumnDefinition(
operation.Schema,
operation.Table,
operation.Name,
new AddColumnOperation
{
Schema = operation.Schema,
Table = operation.Table,
Name = operation.Name,
ClrType = operation.ClrType,
ColumnType = operation.ColumnType,
IsUnicode = operation.IsUnicode,
IsFixedLength = operation.IsFixedLength,
MaxLength = operation.MaxLength,
Precision = operation.Precision,
Scale = operation.Scale,
IsRowVersion = operation.IsRowVersion,
IsNullable = true,
Collation = operation.Collation
},
model,
builder);

Comment on lines +453 to +475
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);

var updateSql = new StringBuilder()
.Append("UPDATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" SET ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
.Append(" = CONVERT(")
.Append(columnType)
.Append(", ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName))
.Append(")")
.ToString();

builder
.Append("EXEC(N'")
.Append(updateSql.Replace("'", "''"))
.Append("')");

builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);

builder
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" DROP COLUMN ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName))
.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);

// NB: ComputedColumnSql, IsStored, DefaultValue, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are
// handled elsewhere. Don't copy them here.
var definitionOperation = new AlterColumnOperation
if (!operation.IsNullable)
{
builder
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ALTER COLUMN ");

// NB: ComputedColumnSql, IsStored, DefaultValue, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are
// handled elsewhere. Don't copy them here.
var definitionOperation = new AlterColumnOperation
{
Schema = operation.Schema,
Table = operation.Table,
Name = operation.Name,
ClrType = operation.ClrType,
ColumnType = operation.ColumnType,
IsUnicode = operation.IsUnicode,
IsFixedLength = operation.IsFixedLength,
MaxLength = operation.MaxLength,
Precision = operation.Precision,
Scale = operation.Scale,
IsRowVersion = operation.IsRowVersion,
IsNullable = false,
Collation = operation.Collation,
OldColumn = operation.OldColumn
};
definitionOperation.AddAnnotations(
operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.ValueGenerationStrategy
&& a.Name != SqlServerAnnotationNames.Identity));

ColumnDefinition(
operation.Schema,
operation.Table,
operation.Name,
definitionOperation,
model,
builder);

builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
}
}
else
{
Schema = operation.Schema,
Table = operation.Table,
Name = operation.Name,
ClrType = operation.ClrType,
ColumnType = operation.ColumnType,
IsUnicode = operation.IsUnicode,
IsFixedLength = operation.IsFixedLength,
MaxLength = operation.MaxLength,
Precision = operation.Precision,
Scale = operation.Scale,
IsRowVersion = operation.IsRowVersion,
IsNullable = operation.IsNullable,
Collation = operation.Collation,
OldColumn = operation.OldColumn
};
definitionOperation.AddAnnotations(
operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.ValueGenerationStrategy
&& a.Name != SqlServerAnnotationNames.Identity));
builder

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract the logic in this block and call it from the if block above

.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ALTER COLUMN ");

ColumnDefinition(
operation.Schema,
operation.Table,
operation.Name,
definitionOperation,
model,
builder);
// NB: ComputedColumnSql, IsStored, DefaultValue, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are
// handled elsewhere. Don't copy them here.
var definitionOperation = new AlterColumnOperation
{
Schema = operation.Schema,
Table = operation.Table,
Name = operation.Name,
ClrType = operation.ClrType,
ColumnType = operation.ColumnType,
IsUnicode = operation.IsUnicode,
IsFixedLength = operation.IsFixedLength,
MaxLength = operation.MaxLength,
Precision = operation.Precision,
Scale = operation.Scale,
IsRowVersion = operation.IsRowVersion,
IsNullable = operation.IsNullable,
Collation = operation.Collation,
OldColumn = operation.OldColumn
};
definitionOperation.AddAnnotations(
operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.ValueGenerationStrategy
&& a.Name != SqlServerAnnotationNames.Identity));

builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
ColumnDefinition(
operation.Schema,
operation.Table,
operation.Name,
definitionOperation,
model,
builder);

builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
}
}

if (!Equals(operation.DefaultValue, oldDefaultValue) || operation.DefaultValueSql != oldDefaultValueSql)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,59 @@ public override async Task Convert_string_column_to_a_json_column_containing_col
AssertSql();
}

[ConditionalFact(typeof(SqlServerTestEnvironment), nameof(SqlServerTestEnvironment.IsJsonTypeSupported))]
public virtual async Task Convert_json_column_back_to_string_column()
{
// Use explicit operation rather than model diffing: the snapshot round-trip of a source model
// with HasColumnType("json") does not reliably preserve identity annotations, causing the
// model differ to emit a spurious AlterColumnOperation for the PK that hits the identity check.
await Test(
builder =>
{
Comment thread
AndriySvyryd marked this conversation as resolved.
builder.Entity(
"Entity", e =>
{
e.Property<int>("Id").ValueGeneratedNever();
e.HasKey("Id");
e.Property<string>("Name").HasColumnType("json");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also declare an index on this property to make sure it's recreated correctly

});
},
new AlterColumnOperation
{
Table = "Entity",
Name = "Name",
ClrType = typeof(string),
ColumnType = "nvarchar(max)",
IsNullable = true,
OldColumn = new AddColumnOperation
{
ClrType = typeof(string),
ColumnType = "json",
IsNullable = true
}
},
model =>
{
var table = Assert.Single(model.Tables);
var column = Assert.Single(table.Columns, c => c.Name == "Name");
Assert.Equal("nvarchar(max)", column.StoreType);
Assert.True(column.IsNullable);
});

AssertSql(
"""
DECLARE @var nvarchar(max);
SELECT @var = QUOTENAME(OBJECT_NAME([c].[default_object_id]))
FROM [sys].[columns] [c]
WHERE [c].[object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Name';
IF @var IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT ' + @var + ';');
EXEC sp_rename N'[Entity].[Name]', N'ef_temp_Name', 'COLUMN';
ALTER TABLE [Entity] ADD [Name] nvarchar(max) NULL;
EXEC(N'UPDATE [Entity] SET [Name] = CONVERT(nvarchar(max), [ef_temp_Name])');
ALTER TABLE [Entity] DROP COLUMN [ef_temp_Name];
""");
}

[Fact]
public virtual async Task Alter_column_make_required_with_index_with_included_properties()
{
Expand Down
Loading