Skip to content

Commit ad662f8

Browse files
committed
Skip ALTER COLUMN for computed columns when only CLR type changes
The migration model differ produced an AlterColumnOperation when the CLR type of a property mapped to a computed column changed (e.g. int → long for a column with .HasComputedColumnSql("DATALENGTH(...)")). The generated ALTER TABLE ... ALTER COLUMN then failed at runtime with "Cannot alter column ... because it is 'COMPUTED'". A computed column's store type and collation are derived from the expression; they aren't user-configurable. CLR-type-only changes are metadata on the EF side and require no database-side change. Suppress columnTypeChanged and collationChanged when both source and target are computed with the same ComputedColumnSql and IsStored. Real changes — nullability, comment, order, annotations, and notably expression changes — still flow through the existing path; the SQL generator's drop+add path handles legitimate computed-column modifications unchanged. Fixes #33425
1 parent a267c3e commit ad662f8

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,20 @@ protected virtual IEnumerable<MigrationOperation> Diff(
10651065
var targetMigrationsAnnotations = target.GetAnnotations();
10661066

10671067
var isNullableChanged = source.IsNullable != target.IsNullable;
1068-
var columnTypeChanged = source.StoreType != target.StoreType;
1068+
1069+
// For computed columns whose expression and persistence are unchanged, the database column's
1070+
// type and collation are derived from the expression — they aren't user-configurable. CLR-type
1071+
// changes that recompute StoreType/Collation cannot (and need not) be applied via ALTER COLUMN;
1072+
// SQL Server rejects with "Cannot alter column ... because it is COMPUTED". When the expression
1073+
// itself changes, the SQL generator's existing drop+add path handles it. See #33425.
1074+
var isComputedWithUnchangedExpression =
1075+
source.ComputedColumnSql != null
1076+
&& target.ComputedColumnSql != null
1077+
&& MultilineEquals(source.ComputedColumnSql, target.ComputedColumnSql)
1078+
&& source.IsStored == target.IsStored;
1079+
1080+
var columnTypeChanged = !isComputedWithUnchangedExpression && source.StoreType != target.StoreType;
1081+
var collationChanged = !isComputedWithUnchangedExpression && source.Collation != target.Collation;
10691082

10701083
if (!source.TryGetDefaultValue(out var sourceDefault))
10711084
{
@@ -1085,7 +1098,7 @@ protected virtual IEnumerable<MigrationOperation> Diff(
10851098
|| sourceDefault?.GetType() != targetDefault?.GetType()
10861099
|| (sourceDefault != DBNull.Value && !target.ProviderValueComparer.Equals(sourceDefault, targetDefault))
10871100
|| !MultilineEquals(source.Comment, target.Comment)
1088-
|| source.Collation != target.Collation
1101+
|| collationChanged
10891102
|| source.Order != target.Order
10901103
|| HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations))
10911104
{

test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3025,6 +3025,66 @@ public void Alter_column_computed_expression()
30253025
Assert.Equal("CreateCatamountName()", operation.ComputedColumnSql);
30263026
});
30273027

3028+
[ConditionalFact]
3029+
public void Computed_column_clr_type_change_alone_is_no_op()
3030+
=> Execute(
3031+
source => source.Entity(
3032+
"FileData",
3033+
x =>
3034+
{
3035+
x.ToTable("Files", "dbo");
3036+
x.Property<int>("Id");
3037+
x.Property<byte[]>("FileContents").IsRequired();
3038+
// Initial: int CLR type → store type "int"
3039+
x.Property<int>("FileSize").HasComputedColumnSql("DATALENGTH([FileContents])");
3040+
}),
3041+
target => target.Entity(
3042+
"FileData",
3043+
x =>
3044+
{
3045+
x.ToTable("Files", "dbo");
3046+
x.Property<int>("Id");
3047+
x.Property<byte[]>("FileContents").IsRequired();
3048+
// Changed: long CLR type → store type "bigint" (DATALENGTH actually returns bigint).
3049+
// Computed column's store type is derived from the expression, not configurable;
3050+
// the CLR-type-only change must NOT produce an AlterColumnOperation since
3051+
// SQL Server rejects ALTER COLUMN on a computed column. See #33425.
3052+
x.Property<long>("FileSize").HasComputedColumnSql("DATALENGTH([FileContents])");
3053+
}),
3054+
operations => Assert.Empty(operations));
3055+
3056+
[ConditionalFact]
3057+
public void Computed_column_expression_change_with_clr_type_change_still_alters()
3058+
=> Execute(
3059+
source => source.Entity(
3060+
"FileData",
3061+
x =>
3062+
{
3063+
x.ToTable("Files", "dbo");
3064+
x.Property<int>("Id");
3065+
x.Property<byte[]>("FileContents").IsRequired();
3066+
x.Property<int>("FileSize").HasComputedColumnSql("DATALENGTH([FileContents])");
3067+
}),
3068+
target => target.Entity(
3069+
"FileData",
3070+
x =>
3071+
{
3072+
x.ToTable("Files", "dbo");
3073+
x.Property<int>("Id");
3074+
x.Property<byte[]>("FileContents").IsRequired();
3075+
// Both expression and CLR type changed — diff must still emit AlterColumnOperation
3076+
// (provider's SQL generator turns it into drop+add for computed columns).
3077+
x.Property<long>("FileSize").HasComputedColumnSql("LEN([FileContents])");
3078+
}),
3079+
operations =>
3080+
{
3081+
Assert.Equal(1, operations.Count);
3082+
var operation = Assert.IsType<AlterColumnOperation>(operations[0]);
3083+
Assert.Equal("FileSize", operation.Name);
3084+
Assert.Equal("LEN([FileContents])", operation.ComputedColumnSql);
3085+
Assert.Equal("DATALENGTH([FileContents])", operation.OldColumn.ComputedColumnSql);
3086+
});
3087+
30283088
[ConditionalFact]
30293089
public void Alter_column_comment()
30303090
=> Execute(

0 commit comments

Comments
 (0)