Refs #25367 -- Moved Oracle Exists() handling to contextual methods.

Oracle requires the EXISTS expression to be wrapped in a CASE WHEN in
the following cases.

1. When part of a SELECT clause.
2. When part of a ORDER BY clause.
3. When compared against another expression in the WHERE clause.

This commit moves the systematic CASE WHEN wrapping of Exists.as_oracle
to contextual .select_format, Lookup.as_oracle, and OrderBy.as_oracle
methods in order to avoid unnecessary wrapping.
This commit is contained in:
Simon Charette 2019-08-11 02:12:11 -04:00 committed by Mariusz Felisiak
parent fff5186d32
commit efa1908f66
4 changed files with 40 additions and 7 deletions

View File

@ -288,6 +288,9 @@ class BaseDatabaseFeatures:
# field(s)? # field(s)?
allows_multiple_constraints_on_same_fields = True allows_multiple_constraints_on_same_fields = True
# Does the backend support boolean expressions in the SELECT clause?
supports_boolean_expr_in_select_clause = True
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection

View File

@ -58,3 +58,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_partial_indexes = False supports_partial_indexes = False
supports_slicing_ordering_in_compound = True supports_slicing_ordering_in_compound = True
allows_multiple_constraints_on_same_fields = False allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False

View File

@ -1075,12 +1075,11 @@ class Exists(Subquery):
sql = 'NOT {}'.format(sql) sql = 'NOT {}'.format(sql)
return sql, params return sql, params
def as_oracle(self, compiler, connection, template=None, **extra_context): def select_format(self, compiler, sql, params):
# Oracle doesn't allow EXISTS() in the SELECT list, so wrap it with a # Wrap EXISTS() with a CASE WHEN expression if a database backend
# CASE WHEN expression. Change the template since the When expression # (e.g. Oracle) doesn't support boolean expression in the SELECT list.
# requires a left hand side (column) to compare against. if not compiler.connection.features.supports_boolean_expr_in_select_clause:
sql, params = self.as_sql(compiler, connection, template, **extra_context) sql = 'CASE WHEN {} THEN 1 ELSE 0 END'.format(sql)
sql = 'CASE WHEN {} THEN 1 ELSE 0 END'.format(sql)
return sql, params return sql, params
@ -1140,6 +1139,19 @@ class OrderBy(BaseExpression):
template = 'IF(ISNULL(%(expression)s),0,1), %(expression)s %(ordering)s ' template = 'IF(ISNULL(%(expression)s),0,1), %(expression)s %(ordering)s '
return self.as_sql(compiler, connection, template=template) return self.as_sql(compiler, connection, template=template)
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):
copy = self.copy()
# XXX: Use Case(When(self.lhs)) once support for boolean
# expressions is added to When.
exists_sql, params = compiler.compile(self.expression)
case_sql = 'CASE WHEN %s THEN 1 ELSE 0 END' % exists_sql
copy.expression = RawSQL(case_sql, params)
return copy.as_sql(compiler, connection)
return self.as_sql(compiler, connection)
def get_group_by_cols(self, alias=None): def get_group_by_cols(self, alias=None):
cols = [] cols = []
for source in self.get_source_expressions(): for source in self.get_source_expressions():

View File

@ -3,7 +3,7 @@ import math
from copy import copy from copy import copy
from django.core.exceptions import EmptyResultSet from django.core.exceptions import EmptyResultSet
from django.db.models.expressions import Func, Value from django.db.models.expressions import Exists, Func, RawSQL, Value
from django.db.models.fields import DateTimeField, Field, IntegerField from django.db.models.fields import DateTimeField, Field, IntegerField
from django.db.models.query_utils import RegisterLookupMixin from django.db.models.query_utils import RegisterLookupMixin
from django.utils.datastructures import OrderedSet from django.utils.datastructures import OrderedSet
@ -112,6 +112,23 @@ class Lookup:
def as_sql(self, compiler, connection): def as_sql(self, compiler, connection):
raise NotImplementedError raise NotImplementedError
def as_oracle(self, compiler, connection):
# Oracle doesn't allow EXISTS() to be compared to another expression
# unless it's wrapped in a CASE WHEN.
wrapped = False
exprs = []
for expr in (self.lhs, self.rhs):
if isinstance(expr, Exists):
# XXX: Use Case(When(self.lhs)) once support for boolean
# expressions is added to When.
sql, params = compiler.compile(expr)
sql = 'CASE WHEN %s THEN 1 ELSE 0 END' % sql
expr = RawSQL(sql, params)
wrapped = True
exprs.append(expr)
lookup = type(self)(*exprs) if wrapped else self
return lookup.as_sql(compiler, connection)
@cached_property @cached_property
def contains_aggregate(self): def contains_aggregate(self):
return self.lhs.contains_aggregate or getattr(self.rhs, 'contains_aggregate', False) return self.lhs.contains_aggregate or getattr(self.rhs, 'contains_aggregate', False)