]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add IS (NOT) DISTINCT FROM operators
authorSebastian Bank <sebastian.bank@uni-leipzig.de>
Tue, 12 Apr 2016 03:16:32 +0000 (23:16 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 6 Jun 2016 19:53:25 +0000 (15:53 -0400)
None / True / False render as literals.
For SQLite, "IS" is used as SQLite lacks
"IS DISTINCT FROM" but its "IS" operator acts
this way for NULL.

Doctext-author: Mike Bayer <mike_mp@zzzcomputing.com>
Change-Id: I9227b81f7207b42627a0349d14d40b46aa756cce
Pull-request: https://github.com/zzzeek/sqlalchemy/pull/248

doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/default_comparator.py
lib/sqlalchemy/sql/operators.py
test/dialect/test_sqlite.py
test/sql/test_operators.py

index 709eaab5e2b3df9fc55cc86900ffd5dbd6f65dd1..789a241d07122ca49e36aa5bf5aeb7713d03ee01 100644 (file)
 
             :ref:`change_3653`
 
+    .. change::
+        :tags: feature, sql
+
+        New :meth:`.ColumnOperators.is_distinct_from` and
+        :meth:`.ColumnOperators.isnot_distinct_from` operators; pull request
+        courtesy Sebastian Bank.
+
+        .. seealso::
+
+            :ref:`change_is_distinct_from`
+
     .. change::
         :tags: bug, orm
         :tickets: 3488
index b217f0420099d9a5c475761818f83d0b89fbcb77..d9f48fcb1dbdbcea790ca3941cac969ed3c33a2e 100644 (file)
@@ -1137,6 +1137,30 @@ will not have much impact on the behavior of the column during an INSERT.
 
 :ticket:`3216`
 
+.. _change_is_distinct_from:
+
+Support for IS DISTINCT FROM and IS NOT DISTINCT FROM
+------------------------------------------------------
+
+New operators :meth:`.ColumnOperators.is_distinct_from` and
+:meth:`.ColumnOperators.isnot_distinct_from` allow the IS DISTINCT
+FROM and IS NOT DISTINCT FROM sql operation::
+
+    >>> print column('x').is_distinct_from(None)
+    x IS DISTINCT FROM NULL
+
+Handling is provided for NULL, True and False::
+
+    >>> print column('x').isnot_distinct_from(False)
+    x IS NOT DISTINCT FROM false
+
+For SQLite, which doesn't have this operator, "IS" / "IS NOT" is rendered,
+which on SQLite works for NULL unlike other backends::
+
+    >>> from sqlalchemy.dialects import sqlite
+    >>> print column('x').is_distinct_from(None).compile(dialect=sqlite.dialect())
+    x IS NOT NULL
+
 .. _change_1957:
 
 Core and ORM support for FULL OUTER JOIN
index 5109ff3a70048df006e25f1275dd879b853dfcd9..07e4592ba2ebf96f5774cf04cd7f9f701f4cf3f5 100644 (file)
@@ -849,6 +849,14 @@ class SQLiteCompiler(compiler.SQLCompiler):
         # sqlite has no "FOR UPDATE" AFAICT
         return ''
 
+    def visit_is_distinct_from_binary(self, binary, operator, **kw):
+        return "%s IS NOT %s" % (self.process(binary.left),
+                                 self.process(binary.right))
+
+    def visit_isnot_distinct_from_binary(self, binary, operator, **kw):
+        return "%s IS %s" % (self.process(binary.left),
+                             self.process(binary.right))
+
 
 class SQLiteDDLCompiler(compiler.DDLCompiler):
 
index 3d2f02006142d335ecf4e8283f97de6650a89b36..144f2aa473b76bda632b33d7fd10c1ccedc946f7 100644 (file)
@@ -81,6 +81,8 @@ OPERATORS = {
     operators.gt: ' > ',
     operators.ge: ' >= ',
     operators.eq: ' = ',
+    operators.is_distinct_from: ' IS DISTINCT FROM ',
+    operators.isnot_distinct_from: ' IS NOT DISTINCT FROM ',
     operators.concat_op: ' || ',
     operators.match_op: ' MATCH ',
     operators.notmatch_op: ' NOT MATCH ',
index 1bb1c344cc8643c4e0ca4ed1433f2c5858f0f4a7..7630a9821c07cc62b731cd1184a22a12edd70911 100644 (file)
@@ -39,6 +39,12 @@ def _boolean_compare(expr, op, obj, negate=None, reverse=False,
                                     op,
                                     type_=result_type,
                                     negate=negate, modifiers=kwargs)
+        elif op in (operators.is_distinct_from, operators.isnot_distinct_from):
+            return BinaryExpression(expr,
+                                    _literal_as_text(obj),
+                                    op,
+                                    type_=result_type,
+                                    negate=negate, modifiers=kwargs)
         else:
             # all other None/True/False uses IS, IS NOT
             if op in (operators.eq, operators.is_):
@@ -51,8 +57,9 @@ def _boolean_compare(expr, op, obj, negate=None, reverse=False,
                                         negate=operators.is_)
             else:
                 raise exc.ArgumentError(
-                    "Only '=', '!=', 'is_()', 'isnot()' operators can "
-                    "be used with None/True/False")
+                    "Only '=', '!=', 'is_()', 'isnot()', "
+                    "'is_distinct_from()', 'isnot_distinct_from()' "
+                    "operators can be used with None/True/False")
     else:
         obj = _check_literal(expr, op, obj)
 
@@ -249,6 +256,8 @@ operator_lookup = {
     "gt": (_boolean_compare, operators.le),
     "ge": (_boolean_compare, operators.lt),
     "eq": (_boolean_compare, operators.ne),
+    "is_distinct_from": (_boolean_compare, operators.isnot_distinct_from),
+    "isnot_distinct_from": (_boolean_compare, operators.is_distinct_from),
     "like_op": (_boolean_compare, operators.notlike_op),
     "ilike_op": (_boolean_compare, operators.notilike_op),
     "notlike_op": (_boolean_compare, operators.like_op),
index 80f08a97c920288912d7c994f247d5b898c6f213..bf470710db7bce229b71d2869e63a41236558d28 100644 (file)
@@ -311,6 +311,28 @@ class ColumnOperators(Operators):
         """
         return self.operate(ne, other)
 
+    def is_distinct_from(self, other):
+        """Implement the ``IS DISTINCT FROM`` operator.
+
+        Renders "a IS DISTINCT FROM b" on most platforms;
+        on some such as SQLite may render "a IS NOT b".
+
+        .. versionadded:: 1.1
+
+        """
+        return self.operate(is_distinct_from, other)
+
+    def isnot_distinct_from(self, other):
+        """Implement the ``IS NOT DISTINCT FROM`` operator.
+
+        Renders "a IS NOT DISTINCT FROM b" on most platforms;
+        on some such as SQLite may render "a IS b".
+
+        .. versionadded:: 1.1
+
+        """
+        return self.operate(isnot_distinct_from, other)
+
     def __gt__(self, other):
         """Implement the ``>`` operator.
 
@@ -722,6 +744,15 @@ def istrue(a):
 def isfalse(a):
     raise NotImplementedError()
 
+
+def is_distinct_from(a, b):
+    return a.is_distinct_from(b)
+
+
+def isnot_distinct_from(a, b):
+    return a.isnot_distinct_from(b)
+
+
 def is_(a, b):
     return a.is_(b)
 
@@ -931,6 +962,8 @@ _PRECEDENCE = {
 
     eq: 5,
     ne: 5,
+    is_distinct_from: 5,
+    isnot_distinct_from: 5,
     gt: 5,
     lt: 5,
     ge: 5,
index 697f21585178c0a437472a08b2447496f9185e14..473f4f46251b0cb677c88149c523f4d83e53fe79 100644 (file)
@@ -670,6 +670,17 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL):
             "1"
         )
 
+    def test_is_distinct_from(self):
+        self.assert_compile(
+            sql.column('x').is_distinct_from(None),
+            "x IS NOT NULL"
+        )
+
+        self.assert_compile(
+            sql.column('x').isnot_distinct_from(False),
+            "x IS 0"
+        )
+
     def test_localtime(self):
         self.assert_compile(
             func.localtimestamp(),
index 86286a9a3e878e5d36f3fc3528d54954b127a246..5712d8f99e04e2b88579864a1a6f41f72ff4d44e 100644 (file)
@@ -99,6 +99,18 @@ class DefaultColumnComparatorTest(fixtures.TestBase):
     def test_notequals_true(self):
         self._do_operate_test(operators.ne, True)
 
+    def test_is_distinct_from_true(self):
+        self._do_operate_test(operators.is_distinct_from, True)
+
+    def test_is_distinct_from_false(self):
+        self._do_operate_test(operators.is_distinct_from, False)
+
+    def test_is_distinct_from_null(self):
+        self._do_operate_test(operators.is_distinct_from, None)
+
+    def test_isnot_distinct_from_true(self):
+        self._do_operate_test(operators.isnot_distinct_from, True)
+
     def test_is_true(self):
         self._do_operate_test(operators.is_, True)
 
@@ -1527,6 +1539,60 @@ class OperatorAssociativityTest(fixtures.TestBase, testing.AssertsCompiledSQL):
         self.assert_compile(f / (f / (f - f)), "f / (f / (f - f))")
 
 
+class IsDistinctFromTest(fixtures.TestBase, testing.AssertsCompiledSQL):
+    __dialect__ = 'default'
+
+    table1 = table('mytable',
+                   column('myid', Integer),
+                   )
+
+    def test_is_distinct_from(self):
+        self.assert_compile(self.table1.c.myid.is_distinct_from(1),
+                            "mytable.myid IS DISTINCT FROM :myid_1")
+
+    def test_is_distinct_from_sqlite(self):
+        self.assert_compile(self.table1.c.myid.is_distinct_from(1),
+                            "mytable.myid IS NOT ?",
+                            dialect=sqlite.dialect())
+
+    def test_is_distinct_from_postgresql(self):
+        self.assert_compile(self.table1.c.myid.is_distinct_from(1),
+                            "mytable.myid IS DISTINCT FROM %(myid_1)s",
+                            dialect=postgresql.dialect())
+
+    def test_not_is_distinct_from(self):
+        self.assert_compile(~self.table1.c.myid.is_distinct_from(1),
+                            "mytable.myid IS NOT DISTINCT FROM :myid_1")
+
+    def test_not_is_distinct_from_postgresql(self):
+        self.assert_compile(~self.table1.c.myid.is_distinct_from(1),
+                            "mytable.myid IS NOT DISTINCT FROM %(myid_1)s",
+                            dialect=postgresql.dialect())
+
+    def test_isnot_distinct_from(self):
+        self.assert_compile(self.table1.c.myid.isnot_distinct_from(1),
+                            "mytable.myid IS NOT DISTINCT FROM :myid_1")
+
+    def test_isnot_distinct_from_sqlite(self):
+        self.assert_compile(self.table1.c.myid.isnot_distinct_from(1),
+                            "mytable.myid IS ?",
+                            dialect=sqlite.dialect())
+
+    def test_isnot_distinct_from_postgresql(self):
+        self.assert_compile(self.table1.c.myid.isnot_distinct_from(1),
+                            "mytable.myid IS NOT DISTINCT FROM %(myid_1)s",
+                            dialect=postgresql.dialect())
+
+    def test_not_isnot_distinct_from(self):
+        self.assert_compile(~self.table1.c.myid.isnot_distinct_from(1),
+                            "mytable.myid IS DISTINCT FROM :myid_1")
+
+    def test_not_isnot_distinct_from_postgresql(self):
+        self.assert_compile(~self.table1.c.myid.isnot_distinct_from(1),
+                            "mytable.myid IS DISTINCT FROM %(myid_1)s",
+                            dialect=postgresql.dialect())
+
+
 class InTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     __dialect__ = 'default'