From: Mike Bayer Date: Wed, 20 Jan 2021 22:06:14 +0000 (-0500) Subject: chain joins from SelectState context, not Select X-Git-Tag: rel_1_4_0b2~36^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b6ff056b91834ec56f685c8735cd67c9b0603de7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git chain joins from SelectState context, not Select Fixed issue in new :meth:`_sql.Select.join` method where chaining from the current JOIN wasn't looking at the right state, causing an expression like "FROM a JOIN b , b JOIN c " rather than "FROM a JOIN b JOIN c ". Added :meth:`_sql.Select.outerjoin_from` method to complement :meth:`_sql.Select.join_from`. Fixes: #5858 Change-Id: I1346ebe0963bbd1e4bf868650e3ee1d6d3072f04 --- diff --git a/doc/build/changelog/unreleased_14/5858.rst b/doc/build/changelog/unreleased_14/5858.rst new file mode 100644 index 0000000000..62cd4a751b --- /dev/null +++ b/doc/build/changelog/unreleased_14/5858.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: bug, sql + :tickets: 5858 + + Fixed issue in new :meth:`_sql.Select.join` method where chaining from the + current JOIN wasn't looking at the right state, causing an expression like + "FROM a JOIN b , b JOIN c " rather than + "FROM a JOIN b JOIN c ". + +.. change:: + :tags: usecase, sql + + Added :meth:`_sql.Select.outerjoin_from` method to complement + :meth:`_sql.Select.join_from`. \ No newline at end of file diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index b4f6c8bc98..8e478583f6 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -3944,7 +3944,7 @@ class SelectState(util.MemoizedSlots, CompileState): replace_from_obj_index = None - from_clauses = self.statement._from_obj + from_clauses = self.from_clauses if from_clauses: @@ -4644,6 +4644,19 @@ class Select( (target, onclause, None, {"isouter": isouter, "full": full}), ) + def outerjoin_from(self, from_, target, onclause=None, full=False): + r"""Create a SQL LEFT OUTER JOIN against this :class:`_expression.Select` + object's criterion + and apply generatively, returning the newly resulting + :class:`_expression.Select`. + + Usage is the same as that of :meth:`_selectable.Select.join_from`. + + """ + return self.join_from( + from_, target, onclause=onclause, isouter=True, full=full + ) + @_generative def join_from( self, from_, target, onclause=None, isouter=False, full=False diff --git a/test/sql/test_select.py b/test/sql/test_select.py index 2a2381427c..96c6abd073 100644 --- a/test/sql/test_select.py +++ b/test/sql/test_select.py @@ -41,6 +41,13 @@ child = Table( Column("data", String(50)), ) +grandchild = Table( + "grandchild", + metadata, + Column("id", Integer, primary_key=True), + Column("child_id", ForeignKey("child.id")), +) + class FutureSelectTest(fixtures.TestBase, AssertsCompiledSQL): __dialect__ = "default" @@ -96,6 +103,20 @@ class FutureSelectTest(fixtures.TestBase, AssertsCompiledSQL): "ON mytable.myid = myothertable.otherid", ) + def test_join_nofrom_implicit_left_side_explicit_onclause_3level(self): + stmt = ( + select(parent) + .join(child, child.c.parent_id == parent.c.id) + .join(grandchild, grandchild.c.child_id == child.c.id) + ) + + self.assert_compile( + stmt, + "SELECT parent.id, parent.data FROM parent JOIN child " + "ON child.parent_id = parent.id " + "JOIN grandchild ON grandchild.child_id = child.id", + ) + def test_join_nofrom_explicit_left_side_explicit_onclause(self): stmt = select(table1).join_from( table1, table2, table1.c.myid == table2.c.otherid @@ -108,6 +129,18 @@ class FutureSelectTest(fixtures.TestBase, AssertsCompiledSQL): "ON mytable.myid = myothertable.otherid", ) + def test_outerjoin_nofrom_explicit_left_side_explicit_onclause(self): + stmt = select(table1).outerjoin_from( + table1, table2, table1.c.myid == table2.c.otherid + ) + + self.assert_compile( + stmt, + "SELECT mytable.myid, mytable.name, mytable.description " + "FROM mytable LEFT OUTER JOIN myothertable " + "ON mytable.myid = myothertable.otherid", + ) + def test_join_nofrom_implicit_left_side_implicit_onclause(self): stmt = select(parent).join(child) @@ -117,6 +150,16 @@ class FutureSelectTest(fixtures.TestBase, AssertsCompiledSQL): "ON parent.id = child.parent_id", ) + def test_join_nofrom_implicit_left_side_implicit_onclause_3level(self): + stmt = select(parent).join(child).join(grandchild) + + self.assert_compile( + stmt, + "SELECT parent.id, parent.data FROM parent JOIN child " + "ON parent.id = child.parent_id " + "JOIN grandchild ON child.id = grandchild.child_id", + ) + def test_join_nofrom_explicit_left_side_implicit_onclause(self): stmt = select(parent).join_from(parent, child) @@ -172,6 +215,19 @@ class FutureSelectTest(fixtures.TestBase, AssertsCompiledSQL): "ON parent.id = child.parent_id", ) + def test_right_nested_inner_join(self): + inner = child.join(grandchild) + + stmt = select(parent).outerjoin_from(parent, inner) + + self.assert_compile( + stmt, + "SELECT parent.id, parent.data FROM parent " + "LEFT OUTER JOIN " + "(child JOIN grandchild ON child.id = grandchild.child_id) " + "ON parent.id = child.parent_id", + ) + def test_joins_w_filter_by(self): stmt = ( select(parent)