Skip to content

fix: hash join dynamic filter with dictionary-encoded join keys - #24587

Merged
jayzhan211 merged 2 commits into
apache:mainfrom
jayzhan211:fix/hash-join-dict-key-dynamic-filter
Aug 27, 2026
Merged

jayzhan211 merged 2 commits into
apache:mainfrom
jayzhan211:fix/hash-join-dict-key-dynamic-filter

Conversation

@jayzhan211

Copy link
Copy Markdown
Contributor

Rationale for this change

Hash joins on a dictionary-encoded integer join key misbehave when dynamic filter pushdown is active. A dictionary array can carry NULL in its values while its key bitmap has no physical nulls (null_count() == 0 but logical_null_count() > 0), and two places in the hash join only looked at the physical count:

  1. Wrong results: for a NullEqualsNull join, keys_have_null was computed with null_count(), so the pushed dynamic filter was not widened with key IS NULL and pruned the probe-side NULL row that should have null-matched the build-side NULL.
  2. Query error: try_create_array_map inferred "the key type is supported by ArrayMap" from the presence of min/max bounds. But bounds are also collected for dynamic filters on any key type, so with a dictionary key and a filter consumer the build side failed with Unsupported type for ArrayMap: Dictionary(Int32, Int32) instead of falling back to the regular hash map.

What changes are included in this PR?

  • stream.rs: compute keys_have_null with logical_null_count().
  • exec.rs: gate ArrayMap (perfect hash join) creation on the key type via is_perfect_hash_join_candidate rather than on the presence of bounds; also use logical_null_count() for the NullEqualsNull check in the same function.
  • Rename should_collect_min_max_for_perfect_hash → is_perfect_hash_join_candidate with a doc comment, since it describes what is checked rather than a side effect.

Are these changes tested?

Yes. A new test, test_null_equal_dynamic_filter_keeps_probe_nulls_for_build_logical_null, builds a NullEqualsNull inner join over dictionary keys with a dynamic filter consumer on the probe side and asserts both that the pushed filter contains the IS NULL disjunct and that the NULL–NULL row is produced. Before this PR it fails with the ArrayMap error; with only the ArrayMap gate it fails on the missing IS NULL. Existing hash join unit tests and the joins, push_down_filter_parquet, and explain_analyze sqllogictests pass.

Are there any user-facing changes?

No API changes. Joins on dictionary-encoded integer keys with dynamic filter pushdown now return correct results instead of erroring / dropping rows.

- Use logical_null_count() for keys_have_null so a null-equal join's pushed
  filter keeps probe NULL rows when the build NULL lives in a dictionary value
- Gate ArrayMap (perfect hash join) creation on the key type instead of the
  presence of bounds, which are also collected for dynamic filters on any type
- Rename should_collect_min_max_for_perfect_hash -> is_perfect_hash_join_candidate
@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Aug 23, 2026
@codecov-commenter

codecov-commenter commented Aug 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.75145% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.45%. Comparing base (5134a1a) to head (e5b1bc0).
⚠️ Report is 51 commits behind head on main.

Files with missing lines Patch % Lines
...tafusion/physical-plan/src/joins/hash_join/exec.rs 86.32% 4 Missing and 12 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24587      +/-   ##
==========================================
+ Coverage   81.38%   81.45%   +0.07%     
==========================================
  Files        1116     1120       +4     
  Lines      397960   401477    +3517     
  Branches   397960   401477    +3517     
==========================================
+ Hits       323880   327043    +3163     
- Misses      55120    55296     +176     
- Partials    18960    19138     +178     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jayzhan211
jayzhan211 requested review from gabotechs and kosiew August 26, 2026 12:37

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jayzhan211,

Thanks for working on this. The fix looks good to me. I left one non-blocking suggestion to add coverage for the partitioned path as well.

.values()
.iter()
.any(|array| array.null_count() > 0);
.any(|array| array.logical_null_count() > 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the same dictionary-value logical-NULL regression coverage for PartitionMode::Partitioned, using multiple partitions? keys_have_null is also aggregated by build_partitioned_filter, while the new test currently exercises only CollectLeft. Covering the partitioned case would help catch regressions in how the logical NULL state is aggregated or routed across partitions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! Thanks your suggestion

@jayzhan211
jayzhan211 enabled auto-merge August 27, 2026 12:49
@jayzhan211
jayzhan211 added this pull request to the merge queue Aug 27, 2026
Merged via the queue into apache:main with commit d66f4b9 Aug 27, 2026
40 of 41 checks passed
@jayzhan211
jayzhan211 deleted the fix/hash-join-dict-key-dynamic-filter branch August 27, 2026 14:15
@jayzhan211

Copy link
Copy Markdown
Contributor Author

Sorry, I though auto-merge will be disabled if the test failed

@alamb

alamb commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Seems it causes CI fail

but that is probably what @jayzhan211 is referring to abve

alamb pushed a commit to tohuya6/datafusion that referenced this pull request Aug 27, 2026
…ions handling (apache#24729)

## Which issue does this PR close?

- resolved apache#24734

apache#24587 is auto merged with
failed test, this fix the test. The issue is something from `feature =
"force_hash_collisions"`

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes apache#123` indicates that this PR will close issue apache#123.
-->

- Closes #.

## Rationale for this change

<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.

Please explain the problem you are trying to solve in terms of the
user-visible
behavior, rather than the implementation.

For example, "The code in `foo.rs` doesn't handle nulls" is a symptom of
the
implementation. "COUNT(DISTINCT) returns wrong results when the column
contains
nulls" is the user-visible problem.
-->

## What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here, but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

## Are these changes tested?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

## Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.

If there are any breaking changes to public APIs, please add the `api
change` label.
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants