-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Value converters can lead to unexpected seed data operations in migrations #18592
Comments
Still depends on dotnet/efcore#18592 Addresses PomeloFoundation#792
@lauxjpn Thanks for reporting this, and thanks for your ongoing work on the MySQL provider. It does look like there is a bug here (@AndriySvyryd and @bricelam have fixed a few things in the snapshots recently) but using the store type in the snapshot is intentional. The reason is that snapshots persist for a long time, even while the application and its types are evolving. Consider, for example, a The reason this should work is because all migrations ultimately cares about are what changes to make to the database. It doesn't matter if the model type changes as long as the store type stays the same. That being said, this is has been a significant source of bugs, of which this is likely one. |
Just to clarify, this issue is not about the use of database store types like
I understand. This is a reciprocity problem. Let's assume the following two custom types exist: public struct MyVanillaMoneyType : System.Double
{
// [...]
}
public struct MyStrawberryMoneyType : System.Int32
{
// [...]
} We then define the following type mapping public class MyVanillaMoneyTypeMapping : MyMoneyTypeMapping
{
public MyVanillaMoneyTypeMapping(
[NotNull] string storeType,
Type clrType,
ValueConverter converter = null,
ValueComparer comparer = null,
int? precision = null)
: this(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(
clrType, // CLR type
converter,
comparer),
storeType, // databse store type
precision == null
? StoreTypePostfix.None
: StoreTypePostfix.Precision,
System.Data.DbType.Double, // DbType
precision: precision))
{
}
// [...]
} Here For our example, we instantiate two different objects of this type mapping in our private readonly RelationalTypeMapping _myVanillaMoneyTypeMapping
= new MyVanillaMoneyTypeMapping(
"mymoney", // database store type
typeof(MyVanillaMoneyType)); // CLR type;
private readonly RelationalTypeMapping _myStrawberryMoneyTypeMapping
= new MyVanillaMoneyTypeMapping(
"mymoney", // database store type
typeof(MyStrawberryMoneyType), // CLR type
new StrawberryToVanillaConverter(), // value converter
new StrawberryComparer()); // comparer We have now mapped both CLR types, When we run this later, the value converter of StrawberryToVanillaConverter.ModelClrType = typeof(MyStrawberryMoneyType);
StrawberryToVanillaConverter.ProviderClrType = typeof(MyVanillaMoneyType); Let's define our model: public class MyIceCreamWallet
{
public MyVanillaMoneyType VanillaMoneyProperty { get; set; }
public MyStrawberryMoneyType StrawberryMoneyProperty { get; set; }
} When we generate the initial migration, we get the following snapshot code: protected override void BuildModel(ModelBuilder modelBuilder)
{
modelBuilder.Entity("MyNamespace.MyIceCreamWallet", b =>
{
b.Property<MyVanillaMoneyType>("VanillaMoneyProperty")
.HasColumnType("mymoney");
/* This should be MyStrawberryMoneyType NOT MyVanillaMoneyType */
b.Property<MyVanillaMoneyType>("StrawberryMoneyProperty")
.HasColumnType("mymoney");
b.ToTable("MyModelEntities");
});
} The line This is not the case, because the C# generator emits Hmm... always dangerous to use food related for examples. Guess I go buy some ice cream now =) |
The migrations model differ calculates what needs to be changed in the database to reflect the changes between the snapshot and the current model. It does not care about ModelClrType, only the ProviderClrType is needed. In fact the snapshot doesn't have access to custom value converters, so if it only had the ModelClrType it would not be able to get the ProviderClrType. |
It seems that this leads to wrong calls in the To make this concrete again for the original problem with: [Required, Timestamp]
public byte[] RowVersion { get; set; } The following script is generated every time a migration (even without change) is added: public partial class NoChange : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "TestEntities",
keyColumn: "Id",
keyValue: 1);
migrationBuilder.InsertData(
table: "TestEntities",
column: "Id",
value: 1);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "TestEntities",
keyColumn: "Id",
keyValue: 1);
migrationBuilder.InsertData(
table: "TestEntities",
column: "Id",
value: 1);
}
} The following code is responsible for that: For the model property This ultimately leads to the following line being Which results in the seed data being recreated: |
@lauxjpn Yes, there are certainly issues in the seed data diffing code.
|
@AndriySvyryd While I look at it a bit closer, maybe we can make it work for now for the Pomelo provider by tweaking our BytesToDateTimeConverter implementation to handle the |
@AndriySvyryd Sorry, I forgot about the After that is fixed, our |
ValueConverter.ModelClrType
instead of ValueConverter.ProviderClrType
in CSharpSnapshotGenerator.GenerateProperty()
Use the default value for non-nullable properties. Fixes #18592
Then there is still this issue left:
And if we drop the [Timestamp]
public byte[] RowVersion { get; set; } We suddenly get the following script generated on every migration: public partial class NoChange : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "RowVersion",
table: "TestEntities",
rowVersion: true,
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp(6)",
oldNullable: true)
.Annotation("MySql:ValueGenerationStrategy",
MySqlValueGenerationStrategy.ComputedColumn);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "RowVersion",
table: "TestEntities",
type: "timestamp(6)",
nullable: true,
oldClrType: typeof(DateTime),
oldRowVersion: true,
oldNullable: true)
.OldAnnotation("MySql:ValueGenerationStrategy",
MySqlValueGenerationStrategy.ComputedColumn);
}
} That should also be due to the fact, that the source property is not nullable, but the target property is. The differ then assumes this is a change and creates the alter column operation. I can open separate issues for them, if you want me to. |
Use the default value for non-nullable properties. Mark the column as non-nullable if the converted provider type is non-nullable. Fixes #18592
I've added that fix to the PR. |
@AndriySvyryd Awesome, thanks! |
On a second look we can't yet mark them as non-nullable before #13850 is fixed as we can still send |
Use the default value for non-nullable properties. Fixes #18592 asfasf
* Add support to reverse engineer views. Add comments for tables and columns. Improve handling of default values. * Handle the `CURRENT_TIMESTAMP` default value for `timestamp` columns correctly. Fixes #703 * Correctly implement `CURRENT_TIMESTAMP` with `ON UPDATE` clauses. Introduce a workaround for the missing EF Core handling of `ValueGenerated.OnUpdate`. Fixes #877 * Remove unnecessary code Can probably remove this code as it only applies to Release Candidate not General Availability versions. https://bugs.mysql.com/bug.php?id=89793 >The NON_UNIQUE column in the INFORMATION_SCHEMA.STATISTICS table had type BIGINT prior to MySQL 8.0, but became VARCHAR in MySQL 8.0 with the introduction of the data dictionary. The NON_UNIQUE column now has an integer type again (INT because the column need not be as large as BIGINT). * Correctly map `unsigned` database types with precision, scale, size or display width to CLR types. * Fix table/view determination. * Support views in `MySqlDatabaseCleaner`. * Fix some Timestamp/RowVersion issues. Still depends on dotnet/efcore#18592 Addresses #792
Use the default value for non-nullable properties. Fixes #18592 asfasf
Shouldn't the C# generator emit the
ValueConverter.ModelClrType
instead of theValueConverter.ProviderClrType
(line 375) for properties in the snapshot, when aValueConverter
is set?https://github.com/aspnet/EntityFrameworkCore/blob/680b887408744f5163c1e09ae4216072665c402b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs#L366-L376
With the current implementation, if we supply a
ValueConverter
to a type mapping, some strange side effects happen when adding migrations. These include regeneration of seed data or unnecessaryAlterColumn()
calls.For example, our
MySqlDateTimeTypeMapping
can use a value converter to convert betweenbyte[]
(model type) andDateTime
(provider type), to support Timestamp/RowVersion columns (which use the database typetimestamp
but should be implemented asbyte[]
in the model).For this model
and this context
the following first migration is generated
with the following snapshot:
I am not sure, if
ModelClrType
needs to be emitted for thetable.Column<>
call (probably not), but I think it should be emitted for theb.Property<>
call.Adding an additional migration (without any change to the model), will now recreate the seed data, as the source and target property will be considered changed.
Notice that as a side effect of using
ProviderClrType
, the[Required]
annotation does not result in.IsRequired()
being emitted in the snapshot (thoughnullable: false
made it into theUp()
script), becauseProviderClrType
isSystem.DateTime
in our example and is not nullable.However, our
ModelClrType
isbyte[]
and therefore nullable, which now can result inAlterColumn()
calls (that set the nullable state of the property) being generated for every migration.https://github.com/aspnet/EntityFrameworkCore/blob/680b887408744f5163c1e09ae4216072665c402b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs#L396-L401
This is tracked on our repo by PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#792.
The text was updated successfully, but these errors were encountered: