Repository: adamchainz/django-mysql

Issue:
Case-sensitive string lookup
MySQL's string comparison is case-insensitive by default ( https://dev.mysql.com/doc/refman/5.5/en/case-sensitivity.html ). None of Django's default lookups allow you to escape this without changing the column to a binary collation ( https://github.com/django/django/blob/master/django/db/backends/mysql/base.py#L201). I imagine a lookup like `field__case_exact=BLA` would be good to implement and add to CharField, TextField, etc.



Discussion:

Resolving patch:
diff --git a/django_mysql/apps.py b/django_mysql/apps.py
--- a/django_mysql/apps.py
+++ b/django_mysql/apps.py
@@ -20,9 +20,13 @@ def perform_monkey_patches(self):
 
     def add_lookups(self):
         from django.db.models import CharField, TextField
-        from django_mysql.models.lookups import Soundex, SoundsLike
+        from django_mysql.models.lookups import (
+            CaseSensitiveExact, Soundex, SoundsLike
+        )
 
+        CharField.register_lookup(CaseSensitiveExact)
         CharField.register_lookup(SoundsLike)
         CharField.register_lookup(Soundex)
+        TextField.register_lookup(CaseSensitiveExact)
         TextField.register_lookup(SoundsLike)
         TextField.register_lookup(Soundex)
diff --git a/django_mysql/models/lookups.py b/django_mysql/models/lookups.py
--- a/django_mysql/models/lookups.py
+++ b/django_mysql/models/lookups.py
@@ -1,5 +1,13 @@
 # -*- coding:utf-8 -*-
 from django.db.models import CharField, Lookup, Transform
+from django.db.models.lookups import BuiltinLookup
+
+
+class CaseSensitiveExact(BuiltinLookup):
+    lookup_name = 'case_exact'
+
+    def get_rhs_op(self, connection, rhs):
+        return '= BINARY %s' % rhs
 
 
 class SoundsLike(Lookup):


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
@@ -3,7 +3,7 @@
 
 from django.db.models import (
     CharField, DateTimeField, DecimalField, ForeignKey, IntegerField,
-    Model as VanillaModel
+    Model as VanillaModel, TextField
 )
 
 from django_mysql.models import (
@@ -75,6 +75,7 @@ class BigIntListModel(Model):
 class Author(Model):
     name = CharField(max_length=32, db_index=True)
     tutor = ForeignKey('self', null=True, related_name='tutees')
+    bio = TextField()
 
     def __unicode__(self):
         return "{} {}".format(self.id, self.name)
diff --git a/tests/django_mysql_tests/test_lookups.py b/tests/django_mysql_tests/test_lookups.py
new file mode 100644
--- /dev/null
+++ b/tests/django_mysql_tests/test_lookups.py
@@ -0,0 +1,58 @@
+# -*- coding:utf-8 -*-
+from django.test import TestCase
+
+from django_mysql_tests.models import Author
+
+
+class CaseExactTests(TestCase):
+
+    def test_charfield(self):
+        dickie = Author.objects.create(name="Dickens")
+
+        authors = Author.objects.filter(name__case_exact="dickens")
+        self.assertEqual(list(authors), [])
+
+        authors = Author.objects.filter(name__case_exact="Dickens")
+        self.assertEqual(list(authors), [dickie])
+
+        authors = Author.objects.filter(name__case_exact="DICKENS")
+        self.assertEqual(list(authors), [])
+
+    def test_charfield_lowercase(self):
+        dickie = Author.objects.create(name="dickens")
+
+        authors = Author.objects.filter(name__case_exact="dickens")
+        self.assertEqual(list(authors), [dickie])
+
+        authors = Author.objects.filter(name__case_exact="Dickens")
+        self.assertEqual(list(authors), [])
+
+    def test_textfield(self):
+        dickie = Author.objects.create(name="Dickens", bio="Aged 10, bald.")
+
+        authors = Author.objects.filter(bio__case_exact="Aged 10, bald.")
+        self.assertEqual(list(authors), [dickie])
+
+        authors = Author.objects.filter(bio__case_exact="Aged 10, BALD.")
+        self.assertEqual(list(authors), [])
+
+
+class SoundexTests(TestCase):
+
+    def test_sounds_like_lookup(self):
+        principles = ["principle", "principal", "princpl"]
+        created = {Author.objects.create(name=name) for name in principles}
+
+        for name in principles:
+            sounding = Author.objects.filter(name__sounds_like=name)
+            self.assertEqual(set(sounding), created)
+
+        sounding = Author.objects.filter(name__sounds_like='')
+        self.assertEqual(set(sounding), set())
+
+        sounding = Author.objects.filter(name__sounds_like='nothing')
+        self.assertEqual(set(sounding), set())
+
+    def test_soundex_strings(self):
+        author = Author.objects.create(name='Robert')
+        self.assertEqual(Author.objects.get(name__soundex='R163'), author)
diff --git a/tests/django_mysql_tests/test_soundex.py b/tests/django_mysql_tests/test_soundex.py
deleted file mode 100644
--- a/tests/django_mysql_tests/test_soundex.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding:utf-8 -*-
-from django.test import TestCase
-
-from django_mysql_tests.models import Author
-
-
-class SoundexTests(TestCase):
-
-    def test_sounds_like_lookup(self):
-        principles = ["principle", "principal", "princpl"]
-        created = {Author.objects.create(name=name) for name in principles}
-
-        for name in principles:
-            sounding = Author.objects.filter(name__sounds_like=name)
-            self.assertEqual(set(sounding), created)
-
-        sounding = Author.objects.filter(name__sounds_like='')
-        self.assertEqual(set(sounding), set())
-
-        sounding = Author.objects.filter(name__sounds_like='nothing')
-        self.assertEqual(set(sounding), set())
-
-    def test_soundex_strings(self):
-        author = Author.objects.create(name='Robert')
-        self.assertEqual(Author.objects.get(name__soundex='R163'), author)