Repository: adamchainz/django-mysql

Issue:
Boolean Field for BIT(1) columns
A real world MySQL/django issue: http://stackoverflow.com/questions/2706041/django-orm-dealing-with-mysql-bit1-field

Django should be able to use `BIT(1)` fields as Booleans, as well as migrate them back to `bool`.

Maybe also the general BIT field should be supported.



Discussion:

Resolving patch:
diff --git a/django_mysql/models/fields/__init__.py b/django_mysql/models/fields/__init__.py
--- a/django_mysql/models/fields/__init__.py
+++ b/django_mysql/models/fields/__init__.py
@@ -1,3 +1,4 @@
+from django_mysql.models.fields.bit import *  # NOQA
 from django_mysql.models.fields.lists import *  # NOQA
 from django_mysql.models.fields.sets import *  # NOQA
 from django_mysql.models.fields.sizes import *  # NOQA
diff --git a/django_mysql/models/fields/bit.py b/django_mysql/models/fields/bit.py
new file mode 100644
--- /dev/null
+++ b/django_mysql/models/fields/bit.py
@@ -0,0 +1,42 @@
+# -*- coding:utf-8 -*-
+from django.db.models import BooleanField, NullBooleanField
+from django.utils import six
+
+from django_mysql.shims import field_class
+
+__all__ = ('Bit1BooleanField', 'NullBit1BooleanField',)
+
+
+class Bit1Mixin(object):
+    def db_type(self, connection):
+        return 'bit(1)'
+
+    def to_python(self, value):
+        # Meant to be binary/bytes but can come back as unicode strings
+        if isinstance(value, six.binary_type):
+            value = (value == b'\x01')
+        elif isinstance(value, six.text_type):
+            value = (value == '\x01')
+        return value
+
+    def from_db_value(self, value, expression, connection, context):
+        # Meant to be binary/bytes but can come back as unicode strings
+        if isinstance(value, six.binary_type):
+            value = (value == b'\x01')
+        elif isinstance(value, six.text_type):
+            value = (value == '\x01')
+        return value
+
+    def get_prep_value(self, value):
+        if value is None:
+            return value
+        else:
+            return 1 if value else 0
+
+
+class Bit1BooleanField(field_class(Bit1Mixin, BooleanField)):
+    pass
+
+
+class NullBit1BooleanField(field_class(Bit1Mixin, NullBooleanField)):
+    pass


Test patch:
diff --git a/tests/django_mysql_tests/models.py b/tests/django_mysql_tests/models.py
--- a/tests/django_mysql_tests/models.py
+++ b/tests/django_mysql_tests/models.py
@@ -7,8 +7,9 @@
 from django.utils import timezone
 
 from django_mysql.models import (
-    ListCharField, ListTextField, Model, SetCharField, SetTextField,
-    SizedBinaryField, SizedTextField
+    Bit1BooleanField, ListCharField, ListTextField, Model,
+    NullBit1BooleanField, SetCharField, SetTextField, SizedBinaryField,
+    SizedTextField
 )
 
 
@@ -169,6 +170,15 @@ class SizeFieldModel(Model):
     text4 = SizedTextField(size_class=4)
 
 
+class Bit1Model(Model):
+    flag_a = Bit1BooleanField(default=True)
+    flag_b = Bit1BooleanField(default=False)
+
+
+class NullBit1Model(Model):
+    flag = NullBit1BooleanField()
+
+
 # For cache tests
 
 def expensive_calculation():
