Fixed #32673 -- Fixed lookups crash when comparing against lookups on PostgreSQL.

Regression in 3a505c70e7.

Nonlitteral right-hand-sides of lookups need to be wrapped in
parentheses to avoid operator precedence ambiguities.

Thanks Charles Lirsac for the detailed report.
This commit is contained in:
Simon Charette 2021-04-22 13:44:25 -04:00 committed by Mariusz Felisiak
parent 67bb1f516c
commit 170b006ce8
4 changed files with 34 additions and 3 deletions

View File

@ -87,6 +87,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
'Raises ORA-00600: internal error code.': {
'model_fields.test_jsonfield.TestQuerying.test_usage_in_subquery',
},
"Oracle doesn't allow filters to be compared to another expression in "
"the WHERE clause.": {
'lookup.tests.LookupTests.test_lookup_rhs',
},
}
django_test_expected_failures = {
# A bug in Django/cx_Oracle with respect to string handling (#23843).

View File

@ -94,7 +94,13 @@ class Lookup:
value = self.apply_bilateral_transforms(value)
value = value.resolve_expression(compiler.query)
if hasattr(value, 'as_sql'):
return compiler.compile(value)
sql, params = compiler.compile(value)
# Ensure expression is wrapped in parentheses to respect operator
# precedence but avoid double wrapping as it can be misinterpreted
# on some backends (e.g. subqueries on SQLite).
if sql and sql[0] != '(':
sql = '(%s)' % sql
return sql, params
else:
return self.get_db_prep_lookup(value, connection)

View File

@ -94,6 +94,7 @@ class Product(models.Model):
class Stock(models.Model):
product = models.ForeignKey(Product, models.CASCADE)
short = models.BooleanField(default=False)
qty_available = models.DecimalField(max_digits=6, decimal_places=2)

View File

@ -5,13 +5,16 @@ from operator import attrgetter
from django.core.exceptions import FieldError
from django.db import connection, models
from django.db.models import Exists, Max, OuterRef
from django.db.models import (
BooleanField, Exists, ExpressionWrapper, F, Max, OuterRef, Q,
)
from django.db.models.functions import Substr
from django.test import TestCase, skipUnlessDBFeature
from django.test.utils import isolate_apps
from .models import (
Article, Author, Freebie, Game, IsNullWithNoneAsRHS, Player, Season, Tag,
Article, Author, Freebie, Game, IsNullWithNoneAsRHS, Player, Product,
Season, Stock, Tag,
)
@ -1000,3 +1003,20 @@ class LookupTests(TestCase):
with self.subTest(qs=qs):
with self.assertRaisesMessage(ValueError, msg):
qs.exists()
def test_lookup_rhs(self):
product = Product.objects.create(name='GME', qty_target=5000)
stock_1 = Stock.objects.create(product=product, short=True, qty_available=180)
stock_2 = Stock.objects.create(product=product, short=False, qty_available=5100)
Stock.objects.create(product=product, short=False, qty_available=4000)
self.assertCountEqual(
Stock.objects.filter(short=Q(qty_available__lt=F('product__qty_target'))),
[stock_1, stock_2],
)
self.assertCountEqual(
Stock.objects.filter(short=ExpressionWrapper(
Q(qty_available__lt=F('product__qty_target')),
output_field=BooleanField(),
)),
[stock_1, stock_2],
)