1
0
mirror of https://github.com/django/django.git synced 2025-05-22 06:46:27 +00:00

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.
This commit is contained in:
Simon Charette 2024-01-28 12:02:33 -05:00 committed by Mariusz Felisiak
parent fe1cb62f5c
commit e67d7d70fa
6 changed files with 50 additions and 20 deletions

View File

@ -412,14 +412,13 @@ class BaseDatabaseSchemaEditor:
"""Return the sql and params for the field's database default.""" """Return the sql and params for the field's database default."""
from django.db.models.expressions import Value from django.db.models.expressions import Value
db_default = field._db_default_expression
sql = ( sql = (
self._column_default_sql(field) self._column_default_sql(field) if isinstance(db_default, Value) else "(%s)"
if isinstance(field.db_default, Value)
else "(%s)"
) )
query = Query(model=field.model) query = Query(model=field.model)
compiler = query.get_compiler(connection=self.connection) 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: if self.connection.features.requires_literal_defaults:
# Some databases doesn't support parameterized defaults (Oracle, # Some databases doesn't support parameterized defaults (Oracle,
# SQLite). If this is the case, the individual schema backend # SQLite). If this is the case, the individual schema backend

View File

@ -219,12 +219,6 @@ class Field(RegisterLookupMixin):
self.remote_field = rel self.remote_field = rel
self.is_relation = self.remote_field is not None self.is_relation = self.remote_field is not None
self.default = default 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.db_default = db_default
self.editable = editable self.editable = editable
self.serialize = serialize self.serialize = serialize
@ -408,7 +402,7 @@ class Field(RegisterLookupMixin):
continue continue
connection = connections[db] 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 connection.features.supports_expression_defaults
): ):
msg = f"{self.db_default} cannot be used in db_default." msg = f"{self.db_default} cannot be used in db_default."
@ -994,7 +988,7 @@ class Field(RegisterLookupMixin):
from django.db.models.expressions import DatabaseDefault from django.db.models.expressions import DatabaseDefault
if isinstance(value, DatabaseDefault): if isinstance(value, DatabaseDefault):
return self.db_default return self._db_default_expression
return value return value
def get_prep_value(self, value): def get_prep_value(self, value):
@ -1047,6 +1041,17 @@ class Field(RegisterLookupMixin):
return return_None return return_None
return str # return empty string 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( def get_choices(
self, self,
include_blank=True, include_blank=True,

View File