diff --git a/tests/django_mysql_tests/test_bit1_field.py b/tests/django_mysql_tests/test_bit1_field.py
new file mode 100644
--- /dev/null
+++ b/tests/django_mysql_tests/test_bit1_field.py
@@ -0,0 +1,174 @@
+# -*- coding:utf-8 -*-
+import json
+
+from django.core import serializers
+from django.db.models import F
+from django.test import TestCase
+
+from django_mysql_tests.models import Bit1Model, NullBit1Model
+
+
+class TestSaveLoad(TestCase):
+    def test_basic(self):
+        m = Bit1Model()
+        m.flag_a = False
+        m.flag_b = True
+        m.save()
+
+        m = Bit1Model.objects.get(id=m.id)
+        assert not m.flag_a
+        assert m.flag_b
+
+        m.save()
+
+        m = Bit1Model.objects.get(id=m.id)
+        assert not m.flag_a
+        assert m.flag_b
+
+        m.flag_a = True
+        m.flag_b = False
+        m.save()
+
+        m = Bit1Model.objects.get(id=m.id)
+        assert m.flag_a
+        assert not m.flag_b
+
+    def test_defaults(self):
+        m = Bit1Model.objects.create()
+        assert m.flag_a
+        assert not m.flag_b
+
+        m = Bit1Model.objects.get(id=m.id)
+        assert m.flag_a
+        assert not m.flag_b
+
+    def test_filter(self):
+        m = Bit1Model.objects.create(flag_a=True, flag_b=True)
+
+        assert list(Bit1Model.objects.filter(flag_a=True)) == [m]
+        assert list(Bit1Model.objects.filter(flag_a=False)) == []
+
+        assert list(Bit1Model.objects.filter(flag_a=F('flag_b'))) == [m]
+        assert list(Bit1Model.objects.exclude(flag_a=F('flag_b'))) == []
+
+        m.flag_a = False
+        m.save()
+
+        assert list(Bit1Model.objects.filter(flag_a=True)) == []
+        assert list(Bit1Model.objects.filter(flag_a=False)) == [m]
+
+        assert list(Bit1Model.objects.filter(flag_a=F('flag_b'))) == []
+        assert list(Bit1Model.objects.exclude(flag_a=F('flag_b'))) == [m]
+
+        Bit1Model.objects.filter(flag_a=False).update(flag_a=True)
+        assert list(Bit1Model.objects.filter(flag_a=True)) == [m]
+        assert list(Bit1Model.objects.filter(flag_a=False)) == []
+
+        Bit1Model.objects.filter(flag_a=True).update(flag_a=False)
+        assert list(Bit1Model.objects.filter(flag_a=True)) == []
+        assert list(Bit1Model.objects.filter(flag_a=False)) == [m]
+
+
+class TestSerialization(TestCase):
+
+    def test_dumping(self):
+        instance = Bit1Model(flag_a=True, flag_b=False)
+        data = json.loads(serializers.serialize('json', [instance]))[0]
+        fields = data['fields']
+        assert fields['flag_a']
+        assert not fields['flag_b']
+
+    def test_loading(self):
+        test_data = '''
+            [{"fields": {"flag_a": false, "flag_b": true},
+              "model": "django_mysql_tests.Bit1Model", "pk": null}]
+        '''
+        objs = list(serializers.deserialize('json', test_data))
+        assert len(objs) == 1
+        instance = objs[0].object
+        assert not instance.flag_a
+        assert instance.flag_b
+
+
+class TestNullSaveLoad(TestCase):
+    def test_basic(self):
+        m = NullBit1Model()
+        assert m.flag is None
+        m.save()
+
+        m = NullBit1Model.objects.get(id=m.id)
+        print(m.flag)
+        print(type(m.flag))
+        assert m.flag is None
+
+        m.flag = True
+        m.save()
+
+        m = NullBit1Model.objects.get(id=m.id)
+        assert m.flag
+
+        m.flag = False
+        m.save()
+
+        m = NullBit1Model.objects.get(id=m.id)
+        assert m.flag is not None and not m.flag
+
+    def test_defaults(self):
+        m = NullBit1Model.objects.create()
+        assert m.flag is None
+        m = NullBit1Model.objects.get(id=m.id)
+        assert m.flag is None
+
+    def test_filter(self):
+        m = NullBit1Model.objects.create()
+
+        assert list(NullBit1Model.objects.filter(flag=None)) == [m]
+        assert list(NullBit1Model.objects.filter(flag=True)) == []
+        assert list(NullBit1Model.objects.filter(flag=False)) == []
+
+        m.flag = True
+        m.save()
+
+        assert list(NullBit1Model.objects.filter(flag=None)) == []
+        assert list(NullBit1Model.objects.filter(flag=True)) == [m]
+        assert list(NullBit1Model.objects.filter(flag=False)) == []
+
+        m.flag = False
+        m.save()
+        assert list(NullBit1Model.objects.filter(flag=None)) == []
+        assert list(NullBit1Model.objects.filter(flag=True)) == []
+        assert list(NullBit1Model.objects.filter(flag=False)) == [m]
+
+        NullBit1Model.objects.filter(flag=False).update(flag=None)
+        assert list(NullBit1Model.objects.filter(flag=None)) == [m]
+        assert list(NullBit1Model.objects.filter(flag=True)) == []
+        assert list(NullBit1Model.objects.filter(flag=False)) == []
+
+        NullBit1Model.objects.filter(flag=None).update(flag=True)
+        assert list(NullBit1Model.objects.filter(flag=None)) == []
+        assert list(NullBit1Model.objects.filter(flag=True)) == [m]
+        assert list(NullBit1Model.objects.filter(flag=False)) == []
+
+        NullBit1Model.objects.filter(flag=True).update(flag=False)
+        assert list(NullBit1Model.objects.filter(flag=None)) == []
+        assert list(NullBit1Model.objects.filter(flag=True)) == []
+        assert list(NullBit1Model.objects.filter(flag=False)) == [m]
+
+
+class TestNullSerialization(TestCase):
+
+    def test_dumping(self):
+        instance = NullBit1Model(flag=None)
+        data = json.loads(serializers.serialize('json', [instance]))[0]
+        fields = data['fields']
+        assert fields['flag'] is None
+
+    def test_loading(self):
+        test_data = '''
+            [{"fields": {"flag": null},
+              "model": "django_mysql_tests.NullBit1Model", "pk": null}]
+        '''
+        objs = list(serializers.deserialize('json', test_data))
+        assert len(objs) == 1
+        instance = objs[0].object
+        assert instance.flag is None