Skip to content

Commit

Permalink
Fixed #35235 -- Removed caching of BaseExpression._output_field_or_none.
Browse files Browse the repository at this point in the history
  • Loading branch information
sharonwoo authored and sarahboyce committed Jan 30, 2025
1 parent 12b9ef3 commit cbb0812
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 7 deletions.
17 changes: 10 additions & 7 deletions django/db/models/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,16 @@ def __invert__(self):
return NegatedExpression(self)


class OutputFieldIsNoneError(FieldError):
pass


class BaseExpression:
"""Base class for all query expressions."""

empty_result_set_value = NotImplemented
# aggregate specific fields
is_summary = False
_output_field_resolved_to_none = False
# Can the expression be used in a WHERE clause?
filterable = True
# Can the expression be used as a source expression in Window?
Expand Down Expand Up @@ -323,21 +326,21 @@ def output_field(self):
"""Return the output type of this expressions."""
output_field = self._resolve_output_field()
if output_field is None:
self._output_field_resolved_to_none = True
raise FieldError("Cannot resolve expression type, unknown output_field")
raise OutputFieldIsNoneError(
"Cannot resolve expression type, unknown output_field"
)
return output_field

@cached_property
@property
def _output_field_or_none(self):
"""
Return the output field of this expression, or None if
_resolve_output_field() didn't return an output type.
"""
try:
return self.output_field
except FieldError:
if not self._output_field_resolved_to_none:
raise
except OutputFieldIsNoneError:
return

def _resolve_output_field(self):
"""
Expand Down
11 changes: 11 additions & 0 deletions tests/expressions/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Combinable,
CombinedExpression,
NegatedExpression,
OutputFieldIsNoneError,
RawSQL,
Ref,
)
Expand Down Expand Up @@ -2329,6 +2330,16 @@ def test_output_field_decimalfield(self):
time = Time.objects.annotate(one=Value(1, output_field=DecimalField())).first()
self.assertEqual(time.one, 1)

def test_output_field_is_none_error(self):
with self.assertRaises(OutputFieldIsNoneError):
Employee.objects.annotate(custom_expression=Value(None)).first()

def test_output_field_or_none_property_not_cached(self):
expression = Value(None, output_field=None)
self.assertIsNone(expression._output_field_or_none)
expression.output_field = BooleanField()
self.assertIsInstance(expression._output_field_or_none, BooleanField)

def test_resolve_output_field(self):
value_types = [
("str", CharField),
Expand Down
16 changes: 16 additions & 0 deletions tests/postgres_tests/test_aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ def test_array_agg_filter_slice(self):
)
self.assertCountEqual(qs, [[], [5]])

def test_array_agg_with_empty_filter_and_default_values(self):
for filter_value in ([-1], []):
for default_value in ([], Value([])):
with self.subTest(filter=filter_value, default=default_value):
queryset = AggregateTestModel.objects.annotate(
test_array_agg=ArrayAgg(
"stattestmodel__int1",
filter=Q(pk__in=filter_value),
default=default_value,
)
)
self.assertSequenceEqual(
queryset.values_list("test_array_agg", flat=True),
[[], [], [], []],
)

def test_bit_and_general(self):
values = AggregateTestModel.objects.filter(integer_field__in=[0, 1]).aggregate(
bitand=BitAnd("integer_field")
Expand Down

0 comments on commit cbb0812

Please sign in to comment.