From: Mike Bayer Date: Fri, 17 Jan 2020 19:31:10 +0000 (-0500) Subject: apply asbool reduction to the onclause in join() X-Git-Tag: rel_1_4_0b1~554^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=bdbe164d392d41991b64ced0f097930a04a2c420;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git apply asbool reduction to the onclause in join() The :func:`.true` and :func:`.false` operators may now be applied as the "onclause" of a :func:`.sql.join` on a backend that does not support "native boolean" expressions, e.g. Oracle or SQL Server, and the expression will render as "1=1" for true and "1=0" false. This is the behavior that was introduced many years ago in :ticket:`2804` for and/or expressions. Change-Id: I85311c31c22d6e226c618f8840f6b95eca611153 --- diff --git a/doc/build/changelog/unreleased_14/asbool_join.rst b/doc/build/changelog/unreleased_14/asbool_join.rst new file mode 100644 index 0000000000..154f056bb4 --- /dev/null +++ b/doc/build/changelog/unreleased_14/asbool_join.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: usecase, sql + + The :func:`.true` and :func:`.false` operators may now be applied as the + "onclause" of a :func:`.sql.join` on a backend that does not support + "native boolean" expressions, e.g. Oracle or SQL Server, and the expression + will render as "1=1" for true and "1=0" false. This is the behavior that + was introduced many years ago in :ticket:`2804` for and/or expressions. diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 136c9f868c..db743f408d 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -698,7 +698,7 @@ class Join(FromClause): if onclause is None: self.onclause = self._match_primaries(self.left, self.right) else: - self.onclause = onclause + self.onclause = onclause.self_group(against=operators._asbool) self.isouter = isouter self.full = full diff --git a/lib/sqlalchemy/testing/suite/test_select.py b/lib/sqlalchemy/testing/suite/test_select.py index 9db2daf7ac..3b64b0f291 100644 --- a/lib/sqlalchemy/testing/suite/test_select.py +++ b/lib/sqlalchemy/testing/suite/test_select.py @@ -1,3 +1,6 @@ +import itertools + +from sqlalchemy import ForeignKey from .. import AssertsCompiledSQL from .. import AssertsExecutionResults from .. import config @@ -292,6 +295,105 @@ class LimitOffsetTest(fixtures.TablesTest): ) +class JoinTest(fixtures.TablesTest): + __backend__ = True + + def _assert_result(self, select, result, params=()): + eq_(config.db.execute(select, params).fetchall(), result) + + @classmethod + def define_tables(cls, metadata): + Table("a", metadata, Column("id", Integer, primary_key=True)) + Table( + "b", + metadata, + Column("id", Integer, primary_key=True), + Column("a_id", ForeignKey("a.id"), nullable=False), + ) + + @classmethod + def insert_data(cls): + config.db.execute( + cls.tables.a.insert(), + [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}], + ) + + config.db.execute( + cls.tables.b.insert(), + [ + {"id": 1, "a_id": 1}, + {"id": 2, "a_id": 1}, + {"id": 4, "a_id": 2}, + {"id": 5, "a_id": 3}, + ], + ) + + def test_inner_join_fk(self): + a, b = self.tables("a", "b") + + stmt = select([a, b]).select_from(a.join(b)).order_by(a.c.id, b.c.id) + + self._assert_result(stmt, [(1, 1, 1), (1, 2, 1), (2, 4, 2), (3, 5, 3)]) + + def test_inner_join_true(self): + a, b = self.tables("a", "b") + + stmt = ( + select([a, b]) + .select_from(a.join(b, true())) + .order_by(a.c.id, b.c.id) + ) + + self._assert_result( + stmt, + [ + (a, b, c) + for (a,), (b, c) in itertools.product( + [(1,), (2,), (3,), (4,), (5,)], + [(1, 1), (2, 1), (4, 2), (5, 3)], + ) + ], + ) + + def test_inner_join_false(self): + a, b = self.tables("a", "b") + + stmt = ( + select([a, b]) + .select_from(a.join(b, false())) + .order_by(a.c.id, b.c.id) + ) + + self._assert_result(stmt, []) + + def test_outer_join_false(self): + a, b = self.tables("a", "b") + + stmt = ( + select([a, b]) + .select_from(a.outerjoin(b, false())) + .order_by(a.c.id, b.c.id) + ) + + self._assert_result( + stmt, + [ + (1, None, None), + (2, None, None), + (3, None, None), + (4, None, None), + (5, None, None), + ], + ) + + def test_outer_join_fk(self): + a, b = self.tables("a", "b") + + stmt = select([a, b]).select_from(a.join(b)).order_by(a.c.id, b.c.id) + + self._assert_result(stmt, [(1, 1, 1), (1, 2, 1), (2, 4, 2), (3, 5, 3)]) + + class CompoundSelectTest(fixtures.TablesTest): __backend__ = True diff --git a/test/profiles.txt b/test/profiles.txt index 4006fb94d0..5e2ca814ac 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -562,9 +562,9 @@ test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased 3.7_sqlite_pys # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain -test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 2.7_postgresql_psycopg2_dbapiunicode_cextensions 3255 -test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 2.7_sqlite_pysqlite_dbapiunicode_cextensions 3255 -test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 3256 +test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 2.7_postgresql_psycopg2_dbapiunicode_cextensions 3555 +test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 2.7_sqlite_pysqlite_dbapiunicode_cextensions 3555 +test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 3556 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index aa56d0b6b3..a90b03b387 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -5,6 +5,7 @@ from sqlalchemy import and_ from sqlalchemy import between from sqlalchemy import exc from sqlalchemy import Integer +from sqlalchemy import join from sqlalchemy import LargeBinary from sqlalchemy import literal_column from sqlalchemy import not_ @@ -988,6 +989,42 @@ class BooleanEvalTest(fixtures.TestBase, testing.AssertsCompiledSQL): or_(false(), true()), "1 = 1", dialect=self._dialect(False) ) + def test_seven_a(self): + t1 = table("t1", column("a")) + t2 = table("t2", column("b")) + self.assert_compile( + join(t1, t2, onclause=true()), + "t1 JOIN t2 ON 1 = 1", + dialect=self._dialect(False), + ) + + def test_seven_b(self): + t1 = table("t1", column("a")) + t2 = table("t2", column("b")) + self.assert_compile( + join(t1, t2, onclause=false()), + "t1 JOIN t2 ON 0 = 1", + dialect=self._dialect(False), + ) + + def test_seven_c(self): + t1 = table("t1", column("a")) + t2 = table("t2", column("b")) + self.assert_compile( + join(t1, t2, onclause=true()), + "t1 JOIN t2 ON true", + dialect=self._dialect(True), + ) + + def test_seven_d(self): + t1 = table("t1", column("a")) + t2 = table("t2", column("b")) + self.assert_compile( + join(t1, t2, onclause=false()), + "t1 JOIN t2 ON false", + dialect=self._dialect(True), + ) + def test_eight(self): self.assert_compile( and_(false(), true()), "false", dialect=self._dialect(True)