Ticket #22432: 22432-prevent-m2m-repointing.patch

File 22432-prevent-m2m-repointing.patch, 7.2 KB (added by Raphaël Barrois, 10 years ago)

Patch+tests to prevent M2M alters on all databases

  • django/db/backends/schema.py

    diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py
    index 2a62803..69bdc25 100644
    a b class BaseDatabaseSchemaEditor(object):  
    481481        old_type = old_db_params['type']
    482482        new_db_params = new_field.db_parameters(connection=self.connection)
    483483        new_type = new_db_params['type']
    484         if old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and old_field.rel.through._meta.auto_created and new_field.rel.through._meta.auto_created):
     484        if (old_type is None and new_type is None
     485                and old_field.rel.through and new_field.rel.through
     486                and old_field.rel.through._meta.auto_created
     487                and new_field.rel.through._meta.auto_created
     488                and old_field.rel.to._meta.db_table == new_field.rel.to._meta.db_table):
     489            # ManyToMany with automatic through and no 'to=' change
    485490            return self._alter_many_to_many(model, old_field, new_field, strict)
    486491        elif old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and not old_field.rel.through._meta.auto_created and not new_field.rel.through._meta.auto_created):
    487492            # Both sides have through models; this is a no-op.
    class BaseDatabaseSchemaEditor(object):  
    491496                old_field,
    492497                new_field,
    493498            ))
     499
     500        self._alter_field(model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict)
     501
     502    def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False):
     503        """Actually perform a "physical" (non-ManyToMany) field update."""
     504
    494505        # Has unique been removed?
    495506        if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)):
    496507            # Find the unique constraint for this field
  • django/db/backends/sqlite3/schema.py

    diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py
    index 5b6155b..771fefb 100644
    a b class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    134134        else:
    135135            self._remake_table(model, delete_fields=[field])
    136136
    137     def alter_field(self, model, old_field, new_field, strict=False):
    138         """
    139         Allows a field's type, uniqueness, nullability, default, column,
    140         constraints etc. to be modified.
    141         Requires a copy of the old field as well so we can only perform
    142         changes that are required.
    143         If strict is true, raises errors if the old column does not match old_field precisely.
    144         """
    145         old_db_params = old_field.db_parameters(connection=self.connection)
    146         old_type = old_db_params['type']
    147         new_db_params = new_field.db_parameters(connection=self.connection)
    148         new_type = new_db_params['type']
    149         if old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and old_field.rel.through._meta.auto_created and new_field.rel.through._meta.auto_created):
    150             return self._alter_many_to_many(model, old_field, new_field, strict)
    151         elif old_type is None and new_type is None and (old_field.rel.through and new_field.rel.through and not old_field.rel.through._meta.auto_created and not new_field.rel.through._meta.auto_created):
    152             # Both sides have through models; this is a no-op.
    153             return
    154         elif old_type is None or new_type is None:
    155             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)" % (
    156                 old_field,
    157                 new_field,
    158             ))
     137    def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False):
    159138        # Alter by remaking table
    160139        self._remake_table(model, alter_fields=[(old_field, new_field)])
    161140
  • tests/migrations/test_operations.py

    diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
    index 388544c..b5da089 100644
    a b class OperationTests(MigrationTestBase):  
    3434        with connection.schema_editor() as editor:
    3535            return migration.unapply(project_state, editor)
    3636
    37     def set_up_test_model(self, app_label, second_model=False, related_model=False, mti_model=False):
     37    def set_up_test_model(self, app_label, second_model=False, third_model=False, related_model=False, mti_model=False):
    3838        """
    3939        Creates a test model state and database table.
    4040        """
    4141        # Delete the tables if they already exist
    4242        with connection.cursor() as cursor:
     43            # Start with ManyToMany tables
     44            try:
     45                cursor.execute("DROP TABLE %s_pony_stables" % app_label)
     46            except DatabaseError:
     47                pass
     48            try:
     49                cursor.execute("DROP TABLE %s_pony_vans" % app_label)
     50            except DatabaseError:
     51                pass
     52
     53            # Then standard model tables
    4354            try:
    4455                cursor.execute("DROP TABLE %s_pony" % app_label)
    4556            except DatabaseError:
    class OperationTests(MigrationTestBase):  
    4859                cursor.execute("DROP TABLE %s_stable" % app_label)
    4960            except DatabaseError:
    5061                pass
     62            try:
     63                cursor.execute("DROP TABLE %s_van" % app_label)
     64            except DatabaseError:
     65                pass
    5166        # Make the "current" state
    5267        operations = [migrations.CreateModel(
    5368            "Pony",
    class OperationTests(MigrationTestBase):  
    6479                    ("id", models.AutoField(primary_key=True)),
    6580                ]
    6681            ))
     82        if third_model:
     83            operations.append(migrations.CreateModel(
     84                "Van",
     85                [
     86                    ("id", models.AutoField(primary_key=True)),
     87                ]
     88            ))
    6789        if related_model:
    6890            operations.append(migrations.CreateModel(
    6991                "Rider",
    class OperationTests(MigrationTestBase):  
    405427        Pony = new_apps.get_model("test_alflmm", "Pony")
    406428        self.assertTrue(Pony._meta.get_field('stables').blank)
    407429
     430    def test_repoint_field_m2m(self):
     431        project_state = self.set_up_test_model("test_alflmm", second_model=True, third_model=True)
     432
     433        project_state = self.apply_operations("test_alflmm", project_state, operations=[
     434            migrations.AddField("Pony", "places", models.ManyToManyField("Stable", related_name="ponies"))
     435        ])
     436        new_apps = project_state.render()
     437        Pony = new_apps.get_model("test_alflmm", "Pony")
     438
     439        with self.assertRaises(ValueError):
     440            # Changing the 'to=' of a ManyToManyField isn't allowed.
     441            self.apply_operations("test_alflmm", project_state, operations=[
     442                migrations.AlterField("Pony", "places", models.ManyToManyField(to="Van", related_name="ponies"))
     443            ])
     444
    408445    def test_remove_field_m2m(self):
    409446        project_state = self.set_up_test_model("test_rmflmm", second_model=True)
    410447
Back to Top