adriangb opened a new pull request, #25348:
URL: https://github.com/apache/datafusion/pull/25348
## Which issue does this PR close?
- Closes #25340.
A follow-up issue covers the remaining `NOT IN` problems. See **Follow-up
work** at the end.
## Rationale for this change
`3 NOT IN (1, NULL)` is UNKNOWN. A `WHERE` clause must remove the row.
DataFusion kept every row. There was no error and no warning.
```sql
CREATE TABLE t1(id INT) AS VALUES (1), (2);
CREATE TABLE t2(id INT) AS VALUES (1), (NULL);
SELECT id FROM t1 WHERE 3 NOT IN (SELECT id FROM t2) ORDER BY id;
```
| | result |
|---|---|
| DataFusion (before) | `1, 2` ❌ |
| DataFusion (after) | *(no rows)* ✅ |
| DuckDB 1.5.2 | *(no rows)* |
| PostgreSQL 17.11 | *(no rows)* |
The subquery gives `{1, NULL}`. The value `3` is not `NULL`, but `3` is also
not known to be absent. Therefore the answer is UNKNOWN for every row of `t1`.
The same expression in a `SELECT` list was already correct. Only the `WHERE`
clause was wrong.
### Why it happened
The value `3` holds no column. Therefore it could not become a join key. It
stayed as a join filter. Two problems followed.
```
BEFORE AFTER
WHERE 3 NOT IN (SELECT id FROM t2) WHERE 3 NOT IN (SELECT id
FROM t2)
│ │
▼ ▼
anti join, no key anti join, key = (3, t2.id)
filter: 3 = t2.id no filter
│ │
├─ (1) the filter moves into the ├─ (1) nothing to
move
│ subquery: WHERE t2.id = 3 │
│ the NULL row disappears │
│ │
└─ (2) no key, so the join becomes a └─ (2) the join has
a key, so it
nested loop join, which cannot stays a hash
join, which
do null-aware work. The flag is does
null-aware work
dropped without an error correctly
│ │
▼ ▼
1, 2 ❌ (no rows) ✅
```
## What changes are included in this PR?
Three changes.
**1. Make the constant a join key**
(`datafusion/optimizer/src/decorrelate_predicate_subquery.rs`)
Add the constant to the outer side as a column. The comparison then becomes
a true equality of two columns, so the existing null-aware hash join does the
work.
```
Before: LeftAnti Join: Filter: Int64(3) = __correlated_sq_1.id null_aware
→ NestedLoopJoinExec (the null_aware flag is lost)
After: LeftAnti Join: __correlated_sq_1_value = __correlated_sq_1.id
null_aware
Projection: t1.id, Int64(3) AS __correlated_sq_1_value
→ HashJoinExec ... null_aware
```
This applies only to uncorrelated subqueries. A correlated subquery needs a
second key, and a null-aware anti join accepts only one key.
**2. Keep the filter out of the subquery**
(`datafusion/optimizer/src/push_down_filter.rs`)
Do not move a predicate into the subquery side of a null-aware join. The
NULLs must reach the join. `infer_join_predicates` has the same rule already.
**3. Report an error instead of a wrong answer**
(`datafusion/core/src/physical_planner.rs`)
Only a hash join can do null-aware work, and it needs a key. If a null-aware
join has no key, report an error. Do not build a nested loop join that gives
wrong results without a warning.
## What is the testing strategy for this PR?
New `sqllogictest` cases in
`datafusion/sqllogictest/test_files/null_aware_anti_join.slt` and
`null_aware_mark_join.slt`. They cover:
- the four queries of the issue, and its controls;
- a subquery with no NULL, which must keep all rows;
- the `OR` and `IS NULL` forms, which use a mark join;
- a user column with the same name as the new column, which must not be
ambiguous;
- the one shape that is not supported, which must report an error.
New unit tests in `decorrelate_predicate_subquery.rs` and
`push_down_filter.rs` hold the new plans.
Results:
| check | result |
|---|---|
| full `sqllogictest` suite | pass, and **no plan changes** anywhere else |
| `datafusion-optimizer` unit tests | 853 pass |
| extended workspace suite | 10,784 tests pass, 0 fail, 66 crates |
| `cargo fmt --all` | clean |
| `cargo clippy` | clean for the changed crates |
The extended workspace figure comes from an earlier run of the same commit
on a slightly older base. The full `sqllogictest` suite and the optimizer tests
were re-run after the rebase onto current `main`.
## Are there any user-facing changes?
Yes. Two.
**1. `<constant> NOT IN (<subquery>)` in a `WHERE` clause now gives correct
results.** This is the fix.
**2. One shape now reports an error.** A constant with a correlation that is
not an equality:
```sql
SELECT id FROM t1 WHERE 3 NOT IN (SELECT id FROM t2 WHERE t2.g > t1.g);
```
```
Error during planning: null_aware LeftAnti join requires equi-join keys, but
the join has none
```
This query gave wrong results before. No correct result is lost. An error is
better than a wrong answer that a user cannot see.
There are no API changes.
## Follow-up work
This PR makes every **uncorrelated** `NOT IN` correct. **Correlated** `NOT
IN` still has problems. They come from the join operator, not from the code
this PR changes.
```
NOT IN (subquery)
│
├── no outer column ──────────────▶ CORRECT (this PR)
│
└── reads an outer column
├── equality condition ───────▶ WRONG or ERROR (follow-up, parts 1 and 2)
└── other condition ──────────▶ WRONG (follow-up, part 3)
```
A test matrix of 18 query shapes gives these totals:
| | wrong or error |
|---|---|
| before this PR | 13 of 18 |
| after this PR | 10 of 18 |
The 10 remaining shapes are all correlated. They fall into three parts:
1. A null-aware anti join accepts only one key, but a correlated `NOT IN`
needs two.
2. A constant does not become a key when the subquery is correlated.
3. A null-aware join looks for NULLs only in the key, not in a leftover
filter.
Part 3 must come first, as an error. A prototype showed that part 2 alone
turns a clear error into a silent wrong answer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01VCeMPyJNgAiFpGXaz5CxF3
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]