--- /dev/null
+.. change::
+ :tags: bug, sql
+ :tickets: 6886
+
+ Adjusted the "from linter" warning feature to accommodate for a chain of
+ joins more than one level deep where the ON clauses don't explicitly match
+ up the targets, such as an expression such as "ON TRUE". This mode of use
+ is intended to cancel the cartesian product warning simply by the fact that
+ there's a JOIN from "a to b", which was not working for the case where the
+ chain of joins had more than one element.
+
+.. change::
+ :tags: bug, postgresql
+ :tickets: 6886
+
+ Added the "is_comparison" flag to the PostgreSQL "overlaps",
+ "contained_by", "contains" operators, so that they work in relevant ORM
+ contexts as well as in conjunction with the "from linter" feature.
return self
-CONTAINS = operators.custom_op("@>", precedence=5)
+CONTAINS = operators.custom_op("@>", precedence=5, is_comparison=True)
-CONTAINED_BY = operators.custom_op("<@", precedence=5)
+CONTAINED_BY = operators.custom_op("<@", precedence=5, is_comparison=True)
-OVERLAP = operators.custom_op("&&", precedence=5)
+OVERLAP = operators.custom_op("&&", precedence=5, is_comparison=True)
class ARRAY(sqltypes.ARRAY):
def visit_join(self, join, asfrom=False, from_linter=None, **kwargs):
if from_linter:
- from_linter.edges.add((join.left, join.right))
+ from_linter.edges.update(
+ itertools.product(
+ join.left._from_objects, join.right._from_objects
+ )
+ )
if join.full:
join_type = " FULL OUTER JOIN "
checkparams={"param_1": 7},
)
+ @testing.combinations(
+ (lambda c: c.overlap, "&&"),
+ (lambda c: c.contains, "@>"),
+ (lambda c: c.contained_by, "<@"),
+ )
+ def test_overlap_no_cartesian(self, op_fn, expected_op):
+ """test #6886"""
+ t1 = table(
+ "t1",
+ column("id", Integer),
+ column("ancestor_ids", postgresql.ARRAY(Integer)),
+ )
+
+ t1a = t1.alias()
+ t1b = t1.alias()
+
+ stmt = (
+ select(t1, t1a, t1b)
+ .where(op_fn(t1a.c.ancestor_ids)(postgresql.array((t1.c.id,))))
+ .where(op_fn(t1b.c.ancestor_ids)(postgresql.array((t1.c.id,))))
+ )
+
+ self.assert_compile(
+ stmt,
+ "SELECT t1.id, t1.ancestor_ids, t1_1.id AS id_1, "
+ "t1_1.ancestor_ids AS ancestor_ids_1, t1_2.id AS id_2, "
+ "t1_2.ancestor_ids AS ancestor_ids_2 "
+ "FROM t1, t1 AS t1_1, t1 AS t1_2 "
+ "WHERE t1_1.ancestor_ids %(op)s ARRAY[t1.id] "
+ "AND t1_2.ancestor_ids %(op)s ARRAY[t1.id]" % {"op": expected_op},
+ from_linting=True,
+ )
+
@testing.combinations((True,), (False,))
def test_array_zero_indexes(self, zero_indexes):
c = Column("x", postgresql.ARRAY(Integer, zero_indexes=zero_indexes))
# TEST: test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 400805
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 417205
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_3.9_sqlite_pysqlite_dbapiunicode_cextensions 405405
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_3.9_sqlite_pysqlite_dbapiunicode_nocextensions 423905
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_cextensions 420805
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_2.7_sqlite_pysqlite_dbapiunicode_nocextensions 437205
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_3.9_sqlite_pysqlite_dbapiunicode_cextensions 425405
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results x86_64_linux_cpython_3.9_sqlite_pysqlite_dbapiunicode_nocextensions 443905
# TEST: test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity
froms, start = find_unmatching_froms(query)
assert not froms
+ def test_join_on_true_muti_levels(self):
+ """test #6886"""
+ # test that a join(a, b).join(c) counts b->c as an edge even if there
+ # isn't actually a join condition. this essentially allows a cartesian
+ # product to be added explicitly.
+
+ query = select(self.a, self.b, self.c).select_from(
+ self.a.join(self.b, true()).join(self.c, true())
+ )
+ froms, start = find_unmatching_froms(query)
+ assert not froms
+
def test_right_nested_join_with_an_issue(self):
query = (
select(self.a)
(True, "omit_alias"), (False, "with_alias"), id_="ai", argnames="omit"
)
@testing.provide_metadata
- @testing.skip_if('mysql < 8')
+ @testing.skip_if("mysql < 8")
def test_duplicate_values_accepted(self, native, omit):
foo_enum = pep435_enum("foo_enum")
foo_enum("one", 1, "two")