mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[5.0.x] Fixed #35359 -- Fixed migration operations ordering when adding fields referenced by GeneratedField.expression.
Thank you to Simon Charette for the review.
Backport of 9aeb38c296 from main.
			
			
This commit is contained in:
		| @@ -1035,6 +1035,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) | ||||
| @@ -1435,6 +1437,20 @@ class MigrationAutodetector: | ||||
|             dependencies.append((through_app_label, through_object_name, None, True)) | ||||
|         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((app_label, model_name, added_field.name, True)) | ||||
|         return dependencies | ||||
|  | ||||
|     def _get_dependencies_for_model(self, app_label, model_name): | ||||
|         """Return foreign key dependencies of the given model.""" | ||||
|         dependencies = [] | ||||
|   | ||||
| @@ -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`). | ||||
|   | ||||
| @@ -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, ignore_warnings, override_settings | ||||
| from django.test.utils import isolate_lru_cache | ||||
| from django.utils.deprecation import RemovedInDjango51Warning | ||||
| @@ -1370,6 +1371,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]) | ||||
|   | ||||
| @@ -8,7 +8,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, | ||||
| @@ -1372,6 +1372,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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user