Ticket #22432: 22432-fix-m2m-repointing.patch
File 22432-fix-m2m-repointing.patch, 8.8 KB (added by , 11 years ago) |
---|
-
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): 491 491 old_field, 492 492 new_field, 493 493 )) 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 494 500 # Has unique been removed? 495 501 if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)): 496 502 # 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 1 import copy 2 1 3 from decimal import Decimal 2 4 from django.utils import six 3 5 from django.apps.registry import Apps … … class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 63 65 del mapping[old_field.column] 64 66 body[new_field.name] = new_field 65 67 mapping[new_field.column] = old_field.column 68 66 69 # Remove any deleted fields 67 70 for field in delete_fields: 68 71 del body[field.name] … … class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 72 75 return self.delete_model(field.rel.through) 73 76 # Work inside a new app registry 74 77 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 75 84 # Construct a new model for the new state 76 85 meta_contents = { 77 86 'app_label': model._meta.app_label, … … class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 82 91 meta = type("Meta", tuple(), meta_contents) 83 92 body['Meta'] = meta 84 93 body['__module__'] = model.__module__ 94 85 95 temp_model = type(model._meta.object_name, model.__bases__, body) 86 96 # Create a new table with that format 87 97 self.create_model(temp_model) … … class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 134 144 else: 135 145 self._remake_table(model, delete_fields=[field]) 136 146 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.""" 159 149 # Alter by remaking table 160 150 self._remake_table(model, alter_fields=[(old_field, new_field)]) 161 151 … … class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 172 162 Alters M2Ms to repoint their to= endpoints. 173 163 """ 174 164 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 ) 175 176 return 176 177 177 178 # 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): 34 34 with connection.schema_editor() as editor: 35 35 return migration.unapply(project_state, editor) 36 36 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): 38 38 """ 39 39 Creates a test model state and database table. 40 40 """ 41 41 # Delete the tables if they already exist 42 42 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 43 54 try: 44 55 cursor.execute("DROP TABLE %s_pony" % app_label) 45 56 except DatabaseError: … … class OperationTests(MigrationTestBase): 48 59 cursor.execute("DROP TABLE %s_stable" % app_label) 49 60 except DatabaseError: 50 61 pass 62 try: 63 cursor.execute("DROP TABLE %s_van" % app_label) 64 except DatabaseError: 65 pass 51 66 # Make the "current" state 52 67 operations = [migrations.CreateModel( 53 68 "Pony", … … class OperationTests(MigrationTestBase): 64 79 ("id", models.AutoField(primary_key=True)), 65 80 ] 66 81 )) 82 if third_model: 83 operations.append(migrations.CreateModel( 84 "Van", 85 [ 86 ("id", models.AutoField(primary_key=True)), 87 ] 88 )) 67 89 if related_model: 68 90 operations.append(migrations.CreateModel( 69 91 "Rider", … … class OperationTests(MigrationTestBase): 405 427 Pony = new_apps.get_model("test_alflmm", "Pony") 406 428 self.assertTrue(Pony._meta.get_field('stables').blank) 407 429 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 408 451 def test_remove_field_m2m(self): 409 452 project_state = self.set_up_test_model("test_rmflmm", second_model=True) 410 453