@ -36,3 +36,8 @@ Bugfixes
* Fixed a bug in Django 5.0 that caused a migration crash on MySQL when adding * Fixed a bug in Django 5.0 that caused a migration crash on MySQL when adding
a ``BinaryField``, ``TextField``, ``JSONField``, or ``GeometryField`` with a a ``BinaryField``, ``TextField``, ``JSONField``, or ``GeometryField`` with a
``db_default`` (:ticket:`35162`). ``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`).

View File

@ -1309,7 +1309,7 @@ class AutodetectorTests(BaseAutodetectorTests):
changes, "testapp", 0, 0, name="name", preserve_default=True changes, "testapp", 0, 0, name="name", preserve_default=True
) )
self.assertOperationFieldAttributes( self.assertOperationFieldAttributes(
changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace") changes, "testapp", 0, 0, db_default="Ada Lovelace"
) )
@mock.patch( @mock.patch(
@ -1515,7 +1515,7 @@ class AutodetectorTests(BaseAutodetectorTests):
changes, "testapp", 0, 0, name="name", preserve_default=True changes, "testapp", 0, 0, name="name", preserve_default=True
) )
self.assertOperationFieldAttributes( self.assertOperationFieldAttributes(
changes, "testapp", 0, 0, db_default=models.Value("Ada Lovelace") changes, "testapp", 0, 0, db_default="Ada Lovelace"
) )
@mock.patch( @mock.patch(

View File

@ -1581,7 +1581,7 @@ class OperationTests(OperationTestBase):
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6) self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
field = new_state.models[app_label, "pony"].fields["height"] field = new_state.models[app_label, "pony"].fields["height"]
self.assertEqual(field.default, models.NOT_PROVIDED) 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) project_state.apps.get_model(app_label, "pony").objects.create(weight=4)
self.assertColumnNotExists(table_name, "height") self.assertColumnNotExists(table_name, "height")
# Add field. # Add field.
@ -1632,7 +1632,7 @@ class OperationTests(OperationTestBase):
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6) self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
field = new_state.models[app_label, "pony"].fields["special_char"] field = new_state.models[app_label, "pony"].fields["special_char"]
self.assertEqual(field.default, models.NOT_PROVIDED) 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") self.assertColumnNotExists(table_name, "special_char")
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
operation.database_forwards( operation.database_forwards(
@ -1700,7 +1700,7 @@ class OperationTests(OperationTestBase):
self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6) self.assertEqual(len(new_state.models[app_label, "pony"].fields), 6)
field = new_state.models[app_label, "pony"].fields["height"] field = new_state.models[app_label, "pony"].fields["height"]
self.assertEqual(field.default, 3) self.assertEqual(field.default, 3)
self.assertEqual(field.db_default, Value(4)) self.assertEqual(field.db_default, 4)
pre_pony_pk = ( pre_pony_pk = (
project_state.apps.get_model(app_label, "pony").objects.create(weight=4).pk project_state.apps.get_model(app_label, "pony").objects.create(weight=4).pk
) )
@ -2145,7 +2145,7 @@ class OperationTests(OperationTestBase):
old_weight = project_state.models[app_label, "pony"].fields["weight"] old_weight = project_state.models[app_label, "pony"].fields["weight"]
self.assertIs(old_weight.db_default, models.NOT_PROVIDED) self.assertIs(old_weight.db_default, models.NOT_PROVIDED)
new_weight = new_state.models[app_label, "pony"].fields["weight"] 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(): with self.assertRaises(IntegrityError), transaction.atomic():
project_state.apps.get_model(app_label, "pony").objects.create() project_state.apps.get_model(app_label, "pony").objects.create()
# Alter field. # Alter field.
@ -2187,7 +2187,7 @@ class OperationTests(OperationTestBase):
self.assertIs(old_pink.db_default, models.NOT_PROVIDED) self.assertIs(old_pink.db_default, models.NOT_PROVIDED)
new_pink = new_state.models[app_label, "pony"].fields["pink"] new_pink = new_state.models[app_label, "pony"].fields["pink"]
self.assertIs(new_pink.default, models.NOT_PROVIDED) 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) pony = project_state.apps.get_model(app_label, "pony").objects.create(weight=1)
self.assertEqual(pony.pink, 3) self.assertEqual(pony.pink, 3)
# Alter field. # Alter field.
@ -2217,7 +2217,7 @@ class OperationTests(OperationTestBase):
old_green = project_state.models[app_label, "pony"].fields["green"] old_green = project_state.models[app_label, "pony"].fields["green"]
self.assertIs(old_green.db_default, models.NOT_PROVIDED) self.assertIs(old_green.db_default, models.NOT_PROVIDED)
new_green = new_state.models[app_label, "pony"].fields["green"] 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( old_pony = project_state.apps.get_model(app_label, "pony").objects.create(
weight=1 weight=1
) )

View File

@ -7,6 +7,7 @@ from unittest import mock
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.serializers.json import DjangoJSONEncoder
from django.db import ( from django.db import (
DatabaseError, DatabaseError,
DataError, DataError,
@ -2333,6 +2334,26 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor, self.assertNumQueries(0): with connection.schema_editor() as editor, self.assertNumQueries(0):
editor.alter_field(Author, Author._meta.get_field("name"), new_field) 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( @skipUnlessDBFeature(
"supports_column_check_constraints", "can_introspect_check_constraints" "supports_column_check_constraints", "can_introspect_check_constraints"
) )