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):  
    15621562    template = "CASE %(cases)s ELSE %(default)s END"
    15631563    case_joiner = " "
    15641564
    1565     def __init__(self, *cases, default=None, output_field=None, **extra):
     1565    def __init__(self, *cases, default=None, output_field=None):
    15661566        if not all(isinstance(case, When) for case in cases):
    15671567            raise TypeError("Positional arguments must all be When objects.")
    15681568        super().__init__(output_field)
    15691569        self.cases = list(cases)
    15701570        self.default = self._parse_expressions(default)[0]
    1571         self.extra = extra
    15721571
    15731572    def __str__(self):
    15741573        return "CASE %s, ELSE %r" % (
    class Case(SQLiteNumericMixin, Expression):  
    16101609        connection.ops.check_expression_support(self)
    16111610        if not self.cases:
    16121611            return compiler.compile(self.default)
    1613         template_params = {**self.extra, **extra_context}
     1612        template_params = {**extra_context}
    16141613        case_parts = []
    16151614        sql_params = []
    16161615        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 Sarah Boyce, 2 months ago

Cc: Tim Graham added
Triage Stage: UnreviewedAccepted
Type: BugCleanup/optimization

So I think it's related to #25759 and extra is documented in Func.
We might want a similar note for Subquery and Case and tests are always welcome 👍

comment:2 by amns13, 7 weeks ago

Owner: changed from nobody to amns13
Status: newassigned
Note: See TracTickets for help on using tickets.
Back to Top