1
0
mirror of https://github.com/django/django.git synced 2025-10-26 07:06:08 +00:00

Fixed #27021 -- Allowed lookup expressions in annotations, aggregations, and QuerySet.filter().

Thanks Hannes Ljungberg and Simon Charette for reviews.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
Ian Foote
2021-04-02 18:25:20 +01:00
committed by Mariusz Felisiak
parent f5dccbafb9
commit f42ccdd835
11 changed files with 268 additions and 48 deletions

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.db import DatabaseError, NotSupportedError
from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.backends.utils import strip_quotes, truncate_name
from django.db.models import AutoField, Exists, ExpressionWrapper
from django.db.models import AutoField, Exists, ExpressionWrapper, Lookup
from django.db.models.expressions import RawSQL
from django.db.models.sql.where import WhereNode
from django.utils import timezone
@@ -202,7 +202,7 @@ END;
# Oracle stores empty strings as null. If the field accepts the empty
# string, undo this to adhere to the Django convention of using
# the empty string instead of null.
if expression.field.empty_strings_allowed:
if expression.output_field.empty_strings_allowed:
converters.append(
self.convert_empty_bytes
if internal_type == 'BinaryField' else
@@ -639,7 +639,7 @@ END;
Oracle supports only EXISTS(...) or filters in the WHERE clause, others
must be compared with True.
"""
if isinstance(expression, (Exists, WhereNode)):
if isinstance(expression, (Exists, Lookup, WhereNode)):
return True
if isinstance(expression, ExpressionWrapper) and expression.conditional:
return self.conditional_expression_supported_in_where_clause(expression.expression)

View File

@@ -1248,9 +1248,9 @@ class OrderBy(Expression):
return (template % placeholders).rstrip(), params
def as_oracle(self, compiler, connection):
# Oracle doesn't allow ORDER BY EXISTS() unless it's wrapped in
# a CASE WHEN.
if isinstance(self.expression, Exists):
# Oracle doesn't allow ORDER BY EXISTS() or filters unless it's wrapped
# in a CASE WHEN.
if connection.ops.conditional_expression_supported_in_where_clause(self.expression):
copy = self.copy()
copy.expression = Case(
When(self.expression, then=True),

View File

@@ -22,6 +22,9 @@ class MultiColSource:
def get_lookup(self, lookup):
return self.output_field.get_lookup(lookup)
def resolve_expression(self, *args, **kwargs):
return self
def get_normalized_value(value, lhs):
from django.db.models import Model

View File

@@ -1,11 +1,10 @@
import itertools
import math
from copy import copy
from django.core.exceptions import EmptyResultSet
from django.db.models.expressions import Case, Func, Value, When
from django.db.models.expressions import Case, Expression, Func, Value, When
from django.db.models.fields import (
CharField, DateTimeField, Field, IntegerField, UUIDField,
BooleanField, CharField, DateTimeField, Field, IntegerField, UUIDField,
)
from django.db.models.query_utils import RegisterLookupMixin
from django.utils.datastructures import OrderedSet
@@ -13,7 +12,7 @@ from django.utils.functional import cached_property
from django.utils.hashable import make_hashable
class Lookup:
class Lookup(Expression):
lookup_name = None
prepare_rhs = True
can_use_none_as_rhs = False
@@ -21,6 +20,7 @@ class Lookup:
def __init__(self, lhs, rhs):
self.lhs, self.rhs = lhs, rhs
self.rhs = self.get_prep_lookup()
self.lhs = self.get_prep_lhs()
if hasattr(self.lhs, 'get_bilateral_transforms'):
bilateral_transforms = self.lhs.get_bilateral_transforms()
else:
@@ -72,12 +72,20 @@ class Lookup:
self.lhs, self.rhs = new_exprs
def get_prep_lookup(self):
if hasattr(self.rhs, 'resolve_expression'):
if not self.prepare_rhs or hasattr(self.rhs, 'resolve_expression'):
return self.rhs
if self.prepare_rhs and hasattr(self.lhs.output_field, 'get_prep_value'):
return self.lhs.output_field.get_prep_value(self.rhs)
if hasattr(self.lhs, 'output_field'):
if hasattr(self.lhs.output_field, 'get_prep_value'):
return self.lhs.output_field.get_prep_value(self.rhs)
elif self.rhs_is_direct_value():
return Value(self.rhs)
return self.rhs
def get_prep_lhs(self):
if hasattr(self.lhs, 'resolve_expression'):
return self.lhs
return Value(self.lhs)
def get_db_prep_lookup(self, value, connection):
return ('%s', [value])
@@ -85,7 +93,11 @@ class Lookup:
lhs = lhs or self.lhs
if hasattr(lhs, 'resolve_expression'):
lhs = lhs.resolve_expression(compiler.query)
return compiler.compile(lhs)
sql, params = compiler.compile(lhs)
if isinstance(lhs, Lookup):
# Wrapped in parentheses to respect operator precedence.
sql = f'({sql})'
return sql, params
def process_rhs(self, compiler, connection):
value = self.rhs
@@ -110,22 +122,12 @@ class Lookup:
def rhs_is_direct_value(self):
return not hasattr(self.rhs, 'as_sql')
def relabeled_clone(self, relabels):
new = copy(self)
new.lhs = new.lhs.relabeled_clone(relabels)
if hasattr(new.rhs, 'relabeled_clone'):
new.rhs = new.rhs.relabeled_clone(relabels)
return new
def get_group_by_cols(self, alias=None):
cols = self.lhs.get_group_by_cols()
if hasattr(self.rhs, 'get_group_by_cols'):
cols.extend(self.rhs.get_group_by_cols())
cols = []
for source in self.get_source_expressions():
cols.extend(source.get_group_by_cols())
return cols
def as_sql(self, compiler, connection):
raise NotImplementedError
def as_oracle(self, compiler, connection):
# Oracle doesn't allow EXISTS() and filters to be compared to another
# expression unless they're wrapped in a CASE WHEN.
@@ -140,16 +142,8 @@ class Lookup:
return lookup.as_sql(compiler, connection)
@cached_property
def contains_aggregate(self):
return self.lhs.contains_aggregate or getattr(self.rhs, 'contains_aggregate', False)
@cached_property
def contains_over_clause(self):
return self.lhs.contains_over_clause or getattr(self.rhs, 'contains_over_clause', False)
@property
def is_summary(self):
return self.lhs.is_summary or getattr(self.rhs, 'is_summary', False)
def output_field(self):
return BooleanField()
@property
def identity(self):
@@ -163,6 +157,21 @@ class Lookup:
def __hash__(self):
return hash(make_hashable(self.identity))
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
c = self.copy()
c.is_summary = summarize
c.lhs = self.lhs.resolve_expression(query, allow_joins, reuse, summarize, for_save)
c.rhs = self.rhs.resolve_expression(query, allow_joins, reuse, summarize, for_save)
return c
def select_format(self, compiler, sql, params):
# Wrap filters with a CASE WHEN expression if a database backend
# (e.g. Oracle) doesn't support boolean expression in SELECT or GROUP
# BY list.
if not compiler.connection.features.supports_boolean_expr_in_select_clause:
sql = f'CASE WHEN {sql} THEN 1 ELSE 0 END'
return sql, params
class Transform(RegisterLookupMixin, Func):
"""

View File

@@ -1262,9 +1262,9 @@ class Query(BaseExpression):
if hasattr(filter_expr, 'resolve_expression'):
if not getattr(filter_expr, 'conditional', False):
raise TypeError('Cannot filter against a non-conditional expression.')
condition = self.build_lookup(
['exact'], filter_expr.resolve_expression(self, allow_joins=allow_joins), True
)
condition = filter_expr.resolve_expression(self, allow_joins=allow_joins)
if not isinstance(condition, Lookup):
condition = self.build_lookup(['exact'], condition, True)
clause = self.where_class()
clause.add(condition, AND)
return clause, []

View File

@@ -208,6 +208,25 @@ class WhereNode(tree.Node):
clone.resolved = True
return clone
@cached_property
def output_field(self):
from django.db.models import BooleanField
return BooleanField()
def select_format(self, compiler, sql, params):
# Wrap filters with a CASE WHEN expression if a database backend
# (e.g. Oracle) doesn't support boolean expression in SELECT or GROUP
# BY list.
if not compiler.connection.features.supports_boolean_expr_in_select_clause:
sql = f'CASE WHEN {sql} THEN 1 ELSE 0 END'
return sql, params
def get_db_converters(self, connection):
return self.output_field.get_db_converters(connection)
def get_lookup(self, lookup):
return self.output_field.get_lookup(lookup)
class NothingNode:
"""A node that matches nothing."""