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

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

Patch+tests to fix M2M repointing on SQLite

  • django/db/backends/schema.py

    diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py
    index 2a62803..3378568 100644
    a b class BaseDatabaseSchemaEditor(object):  
    491491                old_field,
    492492                new_field,
    493493            ))
     494
     495        self._alter_field(model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict)
     496
     497    def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False):
     498        """Actually perform a "physical" (non-ManyToMany) field update."""
     499
    494500        # Has unique been removed?
    495501        if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)):
    496502            # 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..8c50b34 100644
    a b  
     1import copy
     2
    13from decimal import Decimal
    24from django.utils import six
    35from django.apps.registry import Apps
    class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    6365            del mapping[old_field.column]
    6466            body[new_field.name] = new_field
    6567            mapping[new_field.column] = old_field.column
     68
    6669        # Remove any deleted fields
    6770        for field in delete_fields:
    6871            del body[field.name]
    class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    7275                return self.delete_model(field.rel.through)
    7376        # Work inside a new app registry
    7477        apps = Apps()
     78
     79        # Provide isolated instances of the fields to the new model body
     80        # Instantiating the new model with an alternate db_table will alter
     81        # the internal references of some of the provided fields.
     82        body = copy.deepcopy(body)
     83
    7584        # Construct a new model for the new state
    7685        meta_contents = {
    7786            'app_label': model._meta.app_label,
    class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    8291        meta = type("Meta", tuple(), meta_contents)
    8392        body['Meta'] = meta
    8493        body['__module__'] = model.__module__
     94
    8595        temp_model = type(model._meta.object_name, model.__bases__, body)
    8696        # Create a new table with that format
    8797        self.create_model(temp_model)
    class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    134144        else:
    135145            self._remake_table(model, delete_fields=[field])
    136146
    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             ))
     147    def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False):
     148        """Actually perform a "physical" (non-ManyToMany) field update."""
    159149        # Alter by remaking table
    160150        self._remake_table(model, alter_fields=[(old_field, new_field)])
    161151
    class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):  
    172162        Alters M2Ms to repoint their to= endpoints.
    173163        """
    174164        if old_field.rel.through._meta.db_table == new_field.rel.through._meta.db_table:
     165            # The field name didn't change, but some options did; we have to propagate this altering.
     166            self._remake_table(
     167                old_field.rel.through,
     168                alter_fields=[(
     169                    # We need the field that points to the target model, so we can tell alter_field to change it -
     170                    # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
     171                    old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0],
     172                    new_field.rel.through._meta.get_field_by_name(new_field.m2m_reverse_field_name())[0],
     173                )],
     174                override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()),
     175            )
    175176            return
    176177
    177178        # Make a new through table
  • tests/migrations/test_operations.py

    diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
    index 388544c..631f965 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        project_state = self.apply_operations("test_alflmm", project_state, operations=[
     440            migrations.AlterField("Pony", "places", models.ManyToManyField(to="Van", related_name="ponies"))
     441        ])
     442
     443        # Ensure the new field actually works
     444        new_apps = project_state.render()
     445        Pony = new_apps.get_model("test_alflmm", "Pony")
     446        p = Pony.objects.create(pink=False, weight=4.55)
     447        p.places.create()
     448        self.assertEqual(p.places.count(), 1)
     449        p.places.all().delete()
     450
    408451    def test_remove_field_m2m(self):
    409452        project_state = self.set_up_test_model("test_rmflmm", second_model=True)
    410453
Back to Top