1
0
mirror of https://github.com/django/django.git synced 2025-06-05 03:29:12 +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 9aeb38c296c69532c7e64b5e3d706a5eb17b3f12 from main.
This commit is contained in:
DevilsAutumn 2024-04-12 20:01:41 +05:30 committed by Natalia
parent fa202d5cb1
commit 24f54c3b09
4 changed files with 146 additions and 1 deletions

View File

@ -1035,6 +1035,8 @@ class MigrationAutodetector:
self.to_state, 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 # You can't just add NOT NULL fields with no default or fields
# which don't allow empty strings as default. # which don't allow empty strings as default.
time_fields = (models.DateField, models.DateTimeField, models.TimeField) time_fields = (models.DateField, models.DateTimeField, models.TimeField)
@ -1435,6 +1437,20 @@ class MigrationAutodetector:
dependencies.append((through_app_label, through_object_name, None, True)) dependencies.append((through_app_label, through_object_name, None, True))
return dependencies 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): def _get_dependencies_for_model(self, app_label, model_name):
"""Return foreign key dependencies of the given model.""" """Return foreign key dependencies of the given model."""
dependencies = [] dependencies = []

View File

@ -23,3 +23,7 @@ Bugfixes
* Allowed importing ``aprefetch_related_objects`` from ``django.db.models`` * Allowed importing ``aprefetch_related_objects`` from ``django.db.models``
(:ticket:`35392`). (: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`).

View File

@ -13,6 +13,7 @@ from django.db.migrations.graph import MigrationGraph
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.state import ModelState, ProjectState 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 import SimpleTestCase, TestCase, ignore_warnings, override_settings
from django.test.utils import isolate_lru_cache from django.test.utils import isolate_lru_cache
from django.utils.deprecation import RemovedInDjango51Warning from django.utils.deprecation import RemovedInDjango51Warning
@ -1370,6 +1371,82 @@ class AutodetectorTests(BaseAutodetectorTests):
self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True) self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True)
self.assertEqual(mocked_ask_method.call_count, 3) 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): def test_remove_field(self):
"""Tests autodetection of removed fields.""" """Tests autodetection of removed fields."""
changes = self.get_changes([self.author_name], [self.author_empty]) changes = self.get_changes([self.author_name], [self.author_empty])

View File

@ -8,7 +8,7 @@ from django.db.migrations.operations.fields import FieldOperation
from django.db.migrations.state import ModelState, ProjectState from django.db.migrations.state import ModelState, ProjectState
from django.db.models import F from django.db.models import F
from django.db.models.expressions import Value 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.db.transaction import atomic
from django.test import ( from django.test import (
SimpleTestCase, SimpleTestCase,
@ -1372,6 +1372,54 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[1], []) self.assertEqual(definition[1], [])
self.assertEqual(sorted(definition[2]), ["field", "model_name", "name"]) 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): def test_add_charfield(self):
""" """
Tests the AddField operation on TextField. Tests the AddField operation on TextField.