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 1 from django.db.models import ( 2 Case, 3 F, 4 FilteredRelation, 5 OuterRef, 6 Q, 7 Subquery, 8 TextField, 9 When, 10 ) 2 11 from django.db.models.functions import Cast 3 12 from django.db.models.lookups import Exact 4 13 from django.test import TestCase … … class CompositePKFilterTests(TestCase): 62 71 with self.assertRaisesMessage(ValueError, msg): 63 72 Comment.objects.filter(text__gt=F("pk")).count() 64 73 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 65 81 def test_rhs_combinable(self): 66 82 msg = "CompositePrimaryKey is not combinable." 67 83 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 , 6 hours ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:2 by , 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): 1686 1686 def set_source_expressions(self, exprs): 1687 1687 *self.cases, self.default = exprs 1688 1688 1689 def resolve_expression(1690 self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False1691 ):1692 c = self.copy()1693 c.is_summary = summarize1694 for pos, case in enumerate(c.cases):1695 c.cases[pos] = case.resolve_expression(1696 query, allow_joins, reuse, summarize, for_save1697 )1698 c.default = c.default.resolve_expression(1699 query, allow_joins, reuse, summarize, for_save1700 )1701 return c1702 1703 1689 def copy(self): 1704 1690 c = super().copy() 1705 1691 c.cases = c.cases[:]
comment:3 by , 5 hours ago
Cc: | added |
---|
Early results of refactoring of each expression subclass not calling into super()
is looking promising.
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 theallows_composite_expressions
solution.