summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEric Masseran <eric.masseran@gmail.com>2021-11-02 16:40:04 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2021-11-16 14:44:36 -0500
commit958f902b1fc528fed0be550bc573545de47ed854 (patch)
tree806989f5c2fe6f3a2480b6f878aa11db0eed92ca /lib
parent6206f0ff74e95c9339dc0f0e26caab55e9bcda45 (diff)
downloadsqlalchemy-958f902b1fc528fed0be550bc573545de47ed854.tar.gz
Add Non linear CTE support
"Compound select" methods like :meth:`_sql.Select.union`, :meth:`_sql.Select.intersect_all` etc. now accept ``*other`` as an argument rather than ``other`` to allow for multiple additional SELECTs to be compounded with the parent statement at once. In particular, the change as applied to :meth:`_sql.CTE.union` and :meth:`_sql.CTE.union_all` now allow for a so-called "non-linear CTE" to be created with the :class:`_sql.CTE` construct, whereas previously there was no way to have more than two CTE sub-elements in a UNION together while still correctly calling upon the CTE in recursive fashion. Pull request courtesy Eric Masseran. Allow: ```sql WITH RECURSIVE nodes(x) AS ( SELECT 59 UNION SELECT aa FROM edge JOIN nodes ON bb=x UNION SELECT bb FROM edge JOIN nodes ON aa=x ) SELECT x FROM nodes; ``` Based on @zzzeek suggestion: https://github.com/sqlalchemy/sqlalchemy/pull/7133#issuecomment-933882348 Fixes: #7259 Closes: #7260 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/7260 Pull-request-sha: 2565a5fd4b1940e92125e53aeaa731cc682f49bb Change-Id: I685c8379762b5fb6ab4107ff8f4d8a4de70c0ca6
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/sql/selectable.py184
1 files changed, 155 insertions, 29 deletions
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 0e5ae89e4..350e55c49 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -2121,9 +2121,23 @@ class CTE(
_suffixes=self._suffixes,
)
- def union(self, other):
+ def union(self, *other):
+ r"""Return a new :class:`_expression.CTE` with a SQL ``UNION``
+ of the original CTE against the given selectables provided
+ as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28 multiple elements are now accepted.
+
+ .. seealso::
+
+ :meth:`_sql.HasCTE.cte` - examples of calling styles
+
+ """
return CTE._construct(
- self.element.union(other),
+ self.element.union(*other),
name=self.name,
recursive=self.recursive,
nesting=self.nesting,
@@ -2132,9 +2146,23 @@ class CTE(
_suffixes=self._suffixes,
)
- def union_all(self, other):
+ def union_all(self, *other):
+ r"""Return a new :class:`_expression.CTE` with a SQL ``UNION ALL``
+ of the original CTE against the given selectables provided
+ as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28 multiple elements are now accepted.
+
+ .. seealso::
+
+ :meth:`_sql.HasCTE.cte` - examples of calling styles
+
+ """
return CTE._construct(
- self.element.union_all(other),
+ self.element.union_all(*other),
name=self.name,
recursive=self.recursive,
nesting=self.nesting,
@@ -2396,7 +2424,7 @@ class HasCTE(roles.HasCTERole):
connection.execute(upsert)
- Example 4, Nesting CTE::
+ Example 4, Nesting CTE (SQLAlchemy 1.4.24 and above)::
value_a = select(
literal("root").label("n")
@@ -2426,6 +2454,44 @@ class HasCTE(roles.HasCTERole):
SELECT value_a.n AS a, value_b.n AS b
FROM value_a, value_b
+ Example 5, Non-Linear CTE (SQLAlchemy 1.4.28 and above)::
+
+ edge = Table(
+ "edge",
+ metadata,
+ Column("id", Integer, primary_key=True),
+ Column("left", Integer),
+ Column("right", Integer),
+ )
+
+ root_node = select(literal(1).label("node")).cte(
+ "nodes", recursive=True
+ )
+
+ left_edge = select(edge.c.left).join(
+ root_node, edge.c.right == root_node.c.node
+ )
+ right_edge = select(edge.c.right).join(
+ root_node, edge.c.left == root_node.c.node
+ )
+
+ subgraph_cte = root_node.union(left_edge, right_edge)
+
+ subgraph = select(subgraph_cte)
+
+ The above query will render 2 UNIONs inside the recursive CTE::
+
+ WITH RECURSIVE nodes(node) AS (
+ SELECT 1 AS node
+ UNION
+ SELECT edge."left" AS "left"
+ FROM edge JOIN nodes ON edge."right" = nodes.node
+ UNION
+ SELECT edge."right" AS "right"
+ FROM edge JOIN nodes ON edge."left" = nodes.node
+ )
+ SELECT nodes.node FROM nodes
+
.. seealso::
:meth:`_orm.Query.cte` - ORM version of
@@ -6269,47 +6335,107 @@ class Select(
else:
return SelectStatementGrouping(self)
- def union(self, other, **kwargs):
- """Return a SQL ``UNION`` of this select() construct against
- the given selectable.
+ def union(self, *other, **kwargs):
+ r"""Return a SQL ``UNION`` of this select() construct against
+ the given selectables provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
+
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_union(self, other, **kwargs)
+ return CompoundSelect._create_union(self, *other, **kwargs)
+
+ def union_all(self, *other, **kwargs):
+ r"""Return a SQL ``UNION ALL`` of this select() construct against
+ the given selectables provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
- def union_all(self, other, **kwargs):
- """Return a SQL ``UNION ALL`` of this select() construct against
- the given selectable.
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
+
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_union_all(self, other, **kwargs)
+ return CompoundSelect._create_union_all(self, *other, **kwargs)
+
+ def except_(self, *other, **kwargs):
+ r"""Return a SQL ``EXCEPT`` of this select() construct against
+ the given selectable provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
- def except_(self, other, **kwargs):
- """Return a SQL ``EXCEPT`` of this select() construct against
- the given selectable.
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_except(self, other, **kwargs)
+ return CompoundSelect._create_except(self, *other, **kwargs)
- def except_all(self, other, **kwargs):
- """Return a SQL ``EXCEPT ALL`` of this select() construct against
- the given selectable.
+ def except_all(self, *other, **kwargs):
+ r"""Return a SQL ``EXCEPT ALL`` of this select() construct against
+ the given selectables provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
+
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_except_all(self, other, **kwargs)
+ return CompoundSelect._create_except_all(self, *other, **kwargs)
+
+ def intersect(self, *other, **kwargs):
+ r"""Return a SQL ``INTERSECT`` of this select() construct against
+ the given selectables provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
- def intersect(self, other, **kwargs):
- """Return a SQL ``INTERSECT`` of this select() construct against
- the given selectable.
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
+
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_intersect(self, other, **kwargs)
+ return CompoundSelect._create_intersect(self, *other, **kwargs)
+
+ def intersect_all(self, *other, **kwargs):
+ r"""Return a SQL ``INTERSECT ALL`` of this select() construct
+ against the given selectables provided as positional arguments.
+
+ :param \*other: one or more elements with which to create a
+ UNION.
+
+ .. versionchanged:: 1.4.28
+
+ multiple elements are now accepted.
- def intersect_all(self, other, **kwargs):
- """Return a SQL ``INTERSECT ALL`` of this select() construct
- against the given selectable.
+ :param \**kwargs: keyword arguments are forwarded to the constructor
+ for the newly created :class:`_sql.CompoundSelect` object.
"""
- return CompoundSelect._create_intersect_all(self, other, **kwargs)
+ return CompoundSelect._create_intersect_all(self, *other, **kwargs)
@property
@util.deprecated_20(