Skip to content

Commit

Permalink
Fixed #34944 -- Made GeneratedField.output_field required.
Browse files Browse the repository at this point in the history
Regression in f333e35.
  • Loading branch information
felixxm committed Nov 14, 2023
1 parent de4884b commit 5875f03
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 54 deletions.
12 changes: 3 additions & 9 deletions django/db/models/fields/generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class GeneratedField(Field):
_resolved_expression = None
output_field = None

def __init__(self, *, expression, db_persist=None, output_field=None, **kwargs):
def __init__(self, *, expression, output_field, db_persist=None, **kwargs):
if kwargs.setdefault("editable", False):
raise ValueError("GeneratedField cannot be editable.")
if not kwargs.setdefault("blank", True):
Expand All @@ -29,7 +29,7 @@ def __init__(self, *, expression, db_persist=None, output_field=None, **kwargs):
raise ValueError("GeneratedField.db_persist must be True or False.")

self.expression = expression
self._output_field = output_field
self.output_field = output_field
self.db_persist = db_persist
super().__init__(**kwargs)

Expand All @@ -51,11 +51,6 @@ def contribute_to_class(self, *args, **kwargs):
self._resolved_expression = self.expression.resolve_expression(
self._query, allow_joins=False
)
self.output_field = (
self._output_field
if self._output_field is not None
else self._resolved_expression.output_field
)
# Register lookups from the output_field class.
for lookup_name, lookup in self.output_field.get_class_lookups().items():
self.register_lookup(lookup, lookup_name=lookup_name)
Expand Down Expand Up @@ -150,8 +145,7 @@ def deconstruct(self):
del kwargs["editable"]
kwargs["db_persist"] = self.db_persist
kwargs["expression"] = self.expression
if self._output_field is not None:
kwargs["output_field"] = self._output_field
kwargs["output_field"] = self.output_field
return name, path, args, kwargs

def get_internal_type(self):
Expand Down
12 changes: 5 additions & 7 deletions docs/ref/models/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,7 @@ when :attr:`~django.forms.Field.localize` is ``False`` or

.. versionadded:: 5.0

.. class:: GeneratedField(expression, db_persist=None, output_field=None, **kwargs)
.. class:: GeneratedField(expression, output_field, db_persist=None, **kwargs)

A field that is always computed based on other fields in the model. This field
is managed and updated by the database itself. Uses the ``GENERATED ALWAYS``
Expand All @@ -1259,6 +1259,10 @@ materialized view.
the model (in the same database table). Generated fields cannot reference
other generated fields. Database backends can impose further restrictions.

.. attribute:: GeneratedField.output_field

A model field instance to define the field's data type.

.. attribute:: GeneratedField.db_persist

Determines if the database column should occupy storage as if it were a
Expand All @@ -1268,12 +1272,6 @@ materialized view.
PostgreSQL only supports persisted columns. Oracle only supports virtual
columns.

.. attribute:: GeneratedField.output_field

An optional model field instance to define the field's data type. This can
be used to customize attributes like the field's collation. By default, the
output field is derived from ``expression``.

.. admonition:: Refresh the data

Since the database always computed the value, the object must be reloaded
Expand Down
6 changes: 5 additions & 1 deletion docs/releases/5.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ to create a field that is always computed from other fields. For example::

class Square(models.Model):
side = models.IntegerField()
area = models.GeneratedField(expression=F("side") * F("side"), db_persist=True)
area = models.GeneratedField(
expression=F("side") * F("side"),
output_field=models.BigIntegerField(),
db_persist=True,
)

More options for declaring field choices
----------------------------------------
Expand Down
1 change: 1 addition & 0 deletions tests/admin_views/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@ class Square(models.Model):
area = models.GeneratedField(
db_persist=True,
expression=models.F("side") * models.F("side"),
output_field=models.BigIntegerField(),
)

class Meta:
Expand Down
28 changes: 23 additions & 5 deletions tests/invalid_models_tests/test_ordinary_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,9 @@ def test_not_supported(self):
class Model(models.Model):
name = models.IntegerField()
field = models.GeneratedField(
expression=models.F("name"), db_persist=db_persist
expression=models.F("name"),
output_field=models.IntegerField(),
db_persist=db_persist,
)

expected_errors = []
Expand Down Expand Up @@ -1252,7 +1254,11 @@ class Model(models.Model):
def test_not_supported_stored_required_db_features(self):
class Model(models.Model):
name = models.IntegerField()
field = models.GeneratedField(expression=models.F("name"), db_persist=True)
field = models.GeneratedField(
expression=models.F("name"),
output_field=models.IntegerField(),
db_persist=True,
)

class Meta:
required_db_features = {"supports_stored_generated_columns"}
Expand All @@ -1262,7 +1268,11 @@ class Meta:
def test_not_supported_virtual_required_db_features(self):
class Model(models.Model):
name = models.IntegerField()
field = models.GeneratedField(expression=models.F("name"), db_persist=False)
field = models.GeneratedField(
expression=models.F("name"),
output_field=models.IntegerField(),
db_persist=False,
)

class Meta:
required_db_features = {"supports_virtual_generated_columns"}
Expand All @@ -1273,7 +1283,11 @@ class Meta:
def test_not_supported_virtual(self):
class Model(models.Model):
name = models.IntegerField()
field = models.GeneratedField(expression=models.F("name"), db_persist=False)
field = models.GeneratedField(
expression=models.F("name"),
output_field=models.IntegerField(),
db_persist=False,
)
a = models.TextField()

