Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion django/db/backends/base/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
21 changes: 10 additions & 11 deletions django/db/backends/sqlite3/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<SETTING_NAME>`` must be a dictionary
(got ``<value>`` instead).

The following checks verify that your security-related settings are correctly
configured:
Expand All @@ -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 ``<SETTING_NAME>`` must be a dictionary
(got ``<value>`` instead).

Signals
-------
Expand Down
108 changes: 108 additions & 0 deletions tests/migrations/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading