Repository: adamchainz/django-mysql

Issue:
Support SQL If()
Summary: MySQL has a handy `If()` statement, it's shorter than doing the whole `Case(When(` malarkey, we should support it.



Discussion:

Resolving patch:
diff --git a/django_mysql/compat.py b/django_mysql/compat.py
--- a/django_mysql/compat.py
+++ b/django_mysql/compat.py
@@ -8,7 +8,7 @@
 import django
 from django.utils import six
 
-__all__ = ('BaseExpression', 'Func', 'Value', 'field_class')
+__all__ = ('BaseExpression', 'Expression', 'Func', 'Value', 'field_class')
 
 
 # Handle the deprecation of SubfieldBase
@@ -31,10 +31,14 @@ class BaseExpression(object):
         def __init__(self, *args, **kwargs):
             raise ValueError("Database Expessions only exist in Django 1.8+")
 
+    Expression = BaseExpression
+
     class Func(object):
         def __init__(self, *args, **kwargs):
             raise ValueError("Database Functions only exist in Django 1.8+")
 
     Value = tuple
 else:
-    from django.db.models.expressions import BaseExpression, Func, Value
+    from django.db.models.expressions import (
+        BaseExpression, Expression, Func, Value
+    )
diff --git a/django_mysql/models/functions.py b/django_mysql/models/functions.py
--- a/django_mysql/models/functions.py
+++ b/django_mysql/models/functions.py
@@ -1,4 +1,5 @@
 from django.db import DEFAULT_DB_ALIAS, connections
+from django.db.models import Field as DjangoField
 from django.db.models import CharField, IntegerField, TextField
 
 from django_mysql.compat import Func, Value
@@ -30,6 +31,21 @@ class Least(MultiArgFunc):
     function = 'LEAST'
 
 
+# Control Flow Functions
+
+
+class If(Func):
+    function = 'IF'
+
+    def __init__(self, condition, true, false=None, output_field=None):
+        if output_field is None:
+            # Workaround for some ORM weirdness
+            output_field = DjangoField()
+
+        super(If, self).__init__(condition, true, false,
+                                 output_field=output_field)
+
+
 # Numeric Functions
 
 


Test patch:
diff --git a/tests/testapp/test_functions.py b/tests/testapp/test_functions.py
--- a/tests/testapp/test_functions.py
+++ b/tests/testapp/test_functions.py
@@ -5,14 +5,14 @@
 import django
 import pytest
 from django.db import connection
-from django.db.models import F
+from django.db.models import F, Q
 from django.test import TestCase
 from django.utils import six
 
 from django_mysql.compat import Value
 from django_mysql.models.functions import (
     CRC32, ELT, MD5, SHA1, SHA2, Abs, AsType, Ceiling, ColumnAdd, ColumnDelete,
-    ColumnGet, ConcatWS, Field, Floor, Greatest, JSONExtract, JSONKeys,
+    ColumnGet, ConcatWS, Field, Floor, Greatest, If, JSONExtract, JSONKeys,
     JSONLength, LastInsertId, Least, RegexpInstr, RegexpReplace, RegexpSubstr,
     Round, Sign, UpdateXML, XMLExtractValue
 )
@@ -21,9 +21,9 @@
 from testapp.test_jsonfield import JSONFieldTestCase
 
 try:
-    from django.db.models.functions import Length
+    from django.db.models.functions import Length, Lower, Upper
 except ImportError:
-    Length = None
+    Length = Lower = Upper = None
 
 requiresDatabaseFunctions = skipIf(
     django.VERSION[:2] < (1, 8),
@@ -49,6 +49,63 @@ def test_least(self):
         assert ab.worst == -1
 
 
+@requiresDatabaseFunctions
+class ControlFlowFunctionTests(TestCase):
+
+    def test_if_basic(self):
+        Alphabet.objects.create(d='String')
+        Alphabet.objects.create(d='')
+        Alphabet.objects.create(d='String')
+        Alphabet.objects.create(d='')
+
+        results = list(Alphabet.objects.annotate(
+            has_d=If(Length('d'), Value(True), Value(False))
+        ).order_by('id').values_list('has_d', flat=True))
+        assert results == [True, False, True, False]
+
+    def test_if_with_Q(self):
+        Alphabet.objects.create(a=12, b=17)
+        Alphabet.objects.create(a=13, b=17)
+        Alphabet.objects.create(a=14, b=17)
+
+        result = list(Alphabet.objects.annotate(
+            conditional=If(Q(a__lte=13), 'a', 'b')
+        ).order_by('id').values_list('conditional', flat=True))
+        assert result == [12, 13, 17]
+
+    def test_if_with_string_values(self):
+        Alphabet.objects.create(a=1, d='Lentils')
+        Alphabet.objects.create(a=2, d='Cabbage')
+        Alphabet.objects.create(a=3, d='Rice')
+
+        result = list(Alphabet.objects.annotate(
+            conditional=If(Q(a=2), Upper('d'), Lower('d'))
+        ).order_by('id').values_list('conditional', flat=True))
+        assert result == ['lentils', 'CABBAGE', 'rice']
+
+    def test_if_field_lookups_work(self):
+        Alphabet.objects.create(a=1, d='Lentils')
+        Alphabet.objects.create(a=2, d='Cabbage')
+        Alphabet.objects.create(a=3, d='Rice')
+
+        result = list(Alphabet.objects.annotate(
+            conditional=If(Q(a__gte=2), Upper('d'), Value(''))
+        ).filter(
+            conditional__startswith='C'
+        ).order_by('id').values_list('conditional', flat=True))
+        assert result == ['CABBAGE']
+
+    def test_if_false_default_None(self):
+        Alphabet.objects.create(a=1)
+
+        result = list(Alphabet.objects.annotate(
+            conditional=If(Q(a=2), Value(1))
+        ).filter(
+            conditional__isnull=True
+        ).values_list('conditional', flat=True))
+        assert result == [None]
+
+
 @requiresDatabaseFunctions
 class NumericFunctionTests(TestCase):