Opened 7 hours ago

Last modified 5 hours ago

#36117 new Bug

Composite primary key in Case statement evades right-hand side sanity checking

Reported by: Jacob Walls Owned by:
Component: Database layer (models, ORM) Version: 5.2
Severity: Release blocker Keywords:
Cc: Simon Charette Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

As mentioned on ticket:36042, I figured there might be some more cases where a right-hand side composite expression sneaks through to the database layer and fails there with a syntax error. Found one with Case:

Suggesting to fail at the ORM layer instead of the db, like the other sanity checks. This might be a chance to look into Simon's suggestion to consolidate this logic in BaseExpression, but I'm not certain of the level of effort there.

  • tests/composite_pk/test_filter.py

    diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py
    index 4edf947423..4ff9b5a4ea 100644
    a b  
    1 from django.db.models import F, FilteredRelation, OuterRef, Q, Subquery, TextField
     1from django.db.models import (
     2    Case,
     3    F,
     4    FilteredRelation,
     5    OuterRef,
     6    Q,
     7    Subquery,
     8    TextField,
     9    When,
     10)
    211from django.db.models.functions import Cast
    312from django.db.models.lookups import Exact
    413from django.test import TestCase
    class CompositePKFilterTests(TestCase):  
    6271        with self.assertRaisesMessage(ValueError, msg):
    6372            Comment.objects.filter(text__gt=F("pk")).count()
    6473
     74    def test_rhs_case(self):
     75        msg = "CompositePrimaryKey cannot be used as a lookup value."
     76        with self.assertRaisesMessage(ValueError, msg):
     77            Comment.objects.filter(
     78                text=Case(When(text="", then="pk"), default="pk")
     79            ).count()
     80
    6581    def test_rhs_combinable(self):
    6682        msg = "CompositePrimaryKey is not combinable."
    6783        for expr in [F("pk") + (1, 1), (1, 1) + F("pk")]:

======================================================================
ERROR: test_rhs_case (composite_pk.test_filter.CompositePKFilterTests.test_rhs_case)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/source/django/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/backends/sqlite3/base.py", line 360, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.OperationalError: near ",": syntax error

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/source/django/tests/composite_pk/test_filter.py", line 79, in test_rhs_case
    ).count()
      ^^^^^^^
  File "/Users/source/django/django/db/models/query.py", line 603, in count
    return self.query.get_count(using=self.db)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/models/sql/query.py", line 644, in get_count
    return obj.get_aggregation(using, {"__count": Count("*")})["__count"]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/models/sql/query.py", line 626, in get_aggregation
    result = compiler.execute_sql(SINGLE)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/models/sql/compiler.py", line 1623, in execute_sql
    cursor.execute(sql, params)
  File "/Users/source/django/django/db/backends/utils.py", line 122, in execute
    return super().execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/backends/utils.py", line 79, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/backends/utils.py", line 92, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/backends/utils.py", line 100, in _execute
    with self.db.wrap_database_errors:
  File "/Users/source/django/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/Users/source/django/django/db/backends/utils.py", line 105, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/source/django/django/db/backends/sqlite3/base.py", line 360, in execute
    return super().execute(query, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
django.db.utils.OperationalError: near ",": syntax error

----------------------------------------------------------------------
(0.000) SELECT COUNT(*) AS "__count"
FROM "composite_pk_comment"
WHERE "composite_pk_comment"."text" = (CASE
                                           WHEN "composite_pk_comment"."text" = '' THEN "composite_pk_comment"."tenant_id",
                                                                                        "composite_pk_comment"."comment_id"
                                           ELSE "composite_pk_comment"."tenant_id",
                                                "composite_pk_comment"."comment_id"
                                       END); args=('',); alias=default

----------------------------------------------------------------------
Ran 125 tests in 0.563s

FAILED (errors=1)

Change History (3)

comment:1 by Simon Charette, 6 hours ago

Triage Stage: UnreviewedAccepted

Thanks Jacob, I'll see if I can get the resolve_expression consolidation to pass tests that would effectively allow us to get a lot more of the allows_composite_expressions solution.

comment:2 by Simon Charette, 6 hours ago

Without implementing the full thing it seems that simply removing the Case.resolve_expression implementation (which is basically equivalent to BaseExpression.resolve_expression) passes the tests

  • django/db/models/expressions.py

    diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
    index 2494ec4139..83ace22086 100644
    a b def get_source_expressions(self):  
    16861686    def set_source_expressions(self, exprs):
    16871687        *self.cases, self.default = exprs
    16881688
    1689     def resolve_expression(
    1690         self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
    1691     ):
    1692         c = self.copy()
    1693         c.is_summary = summarize
    1694         for pos, case in enumerate(c.cases):
    1695             c.cases[pos] = case.resolve_expression(
    1696                 query, allow_joins, reuse, summarize, for_save
    1697             )
    1698         c.default = c.default.resolve_expression(
    1699             query, allow_joins, reuse, summarize, for_save
    1700         )
    1701         return c
    1702 
    17031689    def copy(self):
    17041690        c = super().copy()
    17051691        c.cases = c.cases[:]
Version 0, edited 6 hours ago by Simon Charette (next)

comment:3 by Simon Charette, 5 hours ago

Cc: Simon Charette added

Early results of refactoring of each expression subclass not calling into super() is looking promising.

Note: See TracTickets for help on using tickets.
Back to Top