]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
propagate _scalar_type() for SelectStatementGrouping
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Nov 2025 20:10:05 +0000 (15:10 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 18 Nov 2025 20:11:16 +0000 (15:11 -0500)
Fixed issue where using the :meth:`.ColumnOperators.in_` operator with a
nested :class:`.CompoundSelect` statement (e.g. an ``INTERSECT`` of
``UNION`` queries) would raise a :class:`NotImplementedError` when the
nested compound select was the first argument to the outer compound select.
The ``_scalar_type()`` internal method now properly handles nested compound
selects.

Fixes: #12987
Change-Id: I6aa1b38863588d371bbac74b3531b99ccd5fcaec
(cherry picked from commit 42710c9220f897487710424981b81a69a7da5def)

doc/build/changelog/unreleased_20/12987.rst [new file with mode: 0644]
lib/sqlalchemy/sql/selectable.py
test/sql/test_operators.py

diff --git a/doc/build/changelog/unreleased_20/12987.rst b/doc/build/changelog/unreleased_20/12987.rst
new file mode 100644 (file)
index 0000000..ef06101
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 12987
+
+    Fixed issue where using the :meth:`.ColumnOperators.in_` operator with a
+    nested :class:`.CompoundSelect` statement (e.g. an ``INTERSECT`` of
+    ``UNION`` queries) would raise a :class:`NotImplementedError` when the
+    nested compound select was the first argument to the outer compound select.
+    The ``_scalar_type()`` internal method now properly handles nested compound
+    selects.
index 8d1f60db1216293d9351ea26fc355dd87688edeb..4b2038e02ed220ceaee8ab1a58c3ef431a498448 100644 (file)
@@ -3951,6 +3951,9 @@ class SelectStatementGrouping(GroupedElement, SelectBase, Generic[_SB]):
     def _from_objects(self) -> List[FromClause]:
         return self.element._from_objects
 
+    def _scalar_type(self) -> TypeEngine[Any]:
+        return self.element._scalar_type()
+
     def add_cte(self, *ctes: CTE, nest_here: bool = False) -> Self:
         # SelectStatementGrouping not generative: has no attribute '_generate'
         raise NotImplementedError
index 87ac3528b88c0692e28b5d59b24fd4c84ae72d0c..b7a3bd2f6b7cba97a1043453c5e55163972699bc 100644 (file)
@@ -11,6 +11,7 @@ from sqlalchemy import bindparam
 from sqlalchemy import Enum
 from sqlalchemy import exc
 from sqlalchemy import Integer
+from sqlalchemy import intersect
 from sqlalchemy import join
 from sqlalchemy import LargeBinary
 from sqlalchemy import literal_column
@@ -2388,6 +2389,51 @@ class InTest(fixtures.TestBase, testing.AssertsCompiledSQL):
                 literal_binds=True,
             )
 
+    def test_in_scalar_grouping(self):
+        """test for :ticket:`12987`
+
+        Test that using in_() with a nested CompoundSelect works correctly.
+        This occurs when a CompoundSelect is the first argument to another
+        CompoundSelect.
+
+        """
+
+        t = self.table1
+
+        # Create nested compound selects
+        inner_compound_stmt = union(
+            select(t.c.myid).where(t.c.myid == 5),
+            select(t.c.myid).where(t.c.myid == 6),
+        )
+        simple_stmt = select(t.c.myid).where(t.c.myid == 7)
+
+        # When simple statement is first, should work
+        outer_compound_stmt = intersect(simple_stmt, inner_compound_stmt)
+        self.assert_compile(
+            select(t).where(t.c.myid.in_(outer_compound_stmt)),
+            "SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid IN ("
+            "SELECT mytable.myid FROM mytable WHERE mytable.myid = :myid_1 "
+            "INTERSECT (SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid = :myid_2 "
+            "UNION SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid = :myid_3))",
+        )
+
+        # When compound statement is first, previously raised
+        # NotImplementedError
+        outer_compound_stmt = intersect(inner_compound_stmt, simple_stmt)
+        self.assert_compile(
+            select(t).where(t.c.myid.in_(outer_compound_stmt)),
+            "SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid IN ("
+            "(SELECT mytable.myid FROM mytable WHERE mytable.myid = :myid_1 "
+            "UNION SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid = :myid_2) "
+            "INTERSECT SELECT mytable.myid FROM mytable "
+            "WHERE mytable.myid = :myid_3)",
+        )
+
     @testing.combinations(True, False, argnames="is_in")
     @testing.combinations(True, False, argnames="negate")
     def test_in_empty_tuple(self, is_in, negate):