summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDevilsAutumn <bhuvnesh875@gmail.com>2024-04-12 20:01:41 +0530
committerNatalia <124304+nessita@users.noreply.github.com>2024-05-02 21:54:52 -0300
commit24f54c3b09ab4f19bfc11c5d7ba80985ed870103 (patch)
tree41f3d8c449ffb882b2fbca22fa8565d5f748394e
parentfa202d5cb1f16b9bbfd9da72eb03125fabc34bb8 (diff)
[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 9aeb38c296c69532c7e64b5e3d706a5eb17b3f12 from main.
-rw-r--r--django/db/migrations/autodetector.py16
-rw-r--r--docs/releases/5.0.5.txt4
-rw-r--r--tests/migrations/test_autodetector.py77
-rw-r--r--tests/migrations/test_operations.py50
4 files changed, 146 insertions, 1 deletions
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index 13af3f0c03..832a31c765 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -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 = []
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 d0a76e1080..1ba814dd3f 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, 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])
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 9fb8f0aa35..49289efcef 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -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.