Skip to content

[fix](nereids) Guard UniqueFunction in multiple filter/topn pushdown rules#62742

Open
yujun777 wants to merge 7 commits into
apache:masterfrom
yujun777:fix/unique-fn-pushdown-batch1
Open

[fix](nereids) Guard UniqueFunction in multiple filter/topn pushdown rules#62742
yujun777 wants to merge 7 commits into
apache:masterfrom
yujun777:fix/unique-fn-pushdown-batch1

Conversation

@yujun777
Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #62705

Problem Summary:

Following up on #62705, several Nereids rewrite rules still moved predicates
containing non-idempotent (unique) functions such as rand() / uuid() /
random_bytes() across operator boundaries in ways that changed query
semantics. The common root cause is that a predicate like rand() > 0.5
has an empty input-slot set, so the containsAll(emptySet) / allMatch
guards used by these rules silently returned true and allowed the push
down / elimination.

This PR adds containsUniqueFunction() guards to the following rules:

  1. PushDownFilterThroughRepeat: skip conjuncts with unique functions.
    Pushing rand() > x below Repeat changes which rows feed each
    grouping set and alters aggregate results.

  2. PushDownFilterThroughWindow: skip conjuncts with unique functions.
    Pushing a unique predicate below a window operator re-samples the
    base rows and changes every window-function value.

  3. PushDownFilterThroughPartitionTopN: same as Window — skip unique
    conjuncts in the split loop.

  4. PushDownFilterThroughSort (sort-elimination branch): do not drop the
    sort when any order key contains a unique function. ORDER BY rand()
    has empty input slots, so the old allMatch on an empty stream
    returned true and wrongly eliminated the sort.

  5. PushDownTopNThroughUnion / PushDownTopNDistinctThroughUnion: do not
    push a TopN whose order keys contain unique functions below Union.
    With non-idempotent ordering each branch would be sorted by a
    different random draw than the global one.

Release note

Fix wrong results when predicates / order keys containing rand(), uuid(),
random_bytes() or uuid_numeric() were pushed across Repeat, Window,
PartitionTopN, Sort, or Union boundaries.

Check List (For Author)

  • Test:
    • Unit Test (FE): PushDownFilterThroughRepeatTest (3 new tests),
      PushDownFilterThroughSortTest (+1 test for unique order key),
      PushDownFilterThroughWindowTest (+1 test for unique conjunct).
    • Regression Test: 5 new suites under
      regression-test/suites/nereids_rules_p0/unique_function/.
  • Behavior changed: Yes (queries that previously returned incorrect
    results due to unsafe pushdown of unique-function predicates / order
    keys will now return correct results).
  • Does this need documentation: No

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

@yujun777
Copy link
Copy Markdown
Contributor Author

run buildall

@Thearas
Copy link
Copy Markdown
Contributor

Thearas commented Apr 23, 2026

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@yujun777
Copy link
Copy Markdown
Contributor Author

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

I found two remaining wrong-result paths, so I can't approve this batch yet.

  1. PushDownJoinOtherCondition still rewrites unique-function join predicates to one side. Because it only checks predicate.getInputSlots() (fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownJoinOtherCondition.java:77-84,110-111), ON rand() > 0.5 and ON t1.id + rand() > 5 are treated as pushable. That changes evaluation from once per joined row to once per input row.
  2. PushDownFilterThroughSetOperation still pushes unique predicates below set operators. It rewrites every conjunct into every child with no containsUniqueFunction() guard (fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java:146-149). On UNION DISTINCT/INTERSECT/EXCEPT, moving rand() below the set op changes semantics.

Critical checkpoints:

  • Goal of current task: partially achieved. The touched rules are guarded, but the overall unique-function pushdown issue is not closed because the two parallel rewrites above remain unsafe.
  • Scope/simplicity: the local edits are small and focused.
  • Concurrency: not applicable.
  • Lifecycle/static init: not applicable.
  • Config: no new config.
  • Compatibility: no FE/BE protocol or storage compatibility impact.
  • Parallel code paths: not fully covered; PushDownJoinOtherCondition and PushDownFilterThroughSetOperation still need the same treatment.
  • Special conditions/comments: the added containsUniqueFunction() guards are appropriate where added.
  • Test coverage: good for the touched rules, but still missing coverage for the remaining join-other-condition and set-operation cases.
  • Test result files: the added shape files look consistent with the touched rewrites.
  • Observability/transactions/data writes/FE-BE variable passing: not applicable.
  • Performance: negligible impact.

User focus points: none provided.

