mirror of
https://github.com/django/django.git
synced 2025-03-09 17:02:43 +00:00
[5.0.x] Fixed #35149 -- Fixed crashes of db_default with unresolvable output field.
Field.db_default accepts either literal Python values or compilables (as_sql) and wrap the former ones in Value internally. While 1e38f11 added support for automatic resolving of output fields for types such as str, int, float, and other unambigous ones it's cannot do so for all types such as dict or even contrib.postgres and contrib.gis primitives. When a literal, non-compilable, value is provided it likely make the most sense to bind its output field to the field its attached to avoid forcing the user to provide an explicit `Value(output_field)`. Thanks David Sanders for the report. Backport of e67d7d70fa10c06aca36b9057f82054eda45269d from main
This commit is contained in:
parent
914eee1a9b
commit
761946f8e1
@ -412,14 +412,13 @@ class BaseDatabaseSchemaEditor:
|
||||
"""Return the sql and params for the field's database default."""
|
||||
from django.db.models.expressions import Value
|
||||
|
||||
db_default = field._db_default_expression
|
||||
sql = (
|
||||
self._column_default_sql(field)
|
||||
if isinstance(field.db_default, Value)
|
||||
else "(%s)"
|
||||
self._column_default_sql(field) if isinstance(db_default, Value) else "(%s)"
|
||||
)
|
||||
query = Query(model=field.model)
|
||||
compiler = query.get_compiler(connection=self.connection)
|
||||
default_sql, params = compiler.compile(field.db_default)
|
||||
default_sql, params = compiler.compile(db_default)
|
||||
if self.connection.features.requires_literal_defaults:
|
||||
# Some databases doesn't support parameterized defaults (Oracle,
|
||||
# SQLite). If this is the case, the individual schema backend
|
||||
|
@ -218,12 +218,6 @@ class Field(RegisterLookupMixin):
|
||||
self.remote_field = rel
|
||||
self.is_relation = self.remote_field is not None
|
||||
self.default = default
|
||||
if db_default is not NOT_PROVIDED and not hasattr(
|
||||
db_default, "resolve_expression"
|
||||
):
|
||||
from django.db.models.expressions import Value
|
||||
|
||||
db_default = Value(db_default)
|
||||
self.db_default = db_default
|
||||
self.editable = editable
|
||||
self.serialize = serialize
|
||||
@ -407,7 +401,7 @@ class Field(RegisterLookupMixin):
|
||||
continue
|
||||
connection = connections[db]
|
||||
|
||||
if not getattr(self.db_default, "allowed_default", False) and (
|
||||
if not getattr(self._db_default_expression, "allowed_default", False) and (
|
||||
connection.features.supports_expression_defaults
|
||||
):
|
||||
msg = f"{self.db_default} cannot be used in db_default."
|
||||
@ -993,7 +987,7 @@ class Field(RegisterLookupMixin):
|
||||
from django.db.models.expressions import DatabaseDefault
|
||||
|
||||
if isinstance(value, DatabaseDefault):
|
||||
return self.db_default
|
||||
return self._db_default_expression
|
||||
return value
|
||||
|
||||
def get_prep_value(self, value):
|
||||
@ -1046,6 +1040,17 @@ class Field(RegisterLookupMixin):
|
||||
return return_None
|
||||
return str # return empty string
|
||||
|
||||
@cached_property
|
||||
def _db_default_expression(self):
|
||||
db_default = self.db_default
|
||||
if db_default is not NOT_PROVIDED and not hasattr(
|
||||
db_default, "resolve_expression"
|
||||
):
|
||||
from django.db.models.expressions import Value
|
||||
|
||||
db_default = Value(db_default, self)
|
||||
return db_default
|
||||
|
||||
def get_choices(
|
||||
self,
|
||||
include_blank=True,
|
||||
|
@ -36,3 +36,8 @@ Bugfixes
|
||||
* Fixed a bug in Django 5.0 that caused a migration crash on MySQL when adding
|
||||
a ``BinaryField``, ``TextField``, ``JSONField``, or ``GeometryField`` with a
|
||||
``db_default`` (:ticket:`35162`).
|
||||
|
||||
* Fixed a bug in Django 5.0 that caused a migration crash on models with a
|
||||
literal ``db_default`` of a complex type such as ``dict`` instance of a
|
||||
``JSONField``. Running ``makemigrations`` might generate no-op ``AlterField``
|
||||
operations for fields using ``db_default`` (:ticket:`35149`).
|
||||
|
@ -1310,7 +1310,7 @@ class AutodetectorTests(BaseAutodetectorTests):
|
||||
changes, "testapp", 0, 0, name="name", preserve_default=True
|
||||
)
|
||||
self.assertOperationFieldAttributes(
|
||||
changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace")
|
||||
changes, "testapp", 0, 0, db_default="Ada Lovelace"
|
||||
)
|
||||
|
||||
@mock.patch(
|
||||
@ -1516,7 +1516,7 @@ class AutodetectorTests(BaseAutodetectorTests):
|
||||
changes, "testapp", 0, 0, name="name", preserve_default=True
|
||||
)
|
||||
self.assertOperationFieldAttributes(
|
||||
changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace")
|
||||
changes, "testapp", 0, 0, db_default="Ada Lovelace"
|
||||
)
|
||||
|
||||
@mock.patch(
|
||||
|
@ -1574,7 +1574,7 @@ class OperationTests(OperationTestBase):
|
||||
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
||||
field = new_state.models[app_label, "pony"].fields["height"]
|
||||
self.assertEqual(field.default, models.NOT_PROVIDED)
|
||||
self.assertEqual(field.db_default, Value(4))
|
||||
self.assertEqual(field.db_default, 4)
|
||||
project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
|
||||
self.assertColumnNotExists(table_name, "height")
|
||||
# Add field.
|
||||
@ -1625,7 +1625,7 @@ class OperationTests(OperationTestBase):
|
||||
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
||||
field = new_state.models[app_label, "pony"].fields["special_char"]
|
||||
self.assertEqual(field.default, models.NOT_PROVIDED)
|
||||
self.assertEqual(field.db_default, Value(db_default))
|
||||
self.assertEqual(field.db_default, db_default)
|
||||
self.assertColumnNotExists(table_name, "special_char")
|
||||
with connection.schema_editor() as editor:
|
||||
operation.database_forwards(
|
||||
@ -1693,7 +1693,7 @@ class OperationTests(OperationTestBase):
|
||||
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
|
||||
field = new_state.models[app_label, "pony"].fields["height"]
|
||||
self.assertEqual(field.default, 3)
|
||||
self.assertEqual(field.db_default, Value(4))
|
||||
self.assertEqual(field.db_default, 4)
|
||||
pre_pony_pk = (
|
||||
project_state.apps.get_model(app_label, "pony").objects.create(weight=4).pk
|
||||
)
|
||||
@ -2128,7 +2128,7 @@ class OperationTests(OperationTestBase):
|
||||
old_weight = project_state.models[app_label, "pony"].fields["weight"]
|
||||
self.assertIs(old_weight.db_default, models.NOT_PROVIDED)
|
||||
new_weight = new_state.models[app_label, "pony"].fields["weight"]
|
||||
self.assertEqual(new_weight.db_default, Value(4.5))
|
||||
self.assertEqual(new_weight.db_default, 4.5)
|
||||
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||
project_state.apps.get_model(app_label, "pony").objects.create()
|
||||
# Alter field.
|
||||
@ -2170,7 +2170,7 @@ class OperationTests(OperationTestBase):
|
||||
self.assertIs(old_pink.db_default, models.NOT_PROVIDED)
|
||||
new_pink = new_state.models[app_label, "pony"].fields["pink"]
|
||||
self.assertIs(new_pink.default, models.NOT_PROVIDED)
|
||||
self.assertEqual(new_pink.db_default, Value(4))
|
||||
self.assertEqual(new_pink.db_default, 4)
|
||||
pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
|
||||
self.assertEqual(pony.pink, 3)
|
||||
# Alter field.
|
||||
@ -2200,7 +2200,7 @@ class OperationTests(OperationTestBase):
|
||||
old_green = project_state.models[app_label, "pony"].fields["green"]
|
||||
self.assertIs(old_green.db_default, models.NOT_PROVIDED)
|
||||
new_green = new_state.models[app_label, "pony"].fields["green"]
|
||||
self.assertEqual(new_green.db_default, Value(4))
|
||||
self.assertEqual(new_green.db_default, 4)
|
||||
old_pony = project_state.apps.get_model(app_label, "pony").objects.create(
|
||||
weight=1
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ from unittest import mock
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
from django.core.management.color import no_style
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import (
|
||||
DatabaseError,
|
||||
DataError,
|
||||
@ -2336,6 +2337,26 @@ class SchemaTests(TransactionTestCase):
|
||||
with connection.schema_editor() as editor, self.assertNumQueries(0):
|
||||
editor.alter_field(Author, Author._meta.get_field("name"), new_field)
|
||||
|
||||
@isolate_apps("schema")
|
||||
def test_db_default_output_field_resolving(self):
|
||||
class Author(Model):
|
||||
data = JSONField(
|
||||
encoder=DjangoJSONEncoder,
|
||||
db_default={
|
||||
"epoch": datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
app_label = "schema"
|
||||
|
||||
with connection.schema_editor() as editor:
|
||||
editor.create_model(Author)
|
||||
|
||||
author = Author.objects.create()
|
||||
author.refresh_from_db()
|
||||
self.assertEqual(author.data, {"epoch": "1970-01-01T00:00:00Z"})
|
||||
|
||||
@skipUnlessDBFeature(
|
||||
"supports_column_check_constraints", "can_introspect_check_constraints"
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user