-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #35235 -- ArrayAgg() doesn't return default when filter contains __in=[]. #17890
base: main
Are you sure you want to change the base?
Fixed #35235 -- ArrayAgg() doesn't return default when filter contains __in=[]. #17890
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello! Thank you for your contribution 💪
As it's your first contribution be sure to check out the patch review checklist.
If you're fixing a ticket from Trac make sure to set the "Has patch" flag and include a link to this PR in the ticket!
If you have any design or process questions then you can ask in the Django forum.
Welcome aboard ⛵️!
) | ||
self.assertEqual([], result) | ||
|
||
self.assertIn("[] != '{}'", str(context.exception)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test, with _output_field_or_none
as @cached_property
, raises an exception as result gives '{}'. I then call the context.exception to show what the result is... a bit clumsy I know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only for experiments, right?
Normally, I'd use:
qs = AggregateTestModel.objects.annotate(
test_array_agg=ArrayAgg(
"stattestmodel__int1",
filter=Q(pk__in=[]),
default=Value([]),
)
)
self.assertSequenceEqual(qs.get().test_array_agg, [])
.first() | ||
.test_array_agg | ||
) | ||
self.assertEqual([], result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's preferred to have an expected value as a second argument.
self.assertEqual([], result) | |
self.assertSequenceEqual(result, []) |
test_array_agg=ArrayAgg( | ||
"stattestmodel__int1", | ||
filter=Q(pk__in=[]), | ||
default=Value([]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An optional idea: maybe setup this as a subtest to test the conditions:
filter value:
[-1]
[]
default value:
[]
Value([])
This increases our "black box" coverage. (Simon mentioned that Value(…)
is no longer necessary for defaults and we may want to update the docs to reflect that but it still may be valuable to use for coverage.)
Edit: Clarified conditions as filter value × default value as per Mariusz' setup in the ticket.
I wonder if we should also add a test around eg
|
51f14bf
to
c4c12a0
Compare
c4c12a0
to
80fab95
Compare
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
I think it might be worth it yes just to capture that is it an unexpected behaviour of Also do you know how to trigger a benchmark run on a PR? I figured it might be worth a run to assert our expectations wrt/to My expectations are that since every expression is composed of other expressions that they resolve their output field from and eventually cache them it should have no impact but it might be valuable to confirm prior to merging if it's just a few more CPU cycle away. |
BTW I also just noticed |
I saw that. Initially I figured we could ignore it as it's only used in that module, but as folks always say we don't know how this module gets used out in the wild 😝 A simpler (and possibly more Pythonic?) approach could just be to remove flags altogether and raise a different exception type, and use that as the distinguishing factor as to whether eg
|
Or maybe this is better as it keeps the API consistent - this way it doesn't disrupt folks expecting I know Mariusz doesn't like specific exceptions but this is a practice I find valuable - a practice which I got from old mate Raymond Hettinger in one of his presentations. Anyway though the point I'm making is that "state is the root of all evil" (aside from premature optimisation that is 😁)
|
I like the One small tweak I would make; the whole --- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -166,13 +166,16 @@ class Combinable:
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 can be used as a source expression in Window?
@@ -310,8 +313,9 @@ class BaseExpression:
"""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
@property
@@ -322,9 +326,10 @@ class BaseExpression:
"""
try:
return self.output_field
+ except OutputFieldIsNoneError:
+ return
- except FieldError:
- if not self._output_field_resolved_to_none:
- raise
def _resolve_output_field(self):
""" |
Ah yes of course you're right, one of those forest-trees moments 😂 |
80fab95
to
b148ed7
Compare
@sharonwoo Just need some tests around |
1e346f5
to
407d534
Compare
with self.assertRaisesMessage(FieldError, msg): | ||
Value(object()).output_field | ||
def test_output_field_resolution(self): | ||
# why does this fail, but test_resolve_output_field_with_null pass? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own understanding - to understand the difference between this test and the set of tests containing test_resolve_output_field_with_null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still reading https://code.djangoproject.com/ticket/33397 and #15271 Q__Q
test_output_field_resolution
should see the expression combinations put into the annotation raise OutputFieldIsNoneError
EXCEPT the three handled by db.models.expressions._connector_combinations
, 'IntegerField', 'DecimalField', 'FloatField'.
407d534
to
14d3065
Compare
Sorry, let me come back to this this or next week :( |
https://code.djangoproject.com/ticket/35235
New to Django core here and wanting to try for next djangonaut run, thought I could try to triage by writing some quickie tests to see if I could replicate the issue... and I can't. My 2 additional tests (and all of them, actually) pass locally.
Let me know if this is not a good approach.