excepted_errors = (
Expand All @@ -1298,7 +1312,11 @@ class Model(models.Model):
def test_not_supported_stored(self):
class Model(models.Model):
name = models.IntegerField()
field = models.GeneratedField(expression=models.F("name"), db_persist=True)
field = models.GeneratedField(
expression=models.F("name"),
output_field=models.IntegerField(),
db_persist=True,
)
a = models.TextField()

expected_errors = (
Expand Down
28 changes: 22 additions & 6 deletions tests/migrations/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5664,10 +5664,14 @@ def assertModelsAndTables(after_db):
def _test_invalid_generated_field_changes(self, db_persist):
regular = models.IntegerField(default=1)
generated_1 = models.GeneratedField(
expression=F("pink") + F("pink"), db_persist=db_persist
expression=F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
)
generated_2 = models.GeneratedField(
expression=F("pink") + F("pink") + F("pink"), db_persist=db_persist
expression=F("pink") + F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
)
tests = [
("test_igfc_1", regular, generated_1),
Expand Down Expand Up @@ -5707,12 +5711,20 @@ def test_invalid_generated_field_persistency_change(self):
migrations.AddField(
"Pony",
"modified_pink",
models.GeneratedField(expression=F("pink"), db_persist=True),
models.GeneratedField(
expression=F("pink"),
output_field=models.IntegerField(),
db_persist=True,
),
),
migrations.AlterField(
"Pony",
"modified_pink",
models.GeneratedField(expression=F("pink"), db_persist=False),
models.GeneratedField(
expression=F("pink"),
output_field=models.IntegerField(),
db_persist=False,
),
),
]
msg = (
Expand All @@ -5729,7 +5741,9 @@ def _test_add_generated_field(self, db_persist):
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("pink") + F("pink"), db_persist=db_persist
expression=F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
),
)
project_state, new_state = self.make_test_state(app_label, operation)
Expand Down Expand Up @@ -5760,7 +5774,9 @@ def _test_remove_generated_field(self, db_persist):
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("pink") + F("pink"), db_persist=db_persist
expression=F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
),
)
project_state, new_state = self.make_test_state(app_label, operation)
Expand Down
28 changes: 22 additions & 6 deletions tests/model_fields/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,11 @@ class UUIDGrandchild(UUIDChild):
class GeneratedModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
field = models.GeneratedField(expression=F("a") + F("b"), db_persist=True)
field = models.GeneratedField(
expression=F("a") + F("b"),
output_field=models.IntegerField(),
db_persist=True,
)

class Meta:
required_db_features = {"supports_stored_generated_columns"}
Expand All @@ -494,7 +498,11 @@ class Meta:
class GeneratedModelVirtual(models.Model):
a = models.IntegerField()
b = models.IntegerField()
field = models.GeneratedField(expression=F("a") + F("b"), db_persist=False)
field = models.GeneratedField(
expression=F("a") + F("b"),
output_field=models.IntegerField(),
db_persist=False,
)

class Meta:
required_db_features = {"supports_virtual_generated_columns"}
Expand All @@ -503,6 +511,7 @@ class Meta:
class GeneratedModelParams(models.Model):
field = models.GeneratedField(
expression=Value("Constant", output_field=models.CharField(max_length=10)),
output_field=models.CharField(max_length=10),
db_persist=True,
)

Expand All @@ -513,14 +522,15 @@ class Meta:
class GeneratedModelParamsVirtual(models.Model):
field = models.GeneratedField(
expression=Value("Constant", output_field=models.CharField(max_length=10)),
output_field=models.CharField(max_length=10),
db_persist=False,
)

class Meta:
required_db_features = {"supports_virtual_generated_columns"}


class GeneratedModelOutputField(models.Model):
class GeneratedModelOutputFieldDbCollation(models.Model):
name = models.CharField(max_length=10)
lower_name = models.GeneratedField(
expression=Lower("name"),
Expand All @@ -532,7 +542,7 @@ class Meta:
required_db_features = {"supports_stored_generated_columns"}


class GeneratedModelOutputFieldVirtual(models.Model):
class GeneratedModelOutputFieldDbCollationVirtual(models.Model):
name = models.CharField(max_length=10)
lower_name = models.GeneratedField(
expression=Lower("name"),
Expand All @@ -547,7 +557,10 @@ class Meta:
class GeneratedModelNull(models.Model):
name = models.CharField(max_length=10, null=True)
lower_name = models.GeneratedField(
expression=Lower("name"), db_persist=True, null=True
expression=Lower("name"),
output_field=models.CharField(max_length=10),
db_persist=True,
null=True,
)

class Meta:
Expand All @@ -557,7 +570,10 @@ class Meta:
class GeneratedModelNullVirtual(models.Model):
name = models.CharField(max_length=10, null=True)
lower_name = models.GeneratedField(
expression=Lower("name"), db_persist=False, null=True
expression=Lower("name"),
output_field=models.CharField(max_length=10),
db_persist=False,
null=True,
)

class Meta:
Expand Down
Loading

0 comments on commit 5875f03

Please sign in to comment.