diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index f000d9fcaa..353b992258 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -1126,6 +1126,8 @@ class MigrationAutodetector: self.to_state, ) ) + if field.generated: + dependencies.extend(self._get_dependencies_for_generated_field(field)) # You can't just add NOT NULL fields with no default or fields # which don't allow empty strings as default. time_fields = (models.DateField, models.DateTimeField, models.TimeField) @@ -1547,6 +1549,27 @@ class MigrationAutodetector: ) return dependencies + def _get_dependencies_for_generated_field(self, field): + dependencies = [] + referenced_base_fields = models.Q(field.expression).referenced_base_fields + newly_added_fields = sorted(self.new_field_keys - self.old_field_keys) + for app_label, model_name, added_field_name in newly_added_fields: + added_field = self.to_state.models[app_label, model_name].get_field( + added_field_name + ) + if ( + added_field.remote_field and added_field.remote_field.model + ) or added_field.name in referenced_base_fields: + dependencies.append( + OperationDependency( + app_label, + model_name, + added_field.name, + OperationDependency.Type.CREATE, + ) + ) + return dependencies + def _get_dependencies_for_model(self, app_label, model_name): """Return foreign key dependencies of the given model.""" dependencies = [] diff --git a/docs/releases/5.0.5.txt b/docs/releases/5.0.5.txt index 8836b86131..5421cc9fec 100644 --- a/docs/releases/5.0.5.txt +++ b/docs/releases/5.0.5.txt @@ -23,3 +23,7 @@ Bugfixes * Allowed importing ``aprefetch_related_objects`` from ``django.db.models`` (:ticket:`35392`). + +* Fixed a bug in Django 5.0 that caused a migration crash when a + ``GeneratedField`` was added before any of the referenced fields from its + ``expression`` definition (:ticket:`35359`). diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 4b532df516..d4345208ca 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -13,6 +13,7 @@ from django.db.migrations.graph import MigrationGraph from django.db.migrations.loader import MigrationLoader from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.state import ModelState, ProjectState +from django.db.models.functions import Concat, Lower from django.test import SimpleTestCase, TestCase, override_settings from django.test.utils import isolate_lru_cache @@ -1369,6 +1370,82 @@ class AutodetectorTests(BaseAutodetectorTests): self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True) self.assertEqual(mocked_ask_method.call_count, 3) + def test_add_field_before_generated_field(self): + initial_state = ModelState( + "testapp", + "Author", + [ + ("name", models.CharField(max_length=20)), + ], + ) + updated_state = ModelState( + "testapp", + "Author", + [ + ("name", models.CharField(max_length=20)), + ("surname", models.CharField(max_length=20)), + ( + "lower_full_name", + models.GeneratedField( + expression=Concat(Lower("name"), Lower("surname")), + output_field=models.CharField(max_length=30), + db_persist=True, + ), + ), + ], + ) + changes = self.get_changes([initial_state], [updated_state]) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["AddField", "AddField"]) + self.assertOperationFieldAttributes( + changes, "testapp", 0, 1, expression=Concat(Lower("name"), Lower("surname")) + ) + + def test_add_fk_before_generated_field(self): + initial_state = ModelState( + "testapp", + "Author", + [ + ("name", models.CharField(max_length=20)), + ], + ) + updated_state = [ + ModelState( + "testapp", + "Publisher", + [ + ("name", models.CharField(max_length=20)), + ], + ), + ModelState( + "testapp", + "Author", + [ + ("name", models.CharField(max_length=20)), + ( + "publisher", + models.ForeignKey("testapp.Publisher", models.CASCADE), + ), + ( + "lower_full_name", + models.GeneratedField( + expression=Concat("name", "publisher_id"), + output_field=models.CharField(max_length=20), + db_persist=True, + ), + ), + ], + ), + ] + changes = self.get_changes([initial_state], updated_state) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, "testapp", 0, ["CreateModel", "AddField", "AddField"] + ) + self.assertOperationFieldAttributes( + changes, "testapp", 0, 2, expression=Concat("name", "publisher_id") + ) + def test_remove_field(self): """Tests autodetection of removed fields.""" changes = self.get_changes([self.author_name], [self.author_empty]) diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 79519db0bc..5767fbc42f 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -9,7 +9,7 @@ from django.db.migrations.operations.fields import FieldOperation from django.db.migrations.state import ModelState, ProjectState from django.db.models import F from django.db.models.expressions import Value -from django.db.models.functions import Abs, Pi +from django.db.models.functions import Abs, Concat, Pi from django.db.transaction import atomic from django.test import ( SimpleTestCase, @@ -1379,6 +1379,54 @@ class OperationTests(OperationTestBase): self.assertEqual(definition[1], []) self.assertEqual(sorted(definition[2]), ["field", "model_name", "name"]) + @skipUnlessDBFeature("supports_stored_generated_columns") + def test_add_generate_field(self): + app_label = "test_add_generate_field" + project_state = self.apply_operations( + app_label, + ProjectState(), + operations=[ + migrations.CreateModel( + "Rider", + fields=[ + ("id", models.AutoField(primary_key=True)), + ], + ), + migrations.CreateModel( + "Pony", + fields=[ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=20)), + ( + "rider", + models.ForeignKey( + f"{app_label}.Rider", on_delete=models.CASCADE + ), + ), + ( + "name_and_id", + models.GeneratedField( + expression=Concat(("name"), ("rider_id")), + output_field=models.TextField(), + db_persist=True, + ), + ), + ], + ), + ], + ) + Pony = project_state.apps.get_model(app_label, "Pony") + Rider = project_state.apps.get_model(app_label, "Rider") + rider = Rider.objects.create() + pony = Pony.objects.create(name="pony", rider=rider) + self.assertEqual(pony.name_and_id, str(pony.name) + str(rider.id)) + + new_rider = Rider.objects.create() + pony.rider = new_rider + pony.save() + pony.refresh_from_db() + self.assertEqual(pony.name_and_id, str(pony.name) + str(new_rider.id)) + def test_add_charfield(self): """ Tests the AddField operation on TextField.