Fixed #16211 -- Added logical NOT support to F expressions.

This commit is contained in:
David Wobrock 2022-09-26 22:59:25 +02:00 committed by Mariusz Felisiak
parent c01e76c95c
commit a320aab512
7 changed files with 164 additions and 29 deletions

View file

@ -162,6 +162,9 @@ class Combinable:
"Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations."
)
def __invert__(self):
return NegatedExpression(self)
class BaseExpression:
"""Base class for all query expressions."""
@ -827,6 +830,9 @@ class F(Combinable):
def __hash__(self):
return hash(self.name)
def copy(self):
return copy.copy(self)
class ResolvedOuterRef(F):
"""
@ -1252,6 +1258,57 @@ class ExpressionWrapper(SQLiteNumericMixin, Expression):
return "{}({})".format(self.__class__.__name__, self.expression)
class NegatedExpression(ExpressionWrapper):
"""The logical negation of a conditional expression."""
def __init__(self, expression):
super().__init__(expression, output_field=fields.BooleanField())
def __invert__(self):
return self.expression.copy()
def as_sql(self, compiler, connection):
try:
sql, params = super().as_sql(compiler, connection)
except EmptyResultSet:
features = compiler.connection.features
if not features.supports_boolean_expr_in_select_clause:
return "1=1", ()
return compiler.compile(Value(True))
ops = compiler.connection.ops
# Some database backends (e.g. Oracle) don't allow EXISTS() and filters
# to be compared to another expression unless they're wrapped in a CASE
# WHEN.
if not ops.conditional_expression_supported_in_where_clause(self.expression):
return f"CASE WHEN {sql} = 0 THEN 1 ELSE 0 END", params
return f"NOT {sql}", params
def resolve_expression(
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
resolved = super().resolve_expression(
query, allow_joins, reuse, summarize, for_save
)
if not getattr(resolved.expression, "conditional", False):
raise TypeError("Cannot negate non-conditional expressions.")
return resolved
def select_format(self, compiler, sql, params):
# Wrap boolean expressions with a CASE WHEN expression if a database
# backend (e.g. Oracle) doesn't support boolean expression in SELECT or
# GROUP BY list.
expression_supported_in_where_clause = (
compiler.connection.ops.conditional_expression_supported_in_where_clause
)
if (
not compiler.connection.features.supports_boolean_expr_in_select_clause
# Avoid double wrapping.
and expression_supported_in_where_clause(self.expression)
):
sql = "CASE WHEN {} THEN 1 ELSE 0 END".format(sql)
return sql, params
@deconstructible(path="django.db.models.When")
class When(Expression):
template = "WHEN %(condition)s THEN %(result)s"
@ -1486,34 +1543,10 @@ class Exists(Subquery):
template = "EXISTS(%(subquery)s)"
output_field = fields.BooleanField()
def __init__(self, queryset, negated=False, **kwargs):
self.negated = negated
def __init__(self, queryset, **kwargs):
super().__init__(queryset, **kwargs)
self.query = self.query.exists()
def __invert__(self):
clone = self.copy()
clone.negated = not self.negated
return clone
def as_sql(self, compiler, connection, **extra_context):
try:
sql, params = super().as_sql(
compiler,
connection,
**extra_context,
)
except EmptyResultSet:
if self.negated:
features = compiler.connection.features
if not features.supports_boolean_expr_in_select_clause:
return "1=1", ()
return compiler.compile(Value(True))
raise
if self.negated:
sql = "NOT {}".format(sql)
return sql, params
def select_format(self, compiler, sql, params):
# Wrap EXISTS() with a CASE WHEN expression if a database backend
# (e.g. Oracle) doesn't support boolean expression in SELECT or GROUP