diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index c465668120d5..af30f7f7fc00 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -36,7 +36,10 @@ def _is_relevant_relation(relation, altered_field): # Foreign key constraint on the primary key, which is being altered. return True # Is the constraint targeting the field being altered? - return altered_field.name in field.to_fields + return ( + altered_field.name in field.to_fields + or altered_field.attname in field.to_fields + ) def _all_related_fields(model): diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index ee6163c25389..94e3fc5bc778 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -3,7 +3,10 @@ from django.apps.registry import Apps from django.db import NotSupportedError -from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.backends.base.schema import ( + BaseDatabaseSchemaEditor, + _related_non_m2m_objects, +) from django.db.backends.ddl_references import Statement from django.db.backends.utils import strip_quotes from django.db.models import CompositePrimaryKey, UniqueConstraint @@ -391,17 +394,13 @@ def _alter_field( if new_field.unique and ( old_type != new_type or old_collation != new_collation ): - related_models = set() - opts = new_field.model._meta - for remote_field in opts.related_objects: + related_models = { + rel.related_model + for _, rel in _related_non_m2m_objects(old_field, new_field) # Ignore self-relationship since the table was already rebuilt. - if remote_field.related_model == model: - continue - if not remote_field.many_to_many: - if remote_field.field_name == new_field.name: - related_models.add(remote_field.related_model) - elif new_field.primary_key and remote_field.through._meta.auto_created: - related_models.add(remote_field.through) + if rel.related_model != model + } + opts = new_field.model._meta if new_field.primary_key: for many_to_many in opts.many_to_many: # Ignore self-relationship since the table was already diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 5b4e31420bc6..c30081ab9a8a 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -592,8 +592,6 @@ The following checks are run if you use the :option:`check --deploy` option: ``'django-insecure-'`` indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack. -* **security.E026**: The CSP setting ```` must be a dictionary - (got ```` instead). The following checks verify that your security-related settings are correctly configured: @@ -604,6 +602,8 @@ configured: correct number of arguments. * **security.E102**: The CSRF failure view ``'path.to.view'`` could not be imported. +* **security.E026**: The CSP setting ```` must be a dictionary + (got ```` instead). Signals ------- diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 85295d8e5099..923fac06aec4 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -3093,6 +3093,114 @@ def test_alter_field_reloads_state_fk_with_to_field_related_name_target_type_cha ) self.apply_operations(app_label, project_state, operations=[operation]) + @skipUnlessDBFeature("supports_foreign_keys") + def test_alter_field_reloads_state_on_transitive_attname_to_field_type_change( + self, + ): + app_label = "test_alflrstfattnamettc" + project_state = self.apply_operations( + app_label, + ProjectState(), + operations=[ + migrations.CreateModel( + "Primary", + fields=[ + ("id", models.AutoField(primary_key=True)), + ("code", models.CharField(max_length=5, unique=True)), + ], + ), + migrations.CreateModel( + "Related", + fields=[ + ("id", models.AutoField(primary_key=True)), + ( + "primary", + models.OneToOneField( + f"{app_label}.Primary", + models.CASCADE, + to_field="code", + ), + ), + ], + ), + migrations.CreateModel( + "Dependent", + fields=[ + ("id", models.AutoField(primary_key=True)), + ( + "related", + models.ForeignKey( + f"{app_label}.Related", + models.CASCADE, + to_field="primary_id", + ), + ), + ], + ), + ], + ) + + def assert_column_lengths(length): + with connection.cursor() as cursor: + primary_length = [ + c.display_size + for c in connection.introspection.get_table_description( + cursor, f"{app_label}_primary" + ) + if c.name == "code" + ][0] + related_length = [ + c.display_size + for c in connection.introspection.get_table_description( + cursor, f"{app_label}_related" + ) + if c.name == "primary_id" + ][0] + dependent_length = [ + c.display_size + for c in connection.introspection.get_table_description( + cursor, f"{app_label}_dependent" + ) + if c.name == "related_id" + ][0] + self.assertEqual(primary_length, length) + self.assertEqual(related_length, length) + self.assertEqual(dependent_length, length) + + assert_column_lengths(5) + self.assertFKExists( + f"{app_label}_related", + ["primary_id"], + (f"{app_label}_primary", "code"), + ) + self.assertFKExists( + f"{app_label}_dependent", + ["related_id"], + (f"{app_label}_related", "primary_id"), + ) + + operation = migrations.AlterField( + "Primary", + "code", + models.CharField(max_length=11, unique=True), + ) + new_state = project_state.clone() + operation.state_forwards(app_label, new_state) + with connection.schema_editor() as editor: + operation.database_forwards(app_label, editor, project_state, new_state) + + assert_column_lengths(11) + self.assertFKExists( + f"{app_label}_related", + ["primary_id"], + (f"{app_label}_primary", "code"), + ) + self.assertFKExists( + f"{app_label}_dependent", + ["related_id"], + (f"{app_label}_related", "primary_id"), + ) + def test_alter_field_reloads_state_on_fk_target_changes(self): """ If AlterField doesn't reload state appropriately, the second AlterField