django / query expression / negate

django / query expression / negate

  • Written by
    Walter Doekes
  • Published on

Suppose you have an is_enabled boolean in your Django model.

class Rule(models.Model):
    is_enabled = models.BooleanField(blank=True)
    # other exciting fields here

And now imagine you want to negate the is_enabled values. Something you would easily do in SQL, with: UPDATE myapp_rule SET is_enabled = NOT is_enabled;

The Django F-syntax is nice, and looks like it should be up for the task.

Let’s sum up a couple of attempts:

Rule.objects.update(is_enabled=(not F('is_enabled')))

No! You get this:
UPDATE myapp_rule SET is_enabled = true;

Rule.objects.update(is_enabled=(True ^ F('is_enabled')))

No! You get this:
unsupported operand type(s) for ^: 'bool' and 'F'

And, as you might guess is_enabled=-F('is_enabled') and is_enabled=~F('is_enabled') yield an error similar to the previous one.

If you’re using MySQL, you just might get away with is_enabled=(True - F('is_enabled')). But PostgreSQL will throw an operator does not exist: boolean - boolean error.

Okay. So, on to implementing the not-operator for the ExpressionNode.

No! There is no __not__() operator for object instances. But we can use __invert__() which is called by the ~-operator (bitwise not).

Getting that to work with Django 1.2, is a matter of this:

--- django/db/models/expressions.py.orig 2011-06-08 17:28:54.647385267 +0200
+++ django/db/models/expressions.py 2011-06-08 21:12:47.607603534 +0200
@@ -20,6 +20,9 @@ class ExpressionNode(tree.Node):
     AND = '&'
     OR = '|'

+    # Logical operators
+    NOT = 'NOT' # unary, needs special attention in combine_expression
+
     def __init__(self, children=None, connector=None, negated=False):
         if children is not None and len(children) > 1 and connector is None:
             raise TypeError('You have to specify a connector.')
@@ -48,6 +51,10 @@ class ExpressionNode(tree.Node):
     # OPERATORS #
     #############

+    def __invert__(self):
+        obj = ExpressionNode([self], connector=self.NOT, negated=True)
+        return obj
+
     def __add__(self, other):
         return self._combine(other, self.ADD, False)

--- django/db/backends/__init__.py.orig 2011-06-08 20:59:19.307387242 +0200
+++ django/db/backends/__init__.py 2011-06-08 21:03:19.367604113 +0200
@@ -472,6 +472,9 @@ class BaseDatabaseOperations(object):
         can vary between backends (e.g., Oracle with %% and &) and between
         subexpression types (e.g., date expressions)
         """
+        if connector == 'NOT':
+            assert len(sub_expressions) == 1
+            return 'NOT (%s)' % sub_expressions[0]
         conn = ' %s ' % connector
         return conn.join(sub_expressions)

And now this works like it’s supposed to:

Rule.objects.update(is_enabled=~F('is_enabled'))

On to find out what the Django community thinks the real solution should be ;-)


Back to overview Newer post: executing remote command / ssh / extra escaping Older post: mysql issue warnings / cron