Set<Slot> conjunctSlots = conjunct.getInputSlots();
if (commonGroupingSetExpressions.containsAll(conjunctSlots)) {
if (commonGroupingSetExpressions.containsAll(conjunctSlots)
&& !conjunct.containsUniqueFunction()) {
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.

Same non-idempotent gap still exists in PushDownJoinOtherCondition. That rule only checks predicate.getInputSlots(), so ON rand() > 0.5 (empty slot set) and ON t1.id + rand() > 5 are still pushed to one child for inner/cross joins. That changes evaluation from once per joined row to once per left/right input row: with one left row joining two right rows, the original plan can keep one pair and drop the other, while the rewritten plan can only keep both or none. Since this PR is fixing exactly this class of unique-function pushdown bug, I think we need the same containsUniqueFunction() guard there as well.

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.

See main PR comment — the three raised concerns (PushDownJoinOtherCondition, PushDownFilterThroughSetOperation, normalized-slot NLJ path) are respectively already fixed in this PR, already fixed in the follow-up #62754, and a pre-existing BETWEEN-expansion trade-off out of scope for this PR.

return ImmutableList.of(
logicalTopN(logicalUnion().when(union -> union.getQualifier() == Qualifier.ALL))
.when(topN -> topN.getOrderKeys().stream()
.noneMatch(key -> key.getExpr().containsUniqueFunction()))
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.

There's still a parallel set-operation rewrite with the same issue: PushDownFilterThroughSetOperation copies every conjunct into every child without checking containsUniqueFunction(). For SELECT * FROM (SELECT 1 UNION SELECT 1) u WHERE rand() > 0.5, the original plan evaluates rand() once after UNION DISTINCT (keep probability 0.5), but the rewritten plan evaluates it once per branch and the row survives if either branch passes (probability 0.75). INTERSECT and EXCEPT have the same problem, so this batch still leaves a wrong-result path open.

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.

See main PR comment — the three raised concerns (PushDownJoinOtherCondition, PushDownFilterThroughSetOperation, normalized-slot NLJ path) are respectively already fixed in this PR, already fixed in the follow-up #62754, and a pre-existing BETWEEN-expansion trade-off out of scope for this PR.

@yujun777 yujun777 force-pushed the fix/unique-fn-pushdown-batch1 branch from de43dd6 to 12d07aa Compare April 23, 2026 08:19
@yujun777
Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777
Copy link
Copy Markdown
Contributor Author

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Requesting changes.

Findings:

  1. I am not repeating the already-open inline thread on PushDownFilterThroughSetOperation, but that wrong-result path still appears unresolved. RuleSet.PUSH_DOWN_FILTERS still runs PushDownFilterThroughSetOperation before AddProjectForUniqueFunction, so rand()/uuid()/random_bytes() predicates above UNION DISTINCT / INTERSECT / EXCEPT can still be copied into each child and change semantics.
  2. I left one new inline comment for a distinct nested-loop join path: after a one-sided unique ON predicate is normalized behind slots, a later PUSH_DOWN_FILTERS pass can still push it into a child, so it is evaluated once per input row instead of once per joined pair.

Critical checkpoints:

  • Goal: Partially met. The direct Window / PartitionTopN / Join-other-conjunct cases are guarded, but the PR still leaves wrong-result paths for set operations and normalized nested-loop join predicates.
  • Minimality/focus: The patch is small and focused on the intended rules.
  • Concurrency: No concurrency or locking changes are involved.
  • Lifecycle/static initialization: No special lifecycle or initialization changes are involved.
  • Config: No new configuration is added.
  • Compatibility: No FE/BE protocol, symbol, or storage compatibility impact is involved.
  • Parallel code paths: Not fully covered; PushDownFilterThroughSetOperation and the later nested-loop join projection/pushdown path still bypass the new guard.
  • Special conditions/comments: The new containsUniqueFunction() checks are correct for direct AST forms, but they are not sufficient once unique expressions have been rewritten behind slots.
  • Test coverage: Improved, but still incomplete. The added FE UTs and regression suites cover the direct cases; they do not cover the remaining set-operation case or the slot-normalized nested-loop join case.
  • Test result files: The new .out files look auto-generated. The updated add_project_for_unique_function.out also exposes the remaining nested-loop join issue.
  • Observability: Not applicable for these optimizer rewrites.
  • Transaction/persistence/data writes/FE-BE variable passing: Not applicable.
  • Performance: The new guards are cheap; the remaining bad nested-loop plan also introduces redundant random evaluation/work on child filters.
  • Other issues: No additional user-provided review focus was provided.

// In addition, a predicate like `rand() > 0.5` has empty input slots,
// so `allCoveredBy(..., child.getOutputSet())` would otherwise wrongly
// return true because `Set.containsAll(emptySet) == true`.
if (otherConjunct.containsUniqueFunction()) {
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.

This fixes the direct AST case, but I think a normalized-slot path is still open. ProjectOtherJoinConditionForNestedLoopJoin can later rewrite a one-sided predicate like t2.id * random(1, 100) between 100 and 200 into a child-local boolean slot; the final PUSH_DOWN_FILTERS pass then sees only that slot and pushes it into the child because containsUniqueFunction() is now false. The updated regression-test/data/nereids_rules_p0/unique_function/add_project_for_unique_function.out still shows exactly that for join_1 (and likewise pushes random(1, 100) between 1 and 10 to the left child), which changes evaluation from once per joined pair to once per input row. This is distinct from the already-raised direct containsUniqueFunction() thread because the unique function has already been hidden behind a slot by the time the later pushdown runs.

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.

See main PR comment — the three raised concerns (PushDownJoinOtherCondition, PushDownFilterThroughSetOperation, normalized-slot NLJ path) are respectively already fixed in this PR, already fixed in the follow-up #62754, and a pre-existing BETWEEN-expansion trade-off out of scope for this PR.

yujun777 added a commit to yujun777/doris that referenced this pull request Apr 23, 2026
…ewrites

### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#62742

Problem Summary:

Two additional Nereids rewrite rules were not aware of `UniqueFunction`
(e.g. `rand()`, `uuid()`, `random()`) and could silently change the
semantics of SQL that uses such functions. This PR is batch 2 of the
`UniqueFunction` pushdown audit. The previously-reviewed batch 1 is in
apache#62742.

1. `PushDownFilterThroughSetOperation`
   Pushing a filter that references a `UniqueFunction` through
   `UNION DISTINCT` / `INTERSECT` / `EXCEPT` duplicates each unique-fn
   call per child, which changes result semantics (e.g. `rand() > 0.5`
   would be evaluated independently for each branch instead of once
   above the set op). `UNION ALL` remains safe, because the set op is a
   pure concatenation and every row evaluates the filter exactly once
   below or above. The rewrite now splits conjuncts: pushable conjuncts
   (`UNION ALL`, or not containing a unique function) go through the
   child branches; non-pushable conjuncts stay above the set op.

2. `ProjectOtherJoinConditionForNestedLoopJoin`
   The rule extracts sub-expressions of NLJ other conditions into
   child projects as aliases. An expression containing a unique
   function must not be materialized once above the scan and then
   referenced in the join, because that separates its evaluation from
   the join pair. Expressions that contain a unique function are now
   left inline in the other condition.

Both fixes follow the same pattern used in batch 1: an early no-op
branch when `expression.containsUniqueFunction()` is true.

### Release note

None

### Check List (For Author)

- Test: Regression test
- Behavior changed: No
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
yujun777 added a commit to yujun777/doris that referenced this pull request Apr 23, 2026
…ewrites

### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#62742

Problem Summary:

Two additional Nereids rewrite rules were not aware of `UniqueFunction`
(e.g. `rand()`, `uuid()`, `random()`) and could silently change the
semantics of SQL that uses such functions. This PR is batch 2 of the
`UniqueFunction` pushdown audit. The previously-reviewed batch 1 is in
apache#62742.

1. `PushDownFilterThroughSetOperation`
   Pushing a filter that references a `UniqueFunction` through
   `UNION DISTINCT` / `INTERSECT` / `EXCEPT` duplicates each unique-fn
   call per child, which changes result semantics (e.g. `rand() > 0.5`
   would be evaluated independently for each branch instead of once
   above the set op). `UNION ALL` remains safe, because the set op is a
   pure concatenation and every row evaluates the filter exactly once
   below or above. The rewrite now splits conjuncts: pushable conjuncts
   (`UNION ALL`, or not containing a unique function) go through the
   child branches; non-pushable conjuncts stay above the set op.

2. `ProjectOtherJoinConditionForNestedLoopJoin`
   The rule extracts sub-expressions of NLJ other conditions into
   child projects as aliases. An expression containing a unique
   function must not be materialized once above the scan and then
   referenced in the join, because that separates its evaluation from
   the join pair. Expressions that contain a unique function are now
   left inline in the other condition.

Both fixes follow the same pattern used in batch 1: an early no-op
branch when `expression.containsUniqueFunction()` is true.

### Release note

None

### Check List (For Author)

- Test: Regression test
- Behavior changed: No
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
yujun777 added a commit to yujun777/doris that referenced this pull request Apr 23, 2026
… SetOp predicate inference rewrites

### What problem does this PR solve?

Issue Number: N/A

Related PR: apache#62742

Problem Summary:

Three Nereids rewrite rules were not aware of `UniqueFunction`
(e.g. `rand()`, `uuid()`, `random()`) and could silently change the
semantics of SQL that uses such functions. This PR is batch 2 of the
`UniqueFunction` pushdown audit. The previously-reviewed batch 1 is in
apache#62742.

1. `PushDownFilterThroughSetOperation`
   Pushing a filter that references a `UniqueFunction` through
   `UNION DISTINCT` / `INTERSECT` / `EXCEPT` duplicates each unique-fn
   call per child, which changes result semantics (e.g. `rand() > 0.5`
   would be evaluated independently for each branch instead of once
   above the set op). `UNION ALL` remains safe, because the set op is a
   pure concatenation and every row evaluates the filter exactly once
   below or above. The rewrite now splits conjuncts: pushable conjuncts
   (`UNION ALL`, or not containing a unique function) go through the
   child branches; non-pushable conjuncts stay above the set op.

2. `ProjectOtherJoinConditionForNestedLoopJoin`
   The rule extracts sub-expressions of NLJ other conditions into
   child projects as aliases. An expression containing a unique
   function must not be materialized once above the scan and then
   referenced in the join, because that separates its evaluation from
   the join pair. Expressions that contain a unique function are now
   left inline in the other condition.

3. `InferPredicates` (EXCEPT / INTERSECT branches)
   `visitLogicalExcept` / `visitLogicalIntersect` substitute slots of
   the first child through the set-op output to sibling children so
   that predicates on the first child can be inferred onto sibling
   children. When the pulled-up predicate contains a unique function
   (e.g. `t1.id + rand() > 5`), the substitution rewrites it into a
   predicate on the sibling (`t2.id + rand() > 5`) and re-adds it,
   which re-evaluates `rand()` on a different set of rows. The JOIN
   path does not do this substitution, so its pre-existing slot
   subset check is sufficient. We now skip any inferred expression
   that contains a unique function inside the universal inference
   loop, which protects SetOp as well as any future consumer that
   does similar slot substitution.

All three fixes follow the same pattern used in batch 1: an early
no-op branch when `expression.containsUniqueFunction()` is true.

### Release note

None

### Check List (For Author)

- Test: Regression test
- Behavior changed: No
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yujun777
Copy link
Copy Markdown
Contributor Author

Thanks for the reviews. Consolidated response:

  1. PushDownJoinOtherCondition – false positive. The current HEAD of this PR already adds the containsUniqueFunction() guard at PushDownJoinOtherCondition.java:85-87. Any conjunct containing a unique function is routed into remainingOther and is never pushed to a single child. The empirical shape I verified before merging matches this: ON rand() > 0.5 and ON t1.id + rand() > 5 stay in the join's other-conjuncts list. The review comment appears to have analyzed the pre-change version of the file.

  2. PushDownFilterThroughSetOperation – valid and already fixed in a follow-up. This is the batch-2 rule in the audit and is addressed in [fix](nereids) Guard UniqueFunction in SetOperation and NLJ project rewrites #62754, which splits conjuncts so that UniqueFunction predicates above UNION DISTINCT / INTERSECT / EXCEPT remain above the set op while safe conjuncts continue to push down. Regression coverage for EXCEPT / INTERSECT / UNION DISTINCT with rand() / uuid() is added there.

  3. Normalized-slot NLJ path (ProjectOtherJoinConditionForNestedLoopJoin → later PUSH_DOWN_FILTERS). The root cause is Doris' BETWEEN rewrite expanding expr BETWEEN a AND b into expr >= a AND expr <= b, duplicating any UniqueFunction inside expr. Once duplicated, the later AddProjectForUniqueFunction.JoinRewrite materializes one alias into the left child project to keep the two bound checks consistent with a single value. Both options (keep duplicates inline per-pair, or alias into a child) lose some part of the original BETWEEN semantics that was already destroyed at the split step; the current choice is the less-bad option in practice. Full discussion is in the [fix](nereids) Guard UniqueFunction in SetOperation and NLJ project rewrites #62754 reply. This is a pre-existing trade-off in AddProjectForUniqueFunction, not something introduced by this PR's changes to PushDownJoinOtherCondition / PushDownFilterThroughWindow / PushDownFilterThroughPartitionTopN, so it is intentionally out of scope here.

@hello-stephen
Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 83.33% (5/6) 🎉
Increment coverage report
Complete coverage report

@yujun777 yujun777 force-pushed the fix/unique-fn-pushdown-batch1 branch from 12d07aa to 310308d Compare April 23, 2026 11:06
@yujun777
Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777
Copy link
Copy Markdown
Contributor Author

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Requesting changes because one blocking wrong-result issue remains.

Finding
1.
This new narrowing is not semantically safe. A one-sided unique predicate such as is still evaluated once per joined row in the original plan, but after this rewrite it is evaluated once per row and then reused for every matching left row. If one right row joins two left rows, the original query can keep one pair and drop the other, while the rewritten plan can only keep both or none. This is distinct from the existing thread because it goes through the path, and the updated regression outputs now explicitly expect that unsafe pushdown.

Critical Checkpoints

  • Goal / correctness: Partially met. The window, partition-topn, and empty-slot join fixes address the empty-input-slot bug, but still has a wrong-result path for one-sided unique predicates.
  • Scope / minimality: Not fully minimal. Narrowing the guard from to broadens behavior beyond the empty-slot fix.
  • Concurrency: Not applicable; these FE rewrite rules do not introduce new shared-state or locking behavior.
  • Lifecycle / static init: Not applicable.
  • Config changes: None.
  • Compatibility / persistence / FE-BE protocol: None.
  • Parallel code paths: Not fully covered. This filter-above-join path remains unsafe even after the fix.
  • Special conditions / comments: The new condition documents only the empty-slot case, but one-sided unique predicates are not safe either.
  • Test coverage: Added tests for window / partition top-N / join-other-condition, but the join-filter regression now asserts the unsafe pushed plan instead of catching it.
  • Test result files: New files are internally consistent with the current planner output, but encodes incorrect behavior for this case.
  • Observability: No new observability needed for these planner-only changes.
  • Data writes / transactions: Not applicable.
  • Other issues: No additional blocking issue beyond the above one.

User Focus

  • No additional user-provided focus points.

for (Expression p : filterPredicates) {
if (p.containsUniqueFunction()) {
Set<Slot> slots = p.collect(SlotReference.class::isInstance);
if (p.containsUniqueFunction() && slots.isEmpty()) {
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.

This guard is still too narrow. A one-sided predicate like above a is evaluated once per joined row in the original plan, but after pushing it into the right child it is evaluated once per row and then reused for every matching left row. With one right row joining two left rows, the original plan can keep one pair and drop the other, while the rewritten plan can only keep both or none. That makes this a wrong-result path distinct from the already-discussed thread, and the updated regression outputs now lock in that unsafe plan.

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.

This is addressed after the rebase to current upstream/master. PushDownFilterThroughJoin now checks p.containsVolatileExpression() before slot-based pushdown, so both empty-slot predicates like rand() > 0.1 and one-sided predicates like t2.id + rand() > 0.2 are kept in remainingPredicates above the join. I also updated the regression case comment to reflect that volatile predicates should stay above the join even when they reference only one side.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Requesting changes because one blocking wrong-result issue remains.

Findings

  1. fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughJoin.java:124
    The new && slots.isEmpty() narrowing is not semantically safe. A one-sided unique predicate such as WHERE t2.id + rand() > 0.2 is evaluated once per joined row in the original Filter(Join(...)) plan, but after this rewrite it is evaluated once per t2 row and then reused for every matching left row. If one right row joins two left rows, the original query can keep one pair and drop the other, while the rewritten plan can only keep both or none. This is distinct from the existing PushDownJoinOtherCondition review threads because it goes through the separate LogicalFilter(logicalJoin()) path, and the updated push_down_filter_through_join_3/4 regression outputs now explicitly expect that unsafe pushdown.

Open Questions / Assumptions

  • This review assumes Doris keeps the usual row-by-row semantics for non-deterministic predicates above a join. The existing guards in PushFilterInsideJoin, PushDownFilterThroughProject, and the rest of this PR all point to that same invariant.

Critical Checkpoints

  • Goal of the task: Partially accomplished. The window, partition-topn, and empty-slot join fixes address the empty-input-slot bug, but PushDownFilterThroughJoin still leaves a wrong-result path for one-sided unique predicates.
  • Small / clear / focused: Not fully. Narrowing the guard from p.containsUniqueFunction() to p.containsUniqueFunction() && slots.isEmpty() broadens behavior beyond the stated empty-slot fix.
  • Concurrency: Not applicable. These FE rewrite rules do not introduce new shared state, threads, or locks.
  • Lifecycle / static initialization: Not applicable.
  • Configuration changes: None.
  • Compatibility / persistence / FE-BE variable passing: None.
  • Functionally parallel code paths: Not fully covered. PushDownJoinOtherCondition got an empty-slot guard, but the separate filter-above-join path remains unsafe.
  • Special conditional checks: The new condition only documents the empty-slot case, while one-sided unique predicates are still not safe to move below the join.
  • Test coverage: Improved for window, partition-topn, and join-other-condition. Missing the corresponding negative coverage for PushDownFilterThroughJoin; the join regression now asserts the unsafe pushed plan instead.
  • Test result files: The new and updated .out files are internally consistent with current planner output, but push_down_filter_through_join_with_unique_function.out now encodes incorrect behavior for this case.
  • Observability: No additional observability is needed for these planner-only changes.
  • Transactions / data writes / persistence: Not applicable.
  • Performance: The pushed plan may reduce join work, but correctness has to win here because row-granularity evaluation changes.
  • Other issues: No additional blocking issue found beyond the one above.

User Focus

  • No additional user-provided focus points.

for (Expression p : filterPredicates) {
if (p.containsUniqueFunction()) {
Set<Slot> slots = p.collect(SlotReference.class::isInstance);
if (p.containsUniqueFunction() && slots.isEmpty()) {
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.

This new && slots.isEmpty() guard is still too narrow. A one-sided predicate like WHERE t2.id + rand() > 0.2 above a LogicalJoin is evaluated once per joined row in the original plan, but after pushing it into the right child it is evaluated once per t2 row and then reused for every matching left row. With one right row joining two left rows, the original plan can keep one pair and drop the other, while the rewritten plan can only keep both or none.

That makes this a wrong-result path distinct from the already-discussed PushDownJoinOtherCondition thread, and the updated push_down_filter_through_join_3/4 regression outputs now lock in that unsafe plan.

Suggested change
if (p.containsUniqueFunction() && slots.isEmpty()) {
if (p.containsUniqueFunction()) {

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.

This has been fixed by the current rebased code as well. The old narrowed guard no longer exists on this branch: PushDownFilterThroughJoin first routes any containsVolatileExpression() predicate to remainingPredicates, before checking input slots. Therefore a one-sided volatile predicate such as WHERE t2.id + rand() > 0.2 is not pushed into the right child and remains evaluated per joined row. I updated the test comment accordingly.

@github-actions github-actions Bot dismissed their stale review April 23, 2026 11:17

Superseded by corrected review 4161915500 after shell-escaping error in the automation payload.

@hello-stephen
Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 66.67% (6/9) 🎉
Increment coverage report
Complete coverage report

@hello-stephen
Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 88.89% (8/9) 🎉
Increment coverage report
Complete coverage report

yujun777 and others added 2 commits June 1, 2026 16:20
…in pushdown

Issue Number: N/A

Related PR: apache#62705

Problem Summary:

Following up on apache#62705, several Nereids rewrite rules still moved
predicates / conjuncts containing non-idempotent (unique) functions
such as rand() / uuid() / random_bytes() across operator boundaries
in ways that changed query semantics. The common root cause is that a
predicate like `rand() > 0.5` has an empty input-slot set, so the
`containsAll(emptySet)` / `allMatch(emptyStream)` guards used by these
rules silently returned true and allowed the push down.

This PR adds guards to the reachable cases:

1. PushDownFilterThroughWindow: skip conjuncts with unique functions.
   Pushing a unique predicate below a window operator re-samples the
   base rows and changes every window-function value.

2. PushDownFilterThroughPartitionTopN: same as Window — skip unique
   conjuncts in the split loop.

3. PushDownJoinOtherCondition: keep conjuncts with unique functions AND
   empty input slots in the join's otherJoinConjuncts. `rand() > 0.5`
   with no slots would otherwise be pushed arbitrarily to the left
   child because `leftOutput.containsAll(emptySet)` is always true.
   When the unique conjunct has side-specific input slots (e.g.
   `t1.id + rand() > 0.5`), push-down is still allowed — output
   cardinality expectation is preserved and pre-join evaluation is
   what users typically want for sampling predicates.

4. PushDownFilterThroughJoin: same as (3) for the filter-through-join
   path. Also protects the "no slots → duplicate to both children"
   branch, which would otherwise produce two independent random draws
   per row.

Other candidates that were initially explored (Repeat / Sort elimination
/ TopN-through-Union) were empirically verified to be unreachable due to
upstream normalization (PushDownFilterThroughAggregation already blocks
unique conjuncts before they reach Repeat; NormalizeSort wraps rand() in
a Project; TopN order keys are already materialized Slots), so no dead
guards were added for those.

Fix wrong results when `rand() > 0.5`-style predicates with empty input
slots were pushed below Window / PartitionTopN, or moved out of a Join's
other-conjuncts / above-Join filter onto one of the join inputs.

- Test:
    - Regression Test: 3 suites under
      regression-test/suites/nereids_rules_p0/unique_function/
      (window, partition_topn, join_other_condition) plus new cases in
      push_down_filter_through_join_with_unique_function for the
      side-specific unique-slot vs empty-slot distinction.
- Behavior changed: Yes (queries that previously returned incorrect
  results due to unsafe pushdown of unique-function predicates across
  Window / PartitionTopN / Join boundaries will now return correct
  results).
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt the unique function pushdown guards to the upstream volatile expression API and update CastException to the current AnalysisException constructors.

Key changes:

- Replace removed containsUniqueFunction checks with containsVolatileExpression in pushdown rules.

- Update CastException to use the current AnalysisException constructor.

Unit Test:

- FE Core Maven compile without build cache

- build.sh --fe
@yujun777 yujun777 force-pushed the fix/unique-fn-pushdown-batch1 branch from 310308d to abb6587 Compare June 1, 2026 08:34
yujun777 and others added 3 commits June 1, 2026 16:43
Update the regression case comment to match the current PushDownFilterThroughJoin behavior after rebasing to upstream master.

Key changes:

- Document that volatile predicates stay above joins even when they reference one side.

Unit Test:

- Not run; comment-only change.
… SetOp predicate inference rewrites

Issue Number: N/A

Related PR: apache#62742

Problem Summary:

Three Nereids rewrite rules were not aware of `UniqueFunction`
(e.g. `rand()`, `uuid()`, `random()`) and could silently change the
semantics of SQL that uses such functions. This PR is batch 2 of the
`UniqueFunction` pushdown audit. The previously-reviewed batch 1 is in

1. `PushDownFilterThroughSetOperation`
   Pushing a filter that references a `UniqueFunction` through
   `UNION DISTINCT` / `INTERSECT` / `EXCEPT` duplicates each unique-fn
   call per child, which changes result semantics (e.g. `rand() > 0.5`
   would be evaluated independently for each branch instead of once
   above the set op). `UNION ALL` remains safe, because the set op is a
   pure concatenation and every row evaluates the filter exactly once
   below or above. The rewrite now splits conjuncts: pushable conjuncts
   (`UNION ALL`, or not containing a unique function) go through the
   child branches; non-pushable conjuncts stay above the set op.

2. `ProjectOtherJoinConditionForNestedLoopJoin`
   The rule extracts sub-expressions of NLJ other conditions into
   child projects as aliases. An expression containing a unique
   function must not be materialized once above the scan and then
   referenced in the join, because that separates its evaluation from
   the join pair. Expressions that contain a unique function are now
   left inline in the other condition.

3. `InferPredicates` (EXCEPT / INTERSECT branches)
   `visitLogicalExcept` / `visitLogicalIntersect` substitute slots of
   the first child through the set-op output to sibling children so
   that predicates on the first child can be inferred onto sibling
   children. When the pulled-up predicate contains a unique function
   (e.g. `t1.id + rand() > 5`), the substitution rewrites it into a
   predicate on the sibling (`t2.id + rand() > 5`) and re-adds it,
   which re-evaluates `rand()` on a different set of rows. The JOIN
   path does not do this substitution, so its pre-existing slot
   subset check is sufficient. We now skip any inferred expression
   that contains a unique function inside the universal inference
   loop, which protects SetOp as well as any future consumer that
   does similar slot substitution.

All three fixes follow the same pattern used in batch 1: an early
no-op branch when `expression.containsUniqueFunction()` is true.

None

- Test: Regression test
- Behavior changed: No
- Does this need documentation: No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the original volatile-predicate handling for join other conditions while keeping slot-free cases from being pushed to an arbitrary child.

Key changes:
- Keep only slot-free volatile join predicates in join otherCondition.
- Preserve performance-oriented pushdown for side-specific volatile ON predicates.
- Update FE unit test and regression expectations to match the restored behavior.

Unit Test:
- bash ./run-fe-ut.sh --run PushDownJoinOtherConditionTest
- MVN_OPT=-Dmaven.build.cache.enabled=false ./build.sh --fe
- ./run-regression-test.sh --run -d nereids_rules_p0/unique_function -s push_down_filter_through_join_with_unique_function,push_down_filter_through_partition_topn_with_unique_function,push_down_filter_through_window_with_unique_function,push_down_join_other_condition_with_unique_function,infer_predicates_set_op_with_unique_function,project_other_join_condition_for_nlj_with_unique_function,push_down_filter_through_set_operation_with_unique_function
@yujun777
Copy link
Copy Markdown
Contributor Author

yujun777 commented Jun 1, 2026

run buildall

@hello-stephen
Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 71.15% (37/52) 🎉
Increment coverage report
Complete coverage report

@morrySnow
Copy link
Copy Markdown
Contributor

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

I reviewed the current GitHub PR diff and did not find a new blocking issue distinct from the existing review threads.

Critical checkpoint conclusions:

  • Goal/test coverage: The PR addresses volatile expression pushdown/inference hazards in Nereids rules, with FE unit/regression coverage for the affected paths.
  • Scope: The current GitHub diff is focused on Nereids volatile-expression handling and related tests/comments.
  • Concurrency/lifecycle/config/compatibility: No new concurrency, lifecycle, configuration, or serialization compatibility issue found.
  • Parallel code paths: Join filter pushdown, join other-condition pushdown, set-operation filter pushdown, NLJ project extraction, and set-op predicate inference were checked. No additional unreported instance found beyond the existing threads.
  • Data correctness: The current guarded paths preserve volatile predicate evaluation granularity according to the behavior documented by this PR; the known side-specific ON-predicate concern is already covered by existing review discussion and was not duplicated.
  • Tests/results: Added regression outputs appear consistent with the changed plans; no handwritten-result concern was identified from the review context.
  • Observability/performance: No additional observability or performance blocker found.

User focus: no additional user-provided review focus was supplied.

@hello-stephen
Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 23.58% (50/212) 🎉
Increment coverage report
Complete coverage report

@hello-stephen
Copy link
Copy Markdown
Contributor

TPC-H: Total hot run time: 29246 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 141e4bfc520388c1f72728f0004eab77e5fd35f0, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17609	4126	4055	4055
q2	q3	10815	1379	804	804
q4	4685	476	341	341
q5	7554	890	594	594
q6	183	175	137	137
q7	773	863	632	632
q8	9434	1593	1592	1592
q9	5870	4479	4472	4472
q10	6829	1812	1535	1535
q11	430	274	251	251
q12	636	424	291	291
q13	18160	3370	2773	2773
q14	270	252	236	236
q15	q16	815	775	705	705
q17	982	885	956	885
q18	7037	5693	5660	5660
q19	1323	1209	1142	1142
q20	521	409	265	265
q21	6246	2772	2562	2562
q22	490	395	314	314
Total cold run time: 100662 ms
Total hot run time: 29246 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	5263	5053	5005	5005
q2	q3	4864	5250	4683	4683
q4	2091	2224	1387	1387
q5	4815	4779	4913	4779
q6	234	179	136	136
q7	1870	1751	1586	1586
q8	2400	2129	2105	2105
q9	8251	7938	7492	7492
q10	4735	4674	4250	4250
q11	524	384	353	353
q12	727	737	525	525
q13	2971	3410	2744	2744
q14	261	278	254	254
q15	q16	677	697	612	612
q17	1277	1253	1253	1253
q18	7168	6671	6733	6671
q19	1122	1088	1129	1088
q20	2205	2218	1935	1935
q21	5239	4979	4429	4429
q22	524	450	424	424
Total cold run time: 57218 ms
Total hot run time: 51711 ms

@hello-stephen
Copy link
Copy Markdown
Contributor

TPC-DS: Total hot run time: 170126 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 141e4bfc520388c1f72728f0004eab77e5fd35f0, data reload: false

query5	4329	641	522	522
query6	329	226	199	199
query7	4222	589	314	314
query8	343	239	242	239
query9	8818	4001	4013	4001
query10	466	337	303	303
query11	5774	2338	2218	2218
query12	182	138	130	130
query13	1352	617	461	461
query14	6144	5424	5152	5152
query14_1	4468	4447	4430	4430
query15	214	208	186	186
query16	1018	460	487	460
query17	1157	761	628	628
query18	2731	511	376	376
query19	250	210	167	167
query20	143	135	134	134
query21	219	140	124	124
query22	13641	13588	13573	13573
query23	17401	16683	16174	16174
query23_1	16393	16379	16396	16379
query24	7400	1780	1314	1314
query24_1	1269	1312	1331	1312
query25	584	514	457	457
query26	1325	313	181	181
query27	2676	558	357	357
query28	4364	2037	2016	2016
query29	1008	643	538	538
query30	320	243	201	201
query31	1140	1080	973	973
query32	91	78	77	77
query33	562	406	286	286
query34	1181	1150	661	661
query35	765	792	696	696
query36	1412	1443	1298	1298
query37	153	103	89	89
query38	3171	3168	3145	3145
query39	936	915	901	901
query39_1	874	862	900	862
query40	233	149	126	126
query41	70	62	63	62
query42	118	113	110	110
query43	328	336	287	287
query44	
query45	216	204	202	202
query46	1118	1194	753	753
query47	2391	2430	2224	2224
query48	396	426	281	281
query49	634	523	393	393
query50	994	369	265	265
query51	4328	4316	4308	4308
query52	106	105	96	96
query53	248	286	210	210
query54	323	273	258	258
query55	94	94	89	89
query56	333	318	305	305
query57	1459	1446	1318	1318
query58	305	271	268	268
query59	1586	1682	1418	1418
query60	329	327	305	305
query61	158	157	159	157
query62	709	652	587	587
query63	245	205	204	204
query64	2366	790	656	656
query65	
query66	1675	475	357	357
query67	29845	29815	28918	28918
query68	
query69	453	334	300	300
query70	1077	1013	949	949
query71	324	276	274	274
query72	2989	2688	2429	2429
query73	817	814	410	410
query74	5102	4946	4754	4754
query75	2730	2597	2284	2284
query76	2313	1150	773	773
query77	399	405	336	336
query78	12452	12579	11933	11933
query79	1481	1089	755	755
query80	671	532	467	467
query81	463	279	247	247
query82	1375	160	126	126
query83	360	280	248	248
query84	270	142	115	115
query85	901	544	460	460
query86	402	349	316	316
query87	3474	3366	3207	3207
query88	3635	2719	2722	2719
query89	460	387	343	343
query90	1985	202	183	183
query91	180	173	141	141
query92	84	76	77	76
query93	1469	1414	818	818
query94	553	356	306	306
query95	671	472	347	347
query96	1078	796	369	369
query97	2749	2708	2624	2624
query98	244	244	237	237
query99	1160	1169	1048	1048
Total cold run time: 254746 ms
Total hot run time: 170126 ms

ProjectOtherJoinConditionForNestedLoopJoin skipped the whole expression when a join condition contained a volatile function. This avoided changing volatile evaluation granularity, but also prevented deterministic child expressions from being extracted, which changed existing join case-when plans.

Key changes:

- Keep volatile join expressions inline while still visiting deterministic child expressions.

- Update the unique-function regression expectation for the resulting project shape.

Unit Test:

- Regression test: add_project_for_unique_function, join_extract_or_from_case_when

- FE checkstyle: fe-core validate with dependent modules
@yujun777
Copy link
Copy Markdown
Contributor Author

yujun777 commented Jun 2, 2026

run buildall

@hello-stephen
Copy link
Copy Markdown
Contributor

TPC-H: Total hot run time: 28968 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit ece53853ea7620ca5434d974848d02e9ab411db8, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17637	4039	4020	4020
q2	q3	10740	1381	803	803
q4	4696	472	352	352
q5	7519	865	599	599
q6	180	171	138	138
q7	779	862	633	633
q8	9418	1489	1532	1489
q9	5746	4511	4512	4511
q10	6776	1807	1568	1568
q11	443	276	249	249
q12	633	427	288	288
q13	18086	3408	2774	2774
q14	267	260	241	241
q15	q16	808	776	708	708
q17	952	935	937	935
q18	6931	5807	5555	5555
q19	1321	1257	1072	1072
q20	504	400	263	263
q21	5927	2600	2466	2466
q22	421	354	304	304
Total cold run time: 99784 ms
Total hot run time: 28968 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4323	4263	4262	4262
q2	q3	4535	4968	4316	4316
q4	2086	2241	1393	1393
q5	4431	4338	4331	4331
q6	231	175	129	129
q7	1748	1646	1593	1593
q8	2878	2258	2165	2165
q9	8184	8217	8090	8090
q10	4802	4803	4276	4276
q11	568	410	387	387
q12	751	773	544	544
q13	3343	3691	3042	3042
q14	303	311	288	288
q15	q16	706	722	646	646
q17	1345	1327	1323	1323
q18	8188	7456	7321	7321
q19	1218	1149	1150	1149
q20	2232	2240	1999	1999
q21	5305	4571	4485	4485
q22	503	468	437	437
Total cold run time: 57680 ms
Total hot run time: 52176 ms

@hello-stephen
Copy link
Copy Markdown
Contributor

TPC-DS: Total hot run time: 170725 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit ece53853ea7620ca5434d974848d02e9ab411db8, data reload: false

query5	4327	663	517	517
query6	351	234	215	215
query7	4271	589	311	311
query8	346	245	227	227
query9	8821	4029	4043	4029
query10	460	351	305	305
query11	5790	2386	2133	2133
query12	189	131	132	131
query13	1343	618	446	446
query14	6062	5478	5125	5125
query14_1	4457	4450	4505	4450
query15	212	210	190	190
query16	993	449	455	449
query17	1164	754	631	631
query18	2471	490	366	366
query19	227	210	172	172
query20	145	142	138	138
query21	220	146	120	120
query22	13681	13668	13436	13436
query23	17393	16599	16203	16203
query23_1	16220	16377	16403	16377
query24	7463	1788	1300	1300
query24_1	1327	1326	1296	1296
query25	611	510	460	460
query26	1315	319	176	176
query27	2703	569	353	353
query28	4429	1995	1985	1985
query29	1055	653	546	546
query30	306	226	190	190
query31	1145	1081	957	957
query32	93	74	75	74
query33	547	355	318	318
query34	1212	1144	669	669
query35	768	799	693	693
query36	1417	1394	1213	1213
query37	158	105	93	93
query38	3199	3132	3062	3062
query39	935	917	904	904
query39_1	884	893	862	862
query40	236	154	129	129
query41	65	63	63	63
query42	121	115	111	111
query43	358	337	287	287
query44	
query45	217	207	200	200
query46	1085	1167	743	743
query47	2399	2399	2308	2308
query48	406	422	304	304
query49	627	502	376	376
query50	992	355	253	253
query51	4330	4375	4286	4286
query52	108	113	96	96
query53	258	284	206	206
query54	315	270	249	249
query55	93	93	85	85
query56	300	314	314	314
query57	1459	1438	1323	1323
query58	309	276	268	268
query59	1601	1682	1430	1430
query60	324	337	316	316
query61	159	157	154	154
query62	716	656	581	581
query63	246	206	236	206
query64	2461	782	626	626
query65	
query66	1735	482	355	355
query67	29794	29744	29686	29686
query68	
query69	473	340	302	302
query70	997	1039	996	996
query71	312	277	268	268
query72	2961	2623	2388	2388
query73	858	767	433	433
query74	5120	4948	4766	4766
query75	2697	2608	2255	2255
query76	2283	1197	778	778
query77	390	420	352	352
query78	12500	12300	11859	11859
query79	1467	1070	761	761
query80	736	519	473	473
query81	478	281	243	243
query82	1380	156	119	119
query83	314	273	244	244
query84	257	139	115	115
query85	907	557	455	455
query86	419	343	328	328
query87	3452	3369	3239	3239
query88	3610	2729	2746	2729
query89	453	397	342	342
query90	1938	187	178	178
query91	179	168	136	136
query92	79	80	72	72
query93	1468	1448	912	912
query94	593	357	308	308
query95	687	488	364	364
query96	1061	859	349	349
query97	2743	2774	2621	2621
query98	241	228	221	221
query99	1153	1186	1056	1056
Total cold run time: 254593 ms
Total hot run time: 170725 ms

@hello-stephen
Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 71.15% (37/52) 🎉
Increment coverage report
Complete coverage report

@hello-stephen
Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 33.78% (50/148) 🎉
Increment coverage report
Complete coverage report

// children. Side-specific volatile predicates are still pushed down for
// performance, matching PostgreSQL's treatment of single-relation INNER
// JOIN ON quals as base restrictions.
if (otherConjunct.containsVolatileExpression()
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.

This guard is still too narrow for volatile ON predicates. A one-sided condition such as t1.id + rand() > 0.5 has non-empty input slots, so it falls through to allCoveredBy(...) and is pushed into one child. For an INNER/CROSS JOIN, though, the ON predicate is evaluated per joined pair; after this rewrite the random value is evaluated once per input row and reused for every matching row from the other side. With one left row joining two right rows, the original plan can keep one pair and drop the other, while the pushed plan can only keep both or none. PushDownFilterThroughJoin already keeps all containsVolatileExpression() predicates above the join for this reason, so this path should not push side-specific volatile ON conjuncts either.

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.

Thanks, addressed in 0e52387. PushDownJoinOtherCondition now keeps every containsVolatileExpression() ON conjunct in otherJoinConjuncts, including one-sided predicates such as t1.id + rand() > 0.5, so the join-pair evaluation granularity is preserved. For BETWEEN-expanded repeated volatile expressions, AddProjectForVolatileExpression materializes one value later: right-only expressions are projected on the right child, otherwise it falls back to the left child, and volatile expressions whose own slots span both join children are skipped because join has no pair-scope project. I also added FE unit tests and explain-shape regression cases for the one-sided volatile ON predicate and left/right/slot-free BETWEEN cases.

Keep volatile ON predicates at join scope so their evaluation granularity is
not changed by predicate pushdown. Repeated volatile expressions are still
materialized by AddProjectForVolatileExpression, using the right child when the
expression is right-only and the left child as the default fallback.

Key changes:
- Keep all volatile ON predicates in otherJoinConjuncts.
- Materialize repeated join volatile expressions on the legal child side.
- Skip volatile expressions whose own input slots span both join children.
- Add FE unit tests and explain shape regression coverage for volatile JOIN cases.

Unit Test:
- AddProjectForVolatileExpressionTest
- PushDownJoinOtherConditionTest
- push_down_join_other_condition_with_unique_function regression
- mvn validate
@yujun777
Copy link
Copy Markdown
Contributor Author

yujun777 commented Jun 2, 2026

run buildall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants