Uh oh!
There was an error while loading. Please reload this page.
[BugFix] Restore dedup pushdown when combined with WHERE clause (#5482) - #5488
Conversation
…search-project#5482) Run PPLSimplifyDedupRule before FilterMergeRule in the HEP optimizer so the bucket-non-null filter PPL emits for dedup is matched as-is. With the previous order, an upstream user where filter sat adjacent to the bucket-non-null filter; FilterMergeRule fired first and merged them into a conjunction that no longer satisfied PPLSimplifyDedupRule's operand predicate, defeating dedup pushdown to the shard. Use sequential addRuleInstance phases for explicit ordering rather than addRuleCollection, which is documented as non-deterministic in firing order. Adds two regression tests in CalcitePPLDedupTest: one that asserts LogicalDedup is produced under the fixed order, and one that pins the buggy behavior under the swapped order. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com>
PR Reviewer Guide 🔍(Review updated until commit 38fc6c3)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 38fc6c3 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 95e2aeb
Suggestions up to commit a66ad5b
Suggestions up to commit f67458f
Suggestions up to commit 0bf8f7c
Suggestions up to commit 19f506d
|
RyanL1997
left a comment
There was a problem hiding this comment.
Hi @ryan-gh-bot , thanks for the change, left some reviews.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…search-project#5488) Per maintainer review feedback, the regression-test JavaDoc for testDedupAfterWhereProducesLogicalDedup mentioned the originating issue URL. The remaining JavaDoc paragraphs already describe the bug shape and the rule-ordering invariant, so the explicit issue link is unnecessary noise. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com>
Persistent review updated to latest commit 19f506d |
RyanL1997
commented
May 29, 2026
@ryan-gh-bot the rule-reordering fix works but treats a symptom rather than the root cause. The actual brittleness is in Please revise to take option 1 from the issue instead:
Please sanity-check that the loosened operand can't accidentally match user-written |
ryan-gh-bot
commented
May 29, 2026
Working on |
…oject#5488) Address review feedback on opensearch-project#5488: extend mayBeFilterFromBucketNonNull to accept the merged conjunction shape FilterMergeRule produces, so PPLSimplifyDedupRule fires regardless of whether FilterMergeRule has already merged the user where clause into the bucket-non-null filter. PPLSimplifyDedupRule.apply now splits the bottom filter into IS_NOT_NULL conjuncts on partition keys (absorbed into LogicalDedup semantics) and any remaining conjuncts (preserved as a separate filter below the new LogicalDedup), so a user predicate that was folded in is no longer dropped. With the operand predicate order-independent, the HEP rule order is no longer a load-bearing invariant. Revert the addRuleCollection -> addRuleInstance change in CalciteToolsHelper.HEP_PROGRAM that the previous patch introduced. Replace the regression test that pinned the buggy rule order with one that asserts the user-visible contract: with where preceding dedup, a LogicalDedup is produced and the user predicate is preserved regardless of which order FilterMergeRule and PPLSimplifyDedupRule fire. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com>
ryan-gh-bot
commented
May 29, 2026
Done — pushed |
Persistent review updated to latest commit 0bf8f7c |
I'm weighing the pros and cons of these two options:
|
penghuo
left a comment
There was a problem hiding this comment.
- Add end-to-end test asserts dedup is actually pushed down when where precedes dedup.
- Refactor PR description to match the implementation.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Per @penghuo review: PlanUtils.java - Revert mayBeFilterFromBucketNonNull to the original ternary form; drop the early-return refactor (no behavior change, just cleaner). - Drop the !rexCall.getOperands().isEmpty() guard before .get(0): IS NOT NULL is always unary in Calcite, so the check is dead. - Trim the JavaDoc to the essentials (un-merged vs merged-AND shape; concrete partition-key match happens in PPLSimplifyDedupRule#apply). - Promote isNotNullOnRef from package-private to public so the dedup rule can reuse it from a different package. PPLSimplifyDedupRule.java - isNotNullOnPartitionKey now delegates the IS NOT NULL($ref) structural check to PlanUtils.isNotNullOnRef and adds the partition-key index check on top. CalciteExplainIT.java - Add testDedupAfterWherePushDown: an end-to-end regression that runs the shape `... | where <pred> | dedup <field>` and asserts (a) LogicalDedup appears in the explain output (PPLSimplifyDedupRule fired even after FilterMergeRule had a chance to merge the two filters), and (b) EnumerableWindow does NOT appear (the in-memory ROW_NUMBER fallback the bug caused is gone). Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Persistent review updated to latest commit ae86ece |
Hi @penghuo, thanks for the feedback:
added
PR description refactored — now describes the actual implementation (loosen predicate at source, not reorder HEP rules) instead of the bot's earlier reorder approach. |
Walked the axes against
Net: this PR covers the |
RyanL1997
commented
Jun 5, 2026
Checking on the CI failures |
7725e0b to
f67458fComparePersistent review updated to latest commit 7725e0b |
Persistent review updated to latest commit f67458f |
PPLSimplifyDedupRule correctly produces Dedup -> Filter(user where) -> Scan when a `where` precedes `dedup`. The Filter between Dedup and Scan blocks DedupPushdownRule's strict Dedup -> Project -> Scan operand chain, so Volcano falls back to PPLDedupConvertRule and the plan ends up with an in-memory ROW_NUMBER window instead of the pushed-down composite + top_hits aggregation. Add a WITH_FILTER operand variant to DedupPushdownRule that matches Dedup -> Filter -> Scan, pushes the filter into the scan, then runs the standard apply() on the resulting Dedup -> Project -> Scan shape. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
f67458f to
a66ad5bComparePersistent review updated to latest commit a66ad5b |
pushDownFilter returns a Filter (not a CalciteLogicalIndexScan) when the predicate analyzer can only partially push the condition. The previous cast would have thrown ClassCastException in that case. Use an instanceof-pattern check so the rule bails out cleanly and leaves the plan untouched, letting other rules handle the residual. Also drop a stale issue-link reference from a test comment. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Persistent review updated to latest commit 95e2aeb |
RyanL1997
commented
Jun 5, 2026
The current CI failure is unrelated. The failure is: |
Reflowed the JavaDoc on testDedupAfterWhereProducesLogicalDedupWithProductionHepProgram to match Google Java Format's preferred line break, fixing the spotlessJavaCheck violation that failed the unit-test matrix on CI. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Persistent review updated to latest commit 38fc6c3 |
Uh oh!
There was an error while loading. Please reload this page.
…search-project#5482) (opensearch-project#5488) * [BugFix] Restore dedup pushdown when combined with WHERE clause (opensearch-project#5482) Run PPLSimplifyDedupRule before FilterMergeRule in the HEP optimizer so the bucket-non-null filter PPL emits for dedup is matched as-is. With the previous order, an upstream user where filter sat adjacent to the bucket-non-null filter; FilterMergeRule fired first and merged them into a conjunction that no longer satisfied PPLSimplifyDedupRule's operand predicate, defeating dedup pushdown to the shard. Use sequential addRuleInstance phases for explicit ordering rather than addRuleCollection, which is documented as non-deterministic in firing order. Adds two regression tests in CalcitePPLDedupTest: one that asserts LogicalDedup is produced under the fixed order, and one that pins the buggy behavior under the swapped order. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com> * [BugFix] Drop issue-link reference from regression-test JavaDoc (opensearch-project#5488) Per maintainer review feedback, the regression-test JavaDoc for testDedupAfterWhereProducesLogicalDedup mentioned the originating issue URL. The remaining JavaDoc paragraphs already describe the bug shape and the rule-ordering invariant, so the explicit issue link is unnecessary noise. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com> * [BugFix] Make dedup simplify operand order-independent (opensearch-project#5488) Address review feedback on opensearch-project#5488: extend mayBeFilterFromBucketNonNull to accept the merged conjunction shape FilterMergeRule produces, so PPLSimplifyDedupRule fires regardless of whether FilterMergeRule has already merged the user where clause into the bucket-non-null filter. PPLSimplifyDedupRule.apply now splits the bottom filter into IS_NOT_NULL conjuncts on partition keys (absorbed into LogicalDedup semantics) and any remaining conjuncts (preserved as a separate filter below the new LogicalDedup), so a user predicate that was folded in is no longer dropped. With the operand predicate order-independent, the HEP rule order is no longer a load-bearing invariant. Revert the addRuleCollection -> addRuleInstance change in CalciteToolsHelper.HEP_PROGRAM that the previous patch introduced. Replace the regression test that pinned the buggy rule order with one that asserts the user-visible contract: with where preceding dedup, a LogicalDedup is produced and the user predicate is preserved regardless of which order FilterMergeRule and PPLSimplifyDedupRule fire. Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com> * Address review comments on opensearch-project#5488 Per @penghuo review: PlanUtils.java - Revert mayBeFilterFromBucketNonNull to the original ternary form; drop the early-return refactor (no behavior change, just cleaner). - Drop the !rexCall.getOperands().isEmpty() guard before .get(0): IS NOT NULL is always unary in Calcite, so the check is dead. - Trim the JavaDoc to the essentials (un-merged vs merged-AND shape; concrete partition-key match happens in PPLSimplifyDedupRule#apply). - Promote isNotNullOnRef from package-private to public so the dedup rule can reuse it from a different package. PPLSimplifyDedupRule.java - isNotNullOnPartitionKey now delegates the IS NOT NULL($ref) structural check to PlanUtils.isNotNullOnRef and adds the partition-key index check on top. CalciteExplainIT.java - Add testDedupAfterWherePushDown: an end-to-end regression that runs the shape `... | where <pred> | dedup <field>` and asserts (a) LogicalDedup appears in the explain output (PPLSimplifyDedupRule fired even after FilterMergeRule had a chance to merge the two filters), and (b) EnumerableWindow does NOT appear (the in-memory ROW_NUMBER fallback the bug caused is gone). Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Push user where filter into scan when blocking dedup pushdown PPLSimplifyDedupRule correctly produces Dedup -> Filter(user where) -> Scan when a `where` precedes `dedup`. The Filter between Dedup and Scan blocks DedupPushdownRule's strict Dedup -> Project -> Scan operand chain, so Volcano falls back to PPLDedupConvertRule and the plan ends up with an in-memory ROW_NUMBER window instead of the pushed-down composite + top_hits aggregation. Add a WITH_FILTER operand variant to DedupPushdownRule that matches Dedup -> Filter -> Scan, pushes the filter into the scan, then runs the standard apply() on the resulting Dedup -> Project -> Scan shape. Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Bail when filter is only partially pushable pushDownFilter returns a Filter (not a CalciteLogicalIndexScan) when the predicate analyzer can only partially push the condition. The previous cast would have thrown ClassCastException in that case. Use an instanceof-pattern check so the rule bails out cleanly and leaves the plan untouched, letting other rules handle the residual. Also drop a stale issue-link reference from a test comment. Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Apply spotless formatting to dedup unit test Reflowed the JavaDoc on testDedupAfterWhereProducesLogicalDedupWithProductionHepProgram to match Google Java Format's preferred line break, fixing the spotlessJavaCheck violation that failed the unit-test matrix on CI. Signed-off-by: Jialiang Liang <jiallian@amazon.com> --------- Signed-off-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> Co-authored-by: ryan-gh-bot <ryan-gh-bot@users.noreply.github.com> Co-authored-by: Jialiang Liang <jiallian@amazon.com>
Description
PPL
dedupis initially compiled into aROW_NUMBER() OVER (PARTITION BY field)window pattern with anIS NOT NULL(field)bucket-non-null filter directly above the scan.PPLSimplifyDedupRulefolds that pattern into aLogicalDedupwhichDedupPushdownRulethen rewrites into acomposite+top_hitsaggregation against the shard.The HEP program registers
FilterMergeRule.DEFAULTandPPLSimplifyDedupRule.DEDUP_SIMPLIFY_RULEviaaddRuleCollection. When a userwhereprecedesdedup, the user's filter sits adjacent to the bucket-non-null filter;FilterMergeRulecan merge them intoAND(IS NOT NULL(field), <user predicate>). The previousmayBeFilterFromBucketNonNulloperand predicate required every conjunct of the AND to beIS NOT NULL($ref)(allMatch), so the merged shape failed the match.PPLSimplifyDedupRulenever fired, noLogicalDedupwas produced, andDedupPushdownRulehad nothing to match — dedup fell through to the in-memoryROW_NUMBERwindow form on the coordinator, which exceeds timeouts on large indices.Fix
Loosen the operand predicate at the source rather than reordering HEP rules.
PlanUtils.mayBeFilterFromBucketNonNullnow accepts the un-mergedIS NOT NULL($ref)shape or anyANDwhose conjuncts contain at least oneIS NOT NULL($ref)(anyMatchinstead ofallMatch). The predicate is order-independent — the simplify rule fires whether or notFilterMergeRuleran first.PPLSimplifyDedupRule.applysplits the matched filter's conjuncts into (a) theIS NOT NULLon a partition key (absorbed byLogicalDedup) and (b) everything else (emitted as a separate filter belowLogicalDedup), so any user predicate folded into the AND is preserved verbatim. A defensive bail-out: if noIS NOT NULLon a partition key is present in the matched filter, the rule returns without transforming.PlanUtils.isNotNullOnRefis promoted topublicsoPPLSimplifyDedupRule.isNotNullOnPartitionKeycan reuse it.Verification
CalcitePPLDedupTest.testDedupAfterWhereProducesLogicalDedupruns the rule sequence (PPLSimplifyDedupRule, FilterMergeRule) on awhere+dedupplan and asserts thatLogicalDedupis produced regardless of rule order. The user predicate appears as a separate filter belowLogicalDedup, ready for downstream pushdown:CalcitePPLDedupTest.testDedupAfterWhereProducesLogicalDedupRegardlessOfRuleOrderexercises the swapped order (FilterMergeRule first) and asserts the same result, pinning the user-visible contract rather than any rule-ordering invariant.CalciteExplainIT.testDedupAfterWherePushDown(new) — end-to-end against a live cluster: runssource=opensearch-sql_test_index_account | where age > 25 | dedup genderand assertsLogicalDedupis present in the explain output andEnumerableWindowis absent (i.e., dedup is actually pushed down and the in-memory window fallback the bug caused is gone).All existing
CalcitePPLDedupTestcases continue to pass.Related Issues
Resolves#5482
Check List
--signoffor-s.