From acb11725f814b925d394f3923241f741b834d1b9 Mon Sep 17 00:00:00 2001 From: Adnan Umer Date: Thu, 22 Aug 2019 14:46:17 +0500 Subject: [PATCH] [3.0.x] Fixed #30591 -- Fixed recreation of foreign key constraints on MySQL when altering type of referenced unique field. Thanks Mariusz Felisiak for tests and Matthijs Kooijman for investigation and initial patch. Backport of 241deed2590bcb1d8c45271d44c86eaedfb57119 from master --- django/db/backends/base/schema.py | 10 +++++-- tests/migrations/test_operations.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 3a0dc39238..98afbcc05a 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -28,12 +28,16 @@ def _is_relevant_relation(relation, altered_field): return altered_field.name in field.to_fields +def _all_related_fields(model): + return model._meta._get_fields(forward=False, reverse=True, include_hidden=True) + + def _related_non_m2m_objects(old_field, new_field): # Filter out m2m objects from reverse relations. # Return (old_relation, new_relation) tuples. return zip( - (obj for obj in old_field.model._meta.related_objects if _is_relevant_relation(obj, old_field)), - (obj for obj in new_field.model._meta.related_objects if _is_relevant_relation(obj, new_field)) + (obj for obj in _all_related_fields(old_field.model) if _is_relevant_relation(obj, old_field)), + (obj for obj in _all_related_fields(new_field.model) if _is_relevant_relation(obj, new_field)), ) @@ -753,7 +757,7 @@ class BaseDatabaseSchemaEditor: # Type alteration on primary key? Then we need to alter the column # referring to us. rels_to_update = [] - if old_field.primary_key and new_field.primary_key and old_type != new_type: + if drop_foreign_keys: rels_to_update.extend(_related_non_m2m_objects(old_field, new_field)) # Changed to become primary key? if self._field_became_primary_key(old_field, new_field): diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 33b76984d4..64e29194ca 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -1308,6 +1308,51 @@ class OperationTests(OperationTestBase): operation.database_backwards("test_alflpkfk", editor, new_state, project_state) assertIdTypeEqualsFkType() + @skipUnlessDBFeature('supports_foreign_keys') + def test_alter_field_reloads_state_on_fk_with_to_field_target_type_change(self): + app_label = 'alter_field_reloads_state_on_fk_with_to_field_target_type_change' + project_state = self.apply_operations(app_label, ProjectState(), operations=[ + migrations.CreateModel('Rider', fields=[ + ('id', models.AutoField(primary_key=True)), + ('code', models.PositiveIntegerField(unique=True)), + ]), + migrations.CreateModel('Pony', fields=[ + ('id', models.AutoField(primary_key=True)), + ('rider', models.ForeignKey('%s.Rider' % app_label, models.CASCADE, to_field='code')), + ]), + ]) + operation = migrations.AlterField( + 'Rider', + 'code', + models.CharField(max_length=100, unique=True), + ) + self.apply_operations(app_label, project_state, operations=[operation]) + + @skipUnlessDBFeature('supports_foreign_keys') + def test_alter_field_reloads_state_on_fk_with_to_field_related_name_target_type_change(self): + app_label = 'alter_field_reloads_state_on_fk_with_to_field_rn_target_type_change' + project_state = self.apply_operations(app_label, ProjectState(), operations=[ + migrations.CreateModel('Rider', fields=[ + ('id', models.AutoField(primary_key=True)), + ('code', models.PositiveIntegerField(unique=True)), + ]), + migrations.CreateModel('Pony', fields=[ + ('id', models.AutoField(primary_key=True)), + ('rider', models.ForeignKey( + '%s.Rider' % app_label, + models.CASCADE, + to_field='code', + related_name='+', + )), + ]), + ]) + operation = migrations.AlterField( + 'Rider', + 'code', + models.CharField(max_length=100, unique=True), + ) + self.apply_operations(app_label, project_state, operations=[operation]) + def test_alter_field_reloads_state_on_fk_target_changes(self): """ If AlterField doesn't reload state appropriately, the second AlterField