Repository: adamchainz/django-mysql

Issue:
Other aggregate functions
Gotta keep up with the joneses of `django.contrib.postgres`! On django master it now has all the aggregate functions, including `BitOr`, `BitAnd`, etc. - https://docs.djangoproject.com/en/dev/ref/contrib/postgres/aggregates/

Of course, MySQL has many others too... https://mariadb.com/kb/en/mariadb/functions-and-modifiers-for-use-with-group-by/



Discussion:

Resolving patch:
diff --git a/django_mysql/models/aggregates.py b/django_mysql/models/aggregates.py
--- a/django_mysql/models/aggregates.py
+++ b/django_mysql/models/aggregates.py
@@ -1,8 +1,73 @@
+import django
 from django.db.models import Aggregate, CharField
 from django.db.models.sql.aggregates import Aggregate as SQLAggregate
 from django.utils.functional import cached_property
 
-__all__ = ('GroupConcat',)
+__all__ = ('BitAnd', 'BitOr', 'BitXor', 'GroupConcat',)
+
+# Major aggregate simplification from 1.7 to 1.8 - it's easier to implement
+# each twice than try fudge a class that works before and after
+
+if django.VERSION < (1, 8):
+
+    class BitAnd(Aggregate):
+        name = 'BitAnd'
+
+        def add_to_query(self, query, alias, col, source, is_summary):
+            query.aggregates[alias] = BitAndSQL(
+                col,
+                source=source,
+                is_summary=is_summary,
+                **self.extra
+            )
+
+    class BitAndSQL(SQLAggregate):
+        sql_function = 'BIT_AND'
+        is_ordinal = True  # is an integer
+
+    class BitOr(Aggregate):
+        name = 'BitOr'
+
+        def add_to_query(self, query, alias, col, source, is_summary):
+            query.aggregates[alias] = BitOrSQL(
+                col,
+                source=source,
+                is_summary=is_summary,
+                **self.extra
+            )
+
+    class BitOrSQL(SQLAggregate):
+        sql_function = 'BIT_OR'
+        is_ordinal = True  # is an integer
+
+    class BitXor(Aggregate):
+        name = 'BitXor'
+
+        def add_to_query(self, query, alias, col, source, is_summary):
+            query.aggregates[alias] = BitXorSQL(
+                col,
+                source=source,
+                is_summary=is_summary,
+                **self.extra
+            )
+
+    class BitXorSQL(SQLAggregate):
+        sql_function = 'BIT_XOR'
+        is_ordinal = True  # is an integer
+
+else:
+
+    class BitAnd(Aggregate):
+        function = 'BIT_AND'
+        name = 'bitand'
+
+    class BitOr(Aggregate):
+        function = 'BIT_OR'
+        name = 'bitor'
+
+    class BitXor(Aggregate):
+        function = 'BIT_XOR'
+        name = 'bitxor'
 
 
 class GroupConcat(Aggregate):


Test patch:
diff --git a/tests/django_mysql_tests/test_aggregates.py b/tests/django_mysql_tests/test_aggregates.py
--- a/tests/django_mysql_tests/test_aggregates.py
+++ b/tests/django_mysql_tests/test_aggregates.py
@@ -1,8 +1,67 @@
 # -*- coding:utf-8 -*-
 from django.test import TestCase
 
-from django_mysql.models import GroupConcat
-from django_mysql_tests.models import Author
+from django_mysql.models import BitAnd, BitOr, BitXor, GroupConcat
+from django_mysql_tests.models import Alphabet, Author
+
+
+class BitAndTests(TestCase):
+
+    def test_implicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=29), Alphabet(a=15)])
+        out = Alphabet.objects.all().aggregate(BitAnd('a'))
+        self.assertEqual(out, {'a__bitand': 13})
+
+    def test_explicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=11), Alphabet(a=24)])
+        out = Alphabet.objects.all().aggregate(aaa=BitAnd('a'))
+        self.assertEqual(out, {'aaa': 8})
+
+    def test_no_rows(self):
+        out = Alphabet.objects.all().aggregate(BitAnd('a'))
+        # Manual:
+        # "This function returns 18446744073709551615 if there were no matching
+        # rows. (This is the value of an unsigned BIGINT value with all bits
+        # set to 1.)"
+        self.assertEqual(out, {'a__bitand': 18446744073709551615})
+
+
+class BitOrTests(TestCase):
+
+    def test_implicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=1), Alphabet(a=84)])
+        out = Alphabet.objects.all().aggregate(BitOr('a'))
+        self.assertEqual(out, {'a__bitor': 85})
+
+    def test_explicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=29), Alphabet(a=15)])
+        out = Alphabet.objects.all().aggregate(aaa=BitOr('a'))
+        self.assertEqual(out, {'aaa': 31})
+
+    def test_no_rows(self):
+        out = Alphabet.objects.all().aggregate(BitOr('a'))
+        # Manual:
+        # "This function returns 0 if there were no matching rows."
+        self.assertEqual(out, {'a__bitor': 0})
+
+
+class BitXorTests(TestCase):
+
+    def test_implicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=11), Alphabet(a=3)])
+        out = Alphabet.objects.all().aggregate(BitXor('a'))
+        self.assertEqual(out, {'a__bitxor': 8})
+
+    def test_explicit_name(self):
+        Alphabet.objects.bulk_create([Alphabet(a=123), Alphabet(a=456)])
+        out = Alphabet.objects.all().aggregate(aaa=BitXor('a'))
+        self.assertEqual(out, {'aaa': 435})
+
+    def test_no_rows(self):
+        out = Alphabet.objects.all().aggregate(BitXor('a'))
+        # Manual:
+        # "This function returns 0 if there were no matching rows."
+        self.assertEqual(out, {'a__bitxor': 0})
 
 
 class GroupConcatTests(TestCase):