Skip to content

Commit 2b3dba1

Browse files
fix(presto): iterate over copy in eliminate_semi_and_anti_joins (#7455)
When a SELECT has multiple SEMI or ANTI joins, eliminate_semi_and_anti_joins iterates over the joins list while calling join.pop(), which mutates the list mid-iteration. This causes every other SEMI/ANTI join to be skipped, leaving bare ANTI JOIN / SEMI JOIN syntax in the generated SQL — invalid for dialects like Presto/Trino that rely on this transform. Fix: iterate over list(...) so removals don't shift unvisited elements. Same approach as PR #4364 which fixed the identical pattern in unnest_to_explode. Made-with: Cursor
1 parent 61bb18c commit 2b3dba1

File tree

2 files changed

+29
-1
lines changed

2 files changed

+29
-1
lines changed

sqlglot/transforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ def epoch_cast_to_ts(expression: exp.Expr) -> exp.Expr:
606606
def eliminate_semi_and_anti_joins(expression: exp.Expr) -> exp.Expr:
607607
"""Convert SEMI and ANTI joins into equivalent forms that use EXIST instead."""
608608
if isinstance(expression, exp.Select):
609-
for join in expression.args.get("joins") or []:
609+
for join in list(expression.args.get("joins") or []):
610610
on = join.args.get("on")
611611
if on and join.kind in ("SEMI", "ANTI"):
612612
subquery = exp.select("1").from_(join.this).where(on)

tests/test_transforms.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
eliminate_distinct_on,
66
eliminate_join_marks,
77
eliminate_qualify,
8+
eliminate_semi_and_anti_joins,
89
eliminate_window_clause,
910
inherit_struct_field_names,
1011
remove_precision_parameterized_types,
@@ -279,6 +280,33 @@ def test_eliminate_join_marks(self):
279280
AssertionError, eliminate_join_marks, parse_one(script, dialect=dialect)
280281
)
281282

283+
def test_eliminate_semi_and_anti_joins(self):
284+
self.validate(
285+
eliminate_semi_and_anti_joins,
286+
"SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.id ANTI JOIN t3 ON t1.id = t3.id",
287+
"SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.id WHERE NOT EXISTS(SELECT 1 FROM t3 WHERE t1.id = t3.id)",
288+
)
289+
self.validate(
290+
eliminate_semi_and_anti_joins,
291+
"SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.id SEMI JOIN t3 ON t1.id = t3.id",
292+
"SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.id WHERE EXISTS(SELECT 1 FROM t3 WHERE t1.id = t3.id)",
293+
)
294+
self.validate(
295+
eliminate_semi_and_anti_joins,
296+
"SELECT t1.id FROM t1 ANTI JOIN t2 ON t1.id = t2.id ANTI JOIN t3 ON t1.id = t3.id",
297+
"SELECT t1.id FROM t1 WHERE NOT EXISTS(SELECT 1 FROM t2 WHERE t1.id = t2.id) AND NOT EXISTS(SELECT 1 FROM t3 WHERE t1.id = t3.id)",
298+
)
299+
self.validate(
300+
eliminate_semi_and_anti_joins,
301+
"SELECT t1.id FROM t1 ANTI JOIN t2 ON t1.id = t2.id ANTI JOIN t3 ON t1.id = t3.id ANTI JOIN t4 ON t1.id = t4.id",
302+
"SELECT t1.id FROM t1 WHERE (NOT EXISTS(SELECT 1 FROM t2 WHERE t1.id = t2.id) AND NOT EXISTS(SELECT 1 FROM t3 WHERE t1.id = t3.id)) AND NOT EXISTS(SELECT 1 FROM t4 WHERE t1.id = t4.id)",
303+
)
304+
self.validate(
305+
eliminate_semi_and_anti_joins,
306+
"SELECT t1.id FROM t1 SEMI JOIN t2 ON t1.id = t2.id SEMI JOIN t3 ON t1.id = t3.id",
307+
"SELECT t1.id FROM t1 WHERE EXISTS(SELECT 1 FROM t2 WHERE t1.id = t2.id) AND EXISTS(SELECT 1 FROM t3 WHERE t1.id = t3.id)",
308+
)
309+
282310
def test_eliminate_window_clause(self):
283311
self.validate(
284312
eliminate_window_clause,

0 commit comments

Comments
 (0)