]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed bug where the negation of an EXISTS expression would not
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 21 Mar 2016 14:57:40 +0000 (10:57 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 21 Mar 2016 14:58:08 +0000 (10:58 -0400)
be properly typed as boolean in the result, and also would fail to be
anonymously aliased in a SELECT list as is the case with a
non-negated EXISTS construct.
fixes #3682

(cherry picked from commit 07a4b6cbcda6e6ee6e67893c5a5d2fd01e5f125f)

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/elements.py
test/sql/test_compiler.py
test/sql/test_selectable.py

index defaf452b1f186ab800a60454bb77a26f7e0ba08..07188b7711e97af38c2c0edac99c7b3ed2194739 100644 (file)
 .. changelog::
     :version: 1.0.13
 
+    .. change::
+        :tags: bug, sql
+        :tickets: 3682
+
+        Fixed bug where the negation of an EXISTS expression would not
+        be properly typed as boolean in the result, and also would fail to be
+        anonymously aliased in a SELECT list as is the case with a
+        non-negated EXISTS construct.
+
     .. change::
         :tags: bug, sql
         :tickets: 3666
index 9f54004552eebce82d0ad70ae690b8dd8ca75804..496844d964a11b4e903a8bb9daa7684585161473 100644 (file)
@@ -1572,7 +1572,6 @@ class SQLCompiler(Compiled):
                 select, select._prefixes, **kwargs)
 
         text += self.get_select_precolumns(select, **kwargs)
-
         # the actual list of columns to print in the SELECT column list.
         inner_columns = [
             c for c in [
index 0470edfbae9d2c1156077438a4fee75a4868ab23..124dbdb98c93c7fe033902cc5d5ad658b6921043 100644 (file)
@@ -705,6 +705,9 @@ class ColumnElement(operators.ColumnOperators, ClauseElement):
 
     def _negate(self):
         if self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity:
+            # TODO: see the note in AsBoolean that it seems to assume
+            # the element is the True_() / False_() constant, so this
+            # is too broad
             return AsBoolean(self, operators.isfalse, operators.istrue)
         else:
             return super(ColumnElement, self)._negate()
@@ -2677,6 +2680,13 @@ class UnaryExpression(ColumnElement):
                 modifier=self.modifier,
                 type_=self.type,
                 wraps_column_expression=self.wraps_column_expression)
+        elif self.type._type_affinity is type_api.BOOLEANTYPE._type_affinity:
+            return UnaryExpression(
+                self.self_group(against=operators.inv),
+                operator=operators.inv,
+                type_=type_api.BOOLEANTYPE,
+                wraps_column_expression=self.wraps_column_expression,
+                negate=None)
         else:
             return ClauseElement._negate(self)
 
@@ -2701,6 +2711,9 @@ class AsBoolean(UnaryExpression):
         return self
 
     def _negate(self):
+        # TODO: this assumes the element is the True_() or False_()
+        # object, but this assumption isn't enforced and
+        # ColumnElement._negate() can send any number of expressions here
         return self.element._negate()
 
 
index 8b161686c249907add3d81e28599f224d0f63ea7..24428beefd81afc8bdcb9f228552dc47798a8658 100644 (file)
@@ -637,6 +637,21 @@ class SelectTest(fixtures.TestBase, AssertsCompiledSQL):
             "myothertable.otherid = :otherid_2)) AS anon_1"
         )
 
+        self.assert_compile(
+            select([exists([1])]),
+            "SELECT EXISTS (SELECT 1) AS anon_1"
+        )
+
+        self.assert_compile(
+            select([~exists([1])]),
+            "SELECT NOT (EXISTS (SELECT 1)) AS anon_1"
+        )
+
+        self.assert_compile(
+            select([~(~exists([1]))]),
+            "SELECT NOT (NOT (EXISTS (SELECT 1))) AS anon_1"
+        )
+
     def test_where_subquery(self):
         s = select([addresses.c.street], addresses.c.user_id
                    == users.c.user_id, correlate=True).alias('s')
index 3390f4a771acc560d67cb5fda78d58755bbf957a..9d714164b915d7d4068035f369d1d0c45a0fca76 100644 (file)
@@ -2193,6 +2193,33 @@ class ResultMapTest(fixtures.TestBase):
             [Boolean]
         )
 
+    def test_plain_exists(self):
+        expr = exists([1])
+        eq_(type(expr.type), Boolean)
+        eq_(
+            [type(entry[-1]) for
+             entry in select([expr]).compile()._result_columns],
+            [Boolean]
+        )
+
+    def test_plain_exists_negate(self):
+        expr = ~exists([1])
+        eq_(type(expr.type), Boolean)
+        eq_(
+            [type(entry[-1]) for
+             entry in select([expr]).compile()._result_columns],
+            [Boolean]
+        )
+
+    def test_plain_exists_double_negate(self):
+        expr = ~(~exists([1]))
+        eq_(type(expr.type), Boolean)
+        eq_(
+            [type(entry[-1]) for
+             entry in select([expr]).compile()._result_columns],
+            [Boolean]
+        )
+
     def test_column_subquery_plain(self):
         t = self._fixture()
         s1 = select([t.c.x]).where(t.c.x > 5).as_scalar()