Expression Comparison with Combinable

TL;DR: Allow us to do

queryset.annotate({
   "is_happy": F("expectations") > F("reality")
})

…and the rest of comparison operators (<, <=, >=, ==).


The Combinable class

Provide[s] the ability to combine one or two objects with some connector. For example F(‘foo’) + F(‘bar’).

However, it currently only allows arithmetic operators( + - * / ^ %) and bitwise operators (& | << >> #). Some people have expressed the desire to allow boolean comparison in here. There are workarounds to this, but they’re not neat (a. using ExpressionWrapper, b. using Case and When).

There is a small disadvantage that we have to declare the output field as a BooleanField, but we can include that in the code.

The code would look something like this:

class Combinable:
    ...
    GT = '>'

    def _bool_combine(self, other, connector):
        return ExpressionWrapper(
            self._combine(other, connector, False),
            output_field=models.BooleanField(),
        )

    def __gt__(self, other):
        return self._bool_combine(other, self.GT)
1 Like

you could do it with what arguably is an extra step but …?

queryset.annotate(
    expectation=Value(10), # or actual field value
    reality=Value(5), # or actual field value
    is_happy=Case(
        When(
            expectation__gt=F('reality'),
            then=Value(True),
        ),
        default=Value(False),
        output_field=models.BooleanField()
    )
)

would that work?

Yes of course it’s possible but it looks a bit awkward. I was just wondering why they never put boolean comparisons in the first place since they’re so easy to implement. The only reason I can think of is that we’d have to place it on an expression wrapper to set the output field to boolean, which I did in my code. Is there any other reason not to have them?

1 Like

In addition to being awkward it prevents from creating reusable expressions that you don’t want to have to set up in an annotate function.
For Example:

def ExcelFormula(field_or_expr):
    formula = Concat(Value('="'),field_or_expr,Value('"'))
    return Case(
        When(GreaterThan(Length(field_or_expr), 15),then=formula),
        default=field_or_expr,
        output_field=models.CharField()
    )

I use this all over my code. so it would be very messy to have to set up field_or_expr each time in annotate. Instead I had to create the GreaterThan func:

def ExcelFormula(field_or_expr):
    formula = Concat(Value('="'),field_or_expr,Value('"'))
    return Case(
        When(GreaterThan(Length(field_or_expr), 15),then=formula),
        default=field_or_expr,
        output_field=models.CharField()
    )

While this works, I believe it would be far clearer to use comparison operators.

If I wanted to push this forwards, I’d wrap that in a little packages, open a proof-of-concept PR, with tests, and simple docs and put it on PyPI. It should be quite easy to get community feedback at that point.

(Update: It’s needs to be an edit to the existing class… :woman_facepalming:)

I believe this should work in Django 4.0 and later:

from django.db.models.lookups import GreaterThan

queryset.annotate(is_happy=GreaterThan(F("expectations"), F("reality")))

The big sticking point with supporting the comparison operators is ==, since F already defines __eq__ for a different purpose.

1 Like