Refs #28643 -- Added NullIf database function.

Thanks Nick Pope, Mariusz Felisiak, and Tim Graham for reviews.
This commit is contained in:
Mads Jensen 2018-01-14 22:00:16 +01:00 committed by Tim Graham
parent 217f4456d8
commit 4b9d72210f
6 changed files with 78 additions and 3 deletions

View File

@ -1,4 +1,4 @@
from .comparison import Cast, Coalesce, Greatest, Least from .comparison import Cast, Coalesce, Greatest, Least, NullIf
from .datetime import ( from .datetime import (
Extract, ExtractDay, ExtractHour, ExtractIsoYear, ExtractMinute, Extract, ExtractDay, ExtractHour, ExtractIsoYear, ExtractMinute,
ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay,
@ -20,7 +20,7 @@ from .window import (
__all__ = [ __all__ = [
# comparison and conversion # comparison and conversion
'Cast', 'Coalesce', 'Greatest', 'Least', 'Cast', 'Coalesce', 'Greatest', 'Least', 'NullIf',
# datetime # datetime
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth', 'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',

View File

@ -1,5 +1,5 @@
"""Database functions that do comparisons or type conversions.""" """Database functions that do comparisons or type conversions."""
from django.db.models.expressions import Func from django.db.models.expressions import Func, Value
class Cast(Func): class Cast(Func):
@ -103,3 +103,14 @@ class Least(Func):
def as_sqlite(self, compiler, connection, **extra_context): def as_sqlite(self, compiler, connection, **extra_context):
"""Use the MIN function on SQLite.""" """Use the MIN function on SQLite."""
return super().as_sqlite(compiler, connection, function='MIN', **extra_context) return super().as_sqlite(compiler, connection, function='MIN', **extra_context)
class NullIf(Func):
function = 'NULLIF'
arity = 2
def as_oracle(self, compiler, connection, **extra_context):
expression1 = self.get_source_expressions()[0]
if isinstance(expression1, Value) and expression1.value is None:
raise ValueError('Oracle does not allow Value(None) for expression1.')
return super().as_sql(compiler, connection, **extra_context)

View File

@ -899,6 +899,8 @@ occur when an Oracle datatype is used as a column name. In
particular, take care to avoid using the names ``date``, particular, take care to avoid using the names ``date``,
``timestamp``, ``number`` or ``float`` as a field name. ``timestamp``, ``number`` or ``float`` as a field name.
.. _oracle-null-empty-strings:
NULL and empty strings NULL and empty strings
---------------------- ----------------------

View File

@ -149,6 +149,25 @@ will result in a database error.
The PostgreSQL behavior can be emulated using ``Coalesce`` if you know The PostgreSQL behavior can be emulated using ``Coalesce`` if you know
a sensible maximum value to provide as a default. a sensible maximum value to provide as a default.
``NullIf``
----------
.. class:: NullIf(expression1, expression2)
.. versionadded:: 2.2
Accepts two expressions and returns ``None`` if they are equal, otherwise
returns ``expression1``.
.. admonition:: Caveats on Oracle
Due to an :ref:`Oracle convention<oracle-null-empty-strings>`, this
function returns the empty string instead of ``None`` when the expressions
are of type :class:`~django.db.models.CharField`.
Passing ``Value(None)`` to ``expression1`` is prohibited on Oracle since
Oracle doesn't accept ``NULL`` as the first argument.
.. _date-functions: .. _date-functions:
Date functions Date functions

View File

@ -220,6 +220,9 @@ Models
* Added many :ref:`math database functions <math-functions>`. * Added many :ref:`math database functions <math-functions>`.
* The new :class:`~django.db.models.functions.NullIf` database function
returns ``None`` if the two expressions are equal.
* Setting the new ``ignore_conflicts`` parameter of * Setting the new ``ignore_conflicts`` parameter of
:meth:`.QuerySet.bulk_create` to ``True`` tells the database to ignore :meth:`.QuerySet.bulk_create` to ``True`` tells the database to ignore
failure to insert rows that fail uniqueness constraints or other checks. failure to insert rows that fail uniqueness constraints or other checks.

View File

@ -0,0 +1,40 @@
from unittest import skipUnless
from django.db import connection
from django.db.models import Value
from django.db.models.functions import NullIf
from django.test import TestCase
from ..models import Author
class NullIfTests(TestCase):
@classmethod
def setUpTestData(cls):
Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='Rhonda', alias='Rhonda')
def test_basic(self):
authors = Author.objects.annotate(nullif=NullIf('alias', 'name')).values_list('nullif')
self.assertSequenceEqual(
authors, [
('smithj',),
('' if connection.features.interprets_empty_strings_as_nulls else None,)
]
)
def test_null_argument(self):
authors = Author.objects.annotate(nullif=NullIf('name', Value(None))).values_list('nullif')
self.assertSequenceEqual(authors, [('John Smith',), ('Rhonda',)])
def test_too_few_args(self):
msg = "'NullIf' takes exactly 2 arguments (1 given)"
with self.assertRaisesMessage(TypeError, msg):
NullIf('name')
@skipUnless(connection.vendor == 'oracle', 'Oracle specific test for NULL-literal')
def test_null_literal(self):
msg = 'Oracle does not allow Value(None) for expression1.'
with self.assertRaisesMessage(ValueError, msg):
list(Author.objects.annotate(nullif=NullIf(Value(None), 'name')).values_list('nullif'))