Repository navigation
Description
Describe the bug
SortMergeJoinExec skips a join filter whose expression references no columns, so a FULL JOIN keeps pairs that the filter rejects. Hash join returns the correct result.
A common way to end up with such a filter is a prepared statement with a parameter-only condition in the ON clause: once the parameter is bound, the condition (e.g. 3 > 5) no longer references any column.
To Reproduce
SET datafusion.optimizer.prefer_hash_join = false;
SET datafusion.execution.target_partitions = 2;
CREATE TABLE a AS SELECT * FROM (VALUES (1, 10), (2, 20), (3, 30)) t(k, v);
CREATE TABLE b AS SELECT * FROM (VALUES (1, 100), (2, 200), (4, 400)) t(k, w);
PREPARE q(INT) AS SELECT * FROM a FULL JOIN b ON a.k = b.k AND $1 > 5 ORDER BY a.k, b.k;
EXECUTE q(3);
+------+------+------+------+
| k | v | k | w |
+------+------+------+------+
| 1 | 10 | 1 | 100 |
| 2 | 20 | 2 | 200 |
| 3 | 30 | NULL | NULL |
| NULL | NULL | 4 | 400 |
+------+------+------+------+The same happens with a boolean parameter, and with a volatile function that takes no columns:
PREPARE qb(BOOLEAN) AS SELECT count(*) FROM a FULL JOIN b ON a.k = b.k AND $1;
EXECUTE qb(false);
-- 4, expected 6
SELECT * FROM a FULL JOIN b ON a.k = b.k AND random() > 2 ORDER BY a.k, b.k;
-- the same 4 rows as above
-- EXPLAIN: SortMergeJoinExec: join_type=Full, on=[(k@0, k@0)], filter=random() > 2(With target_partitions = 1 the planner picks HashJoinExec and the results are correct.)
Expected behavior
3 > 5 (and random() > 2) is false for every pair, so nothing matches and every row is NULL-padded, as with SET datafusion.optimizer.prefer_hash_join = true:
+------+------+------+------+
| k | v | k | w |
+------+------+------+------+
| 1 | 10 | NULL | NULL |
| 2 | 20 | NULL | NULL |
| 3 | 30 | NULL | NULL |
| NULL | NULL | 1 | 100 |
| NULL | NULL | 2 | 200 |
| NULL | NULL | 4 | 400 |
+------+------+------+------+
Additional context
Reproduced on current main (Oct 3 2026). LEFT, RIGHT and INNER joins with the same conditions return correct results.
In sort_merge_join/materializing_stream.rs the filter is only evaluated under if !filter_columns.is_empty(), so a filter whose expression reads no columns is skipped and every candidate pair is kept. A possible fix is to evaluate the filter even when it has no columns, building the filter batch with RecordBatchOptions::with_row_count(...) so its row count matches the candidate pairs.
Related: #26001 (same code path, filter batch construction for outer joins).
Activity
fix: evaluate sort-merge join filters that reference no columns (apac…