Opened 2 months ago
Last modified 7 weeks ago
#35459 assigned Cleanup/optimization
Case.extra is undocumented, untested, and can hide potential issues
Reported by: | Baptiste Mispelon | Owned by: | amns13 |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.0 |
Severity: | Normal | Keywords: | |
Cc: | Tim Graham | Triage Stage: | Accepted |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I came across this today while accidentally doing something like the following:
MyModel.objects.annotate(x=Case(When(somefield=123), then=456, default=789))
The query ran without errors and produced results but they were slightly wrong. It took me longer than I'd like to admit to realize that I should have written:
MyModel.objects.annotate(x=Case(When(somefield=123, then=456), default=789))
(in case you hadn't seen the difference, the then
kwarg was passed to Case
instead of When
).
Once I figured out what was going on, I was surprised that Django had accepted the then
argument for Case
. I would have expected a TypeError
.
The documentation [1] does mention that the signature is class Case(*cases, **extra)
, but doesn't explain what happens to **extra
, and even later refers to "the default keyword argument" which seems inconsistent.
To top it all of, it seems that the feature is untested. I removed **extra
from Case.__init__
and Case.as_sql()
, ran the full test suite (under sqlite), and got zero errors:
-
django/db/models/expressions.py
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 4ee22420d9..34308fad9c 100644
a b class Case(SQLiteNumericMixin, Expression): 1562 1562 template = "CASE %(cases)s ELSE %(default)s END" 1563 1563 case_joiner = " " 1564 1564 1565 def __init__(self, *cases, default=None, output_field=None , **extra):1565 def __init__(self, *cases, default=None, output_field=None): 1566 1566 if not all(isinstance(case, When) for case in cases): 1567 1567 raise TypeError("Positional arguments must all be When objects.") 1568 1568 super().__init__(output_field) 1569 1569 self.cases = list(cases) 1570 1570 self.default = self._parse_expressions(default)[0] 1571 self.extra = extra1572 1571 1573 1572 def __str__(self): 1574 1573 return "CASE %s, ELSE %r" % ( … … class Case(SQLiteNumericMixin, Expression): 1610 1609 connection.ops.check_expression_support(self) 1611 1610 if not self.cases: 1612 1611 return compiler.compile(self.default) 1613 template_params = {** self.extra, **extra_context}1612 template_params = {**extra_context} 1614 1613 case_parts = [] 1615 1614 sql_params = [] 1616 1615 default_sql, default_params = compiler.compile(self.default)
As far as I can tell, this feature has been present since Case
was introduced in #24031 (65246de7b1d70d25831ab394c4f4a75813f629fe).
I would be tempted to drop it considering the way it silently swallows unknown kwargs. Am I missing something?
[1] https://docs.djangoproject.com/en/dev/ref/models/conditional-expressions/#case
Change History (2)
comment:1 by , 2 months ago
Cc: | added |
---|---|
Triage Stage: | Unreviewed → Accepted |
Type: | Bug → Cleanup/optimization |
comment:2 by , 7 weeks ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
So I think it's related to #25759 and
extra
is documented in Func.We might want a similar note for
Subquery
andCase
and tests are always welcome 👍