#22432 closed Bug (fixed)
Switching model on an M2M field results in a broken db (no change to the automatic through table)
| Reported by: | Stephen Burrows | Owned by: | Raphaël Barrois | 
|---|---|---|---|
| Component: | Migrations | Version: | 1.7-beta-1 | 
| Severity: | Release blocker | Keywords: | |
| Cc: | stephen.r.burrows@… | Triage Stage: | Accepted | 
| Has patch: | yes | Needs documentation: | no | 
| Needs tests: | no | Patch needs improvement: | no | 
| Easy pickings: | no | UI/UX: | no | 
Description
I had a field that looked like this:
class EventPerson(models.Model)
    person_prefer = models.ManyToManyField('self')
But that was wrong (copy-paste mistake) so I switched it to this:
class EventPerson(models.Model):
    person_prefer = models.ManyToManyField(Person)
Then I ran makemigrations and migrate. And now I'm getting database errors because the auto-generated through table still looks like this:
CREATE TABLE "brambling_eventperson_person_prefer"
("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
 "from_eventperson_id" integer NOT NULL REFERENCES "brambling_eventperson" ("id"),
 "to_eventperson_id" integer NOT NULL REFERENCES "brambling_eventperson" ("id"),
UNIQUE ("from_eventperson_id", "to_eventperson_id"));
... and I have no easy way to recover.
Attachments (3)
Change History (19)
comment:1 by , 12 years ago
| Cc: | added | 
|---|
comment:2 by , 12 years ago
| Triage Stage: | Unreviewed → Accepted | 
|---|
Hi,
I can indeed reproduce this with the following models:
from django.db import models class Foo(models.Model): pass class Bar(models.Model): pass class Baz(models.Model): m2m = models.ManyToManyField(Foo)
After changing to models.ManyToManyField(Bar) and running makemigrations and migrate, I can see (using ./manage.py dbshell) that the schema for the m2m table is wrong (the m2m table still references Foo instead of Bar).
I agree that this is a release blocker.
Thanks
comment:3 by , 12 years ago
| Owner: | changed from to | 
|---|---|
| Status: | new → assigned | 
comment:4 by , 12 years ago
The check here prevents any action from being taken if the many to many table name hasn't changed which is the case if you simply change the model. I'm not sure exactly how we should handle this as I would guess existing data should probably be dropped. I checked how south handled this, but it looks like schemamigration --auto doesn't detect a change if the ManyToManyField model is changed.
comment:5 by , 11 years ago
This is a tricky one to fix. I'm tempted to just block any changes on M2M fields (for this and #22476) in 1.7 and ship it so that makemigrations dies saying "you can't alter M2Ms, if you want to change it delete and add a new one!", as there's no way we can write generic M2M alteration code in time, and it's not a regression, as you couldn't do any alteration at all before.
Thoughts?
comment:6 by , 11 years ago
Blocking this one with an error makes sense, because it's a weird edge case (and probably shouldn't be done, honestly) but #22476 is related to changes to fields that *don't* affect the database (blank, help_text, etc) but still generate migrations) so it should probably still get fixed (IMO).
comment:7 by , 11 years ago
I cannot replicate this locally - the foreign key is correctly repointed - and we even have a test for this, it turns out, in schema.tests.SchemaTests.test_m2m_repoint.
Could the reporter or other contributors please try and replicate and tell me their results? I followed the bmispelon's reproduction notes - made the three models, changed the field to models.ManyToManyField(Bar), ran makemigrations and migrate, and voila, the foreign key had changed to point to bar.
comment:8 by , 11 years ago
| Owner: | removed | 
|---|---|
| Status: | assigned → new | 
I think this only affects SQLite -- the check I linked in comment 4 that causes the issue is only in the SQLite schema editor. In addition, test_m2m_repoint is largely skipped on SQlite since supports_foreign_keys = False (although newer version of it do support FKs; see #14204).
comment:9 by , 11 years ago
Yes, I can still replicate this with the latest 1.7, using the method described by bmispelon. After creating the models and then creating and running the migrations, the database looks like this:
sqlite> .schema brambling_baz_m2m
CREATE TABLE "brambling_baz_m2m" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "baz_id" integer NOT NULL REFERENCES "brambling_baz" ("id"), "foo_id" integer NOT NULL REFERENCES "brambling_foo" ("id"), UNIQUE ("baz_id", "foo_id"));
CREATE INDEX brambling_baz_m2m_60ef55ec ON "brambling_baz_m2m" ("baz_id");
CREATE INDEX brambling_baz_m2m_a8d1fe7d ON "brambling_baz_m2m" ("foo_id");
The second FK *should* be bar_id and reference brambling_bar ("id").
Behavior that would make sense to me:
- Delete the through table (perhaps with a warning when creating the migration), then create the new table.
- Raise an error to prevent this type of change for now until a better solution is found.
comment:10 by , 11 years ago
| Owner: | set to | 
|---|---|
| Status: | new → assigned | 
comment:11 by , 11 years ago
I've added a failing test, I'm attaching the patch.
The bug indeed occurs with sqlite, not with postgresql.
comment:12 by , 11 years ago
After careful examination, the code indicates that altering the to= of a ManyToManyField was meant to be forbidden in all cases:
# backends/schema.py:490 raise ValueError("Cannot alter field %s into %s - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M fields)")
There are two problems with this approach:
- Changing the to=field actually works on all backends but SQLite (provided the newForeignKeyconstraint applies properly, i.e the primary key of all instances of the oldto=field are found in thepkcolumn of the newto=field)
- The standard alter_fieldcode can't be called on SQLite (remaking the table is required), and the SQLite-specific version doesn't execute anything when changing DB-related attributes of aManyToManyField.
We have two options here:
- Go for the intended direction of forbidding altering a ManyToManyFieldthrough migrations
- Implement the ManyToManyField-altering logic for SQLite
The second approach sounds more useful to me, but I've provided both patches.
Note: Changing the to= of a ManyToManyField is actually simply changing the target of a ForeignKey, which is supported by migrations as long as the FK constraint is valid when using the new target column (all source pks are present in the target column).
by , 11 years ago
| Attachment: | 22432-fix-m2m-repointing.patch added | 
|---|
Patch+tests to fix M2M repointing on SQLite
by , 11 years ago
| Attachment: | 22432-prevent-m2m-repointing.patch added | 
|---|
Patch+tests to prevent M2M alters on all databases
comment:13 by , 11 years ago
| Has patch: | set | 
|---|
comment:14 by , 11 years ago
| Resolution: | → fixed | 
|---|---|
| Status: | assigned → closed | 
comment:15 by , 11 years ago
xelnor: You are amazing. It's like a choose-your-own-adventure of patches.
This seems pretty severe, with potential for data loss/unexpected crashes, so I marked it as a release blocker. Apologies if I shouldn't have.