[Analytics-Engine] Decorrelate RexSubQuery before marking in PlannerImpl#21795
Conversation
PPL queries that contain EXISTS / IN / SOME / ANY subqueries (and the
`subsearch` shapes the frontend lowers to them) reach the analytics
engine as a LogicalFilter / LogicalProject / LogicalJoin holding a
RexSubQuery. The downstream marking rules — particularly
OpenSearchFilterRule, which resolves leaf predicates through a
ScalarFunction table that doesn't (and shouldn't) cover EXISTS — reject
the operator at run time:
IllegalStateException: Unrecognized filter operator [EXISTS / EXISTS]
Add a 'subquery-remove' phase at the top of PlannerImpl.runAllOptimizations
that runs Calcite's three SUB_QUERY_TO_CORRELATE rules to lower every
RexSubQuery into a LogicalCorrelate, then decorrelates the result back to
a straight join shape via RelDecorrelator.decorrelateQuery. Every later
phase (reduce / pushdown / decompose / marking / CBO) sees a
subquery-free tree.
Placement: per the Component Responsibilities table in
opensearch-project/sql#5246, logical-plan rewrites are an SQL-plugin
concern long-term — but the unified path has no logical-optimizer stage
yet, and AE already owns RelNode → physical-execution. Putting the
rewrite here lets every AE client (PPL today, ANSI SQL / Spark / future
frontends) get correct subquery handling without each frontend having to
remember to run SubQueryRemoveRule + RelDecorrelator before handoff. When
a UnifiedQueryOptimizer lands SQL-side, this phase can migrate upward.
After: CalcitePPLExistsSubqueryIT goes from 19/19 fail (all
IllegalStateException at the filter rule) to 6/19 pass + 13/19
AssertionError result mismatches. The remaining 13 failures are
downstream analytics-engine semantics for bare-group-by Aggregate inside
the decorrelated join — unrelated to subquery removal, tracked
separately.
Tests:
* SubQueryPlanShapeTests — 3 new tests covering uncorrelated EXISTS,
correlated EXISTS, and IN-subquery; each asserts no RexSubQuery
survives the planner pipeline.
* RuleProfilingListenerTests — EXPECTED_PHASES extended with
'subquery-remove' so the profile-listener phase-list assertion still
matches the actual phase ordering.
* Full :sandbox:plugins:analytics-engine:test — 311 tests, 0 failures.
Signed-off-by: Jialiang Liang <jialianl@amazon.com>
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
PR Reviewer Guide 🔍(Review updated until commit a8b1151)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to a8b1151 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit fdf67ea
Suggestions up to commit 05c262a
Suggestions up to commit 9f49af5
Suggestions up to commit 2632a5d
Suggestions up to commit b37e0d2
|
Inline the RelDecorrelator.decorrelateQuery call and reorder the RelHomogeneousShuttle import — fixes the spotlessJavaCheck failure sandbox-check flagged on the prior commit. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com>
|
Persistent review updated to latest commit d1140a5 |
…ive assertion The subquery-remove phase introduced in PlannerImpl makes the where-in-with-streamstats-subquery shape reachable on the analytics-engine route — RexSubQuery lowers to LogicalCorrelate, then decorrelates into a semi-join that the marking + capability rules already support. The test was previously pinned as assertErrorAny(...) because the query crashed at the filter rule before this phase existed. Flip it to executePpl(...) + assertRowCount(1) — `| head 1` over a non-empty matching set returns one row — and update the class-level 'known limitations' comment so testWhereInWithStreamstatsSubquery is no longer listed alongside testLeftJoinWithStreamstats. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com>
|
Persistent review updated to latest commit b37e0d2 |
|
❌ Gradle check result for b37e0d2: null Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com>
Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com>
Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com>
|
Thanks for landing the subquery decorrelation work, @ryanbogan! This is a solid foundation — I've opened a draft PR #21804 that builds on top of this one with two follow-up marking-phase fixes (LITERAL_AGG lowering for |
CI surfaced four StreamstatsCommandIT failures and two InSubqueryCommandIT failures. All six are reconciled here. Streamstats — testStreamstatsResetWithNull / GlobalWithNull / ResetWithNullBucket / GlobalWithNullBucket previously asserted the query *must error* via assertErrorAny. After PR opensearch-project#21795's decorrelation plus this branch's marking-phase fixes (LITERAL_AGG lowering and OpenSearchJoinRule relaxation), these PPL forms now plan and execute end-to-end. Flip the four assertions to executePpl + assertNotNull, matching how testStreamstatsReset is already structured. InSubquery — testTwoExpressionsInSubquery's exact-row assertion was fragile across hash-join output ordering for two rows sharing salary=120000. Add 'id' as a secondary sort key so the order is fully determined. InSubquery — testTwoExpressionsNotInSubquery hits an independent OpenSearchAggregateSplitRule bug ("filter must be BOOLEAN NOT NULL") unrelated to subquery decorrelation. Mark @AwaitsFix with a note pointing at the split-rule limitation. Local verification: * StreamstatsCommandIT — all tests pass. * InSubqueryCommandIT — 12/13 pass, the @AwaitsFix-skipped one is the split-rule case. Signed-off-by: Songkan Tang <songkant@amazon.com>
The previous flip to executePpl + assertRowCount(1) was based on a single-node local run where the query succeeded. On the 4-node testcluster that sandbox-check uses, the decorrelated streamstats-inside-correlate shape errors with 'Stage 0 sink feed failed: partition stream receiver dropped before send' rather than producing rows. The subquery-remove phase moves the failure point from plan-time (IllegalStateException at OpenSearchFilterRule) to runtime (sink-feed failure during distributed execution) — same overall outcome from the test's perspective (HTTP 500), just a different message. Restore the original assertErrorAny(...) assertion: it catches any ResponseException so both the old and new error shapes satisfy it. Keep the test on the broad-failure pin until the downstream multi-node correctness gap is closed. Locally verified: integTest --tests testWhereInWithStreamstatsSubquery passes 1/1 against a 4-node testcluster on this commit. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com>
|
Persistent review updated to latest commit 2632a5d |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21795 +/- ##
============================================
+ Coverage 73.34% 73.36% +0.02%
- Complexity 75417 75429 +12
============================================
Files 6032 6032
Lines 342404 342404
Branches 49235 49235
============================================
+ Hits 251142 251218 +76
+ Misses 71272 71266 -6
+ Partials 19990 19920 -70 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Persistent review updated to latest commit 9f49af5 |
|
❕ Gradle check result for 9f49af5: UNSTABLE Please review all flaky tests that succeeded after retry and create an issue if one does not already exist to track the flaky failure. |
…istic on multi-node The subquery-remove phase makes this where-in-with-streamstats shape reachable on the analytics-engine route, but the streamstats-inside- decorrelated-correlate execution path races on multi-node clusters: sometimes the Stage 0 sender hits a 'partition stream receiver dropped before send' error, sometimes it returns one row successfully. Neither a positive assertion (executePpl + assertRowCount) nor the prior broad-failure assertion (assertErrorAny) is stable across CI runs. Pin with @AwaitsFix until the downstream multi-node race is closed — re-enable by removing the annotation. The annotation message names the specific failure signature so the next person picking this up knows which subsystem to investigate. Locally verified: integTest --tests testWhereInWithStreamstatsSubquery reports the test as skipped (tests=1 skipped=1 failures=0 errors=0). Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com>
|
Persistent review updated to latest commit 05c262a |
|
❌ Gradle check result for 05c262a: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit fdf67ea |
|
Persistent review updated to latest commit a8b1151 |
Builds on PR opensearch-project#21795 (Decorrelate RexSubQuery before marking). ITs ported from opensearch-project/sql: * InSubqueryCommandIT (13 tests) * ScalarSubqueryCommandIT (13 tests) * ExistsSubqueryCommandIT (11 tests) * worker / work_information / occupation datasets OpenSearchJoinRule.matches() relaxed to accept mixed equi+non-equi conditions, IS_NOT_DISTINCT_FROM (null-safe equi), and equi conjuncts where one operand is a RexCall expression (e.g. uid = salary + 1000) - DataFusion hash-join evaluates each operand to derive the key. RelDecorrelator's output for streamstats reset's directly-built LogicalCorrelate carries an AND(<=, =, =) condition; without this relaxation the rule rejected it. StreamstatsCommandIT.testStreamstatsReset flipped to positive (the other two reset_* variants are flipped in the follow-up commit that adds LITERAL_AGG lowering). Signed-off-by: Songkan Tang <songkant@amazon.com>
Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com>
CI surfaced four StreamstatsCommandIT failures and two InSubqueryCommandIT failures. All six are reconciled here. Streamstats — testStreamstatsResetWithNull / GlobalWithNull / ResetWithNullBucket / GlobalWithNullBucket previously asserted the query *must error* via assertErrorAny. After PR opensearch-project#21795's decorrelation plus this branch's marking-phase fixes (LITERAL_AGG lowering and OpenSearchJoinRule relaxation), these PPL forms now plan and execute end-to-end. Flip the four assertions to executePpl + assertNotNull, matching how testStreamstatsReset is already structured. InSubquery — testTwoExpressionsInSubquery's exact-row assertion was fragile across hash-join output ordering for two rows sharing salary=120000. Add 'id' as a secondary sort key so the order is fully determined. InSubquery — testTwoExpressionsNotInSubquery hits an independent OpenSearchAggregateSplitRule bug ("filter must be BOOLEAN NOT NULL") unrelated to subquery decorrelation. Mark @AwaitsFix with a note pointing at the split-rule limitation. Local verification: * StreamstatsCommandIT — all tests pass. * InSubqueryCommandIT — 12/13 pass, the @AwaitsFix-skipped one is the split-rule case. Signed-off-by: Songkan Tang <songkant@amazon.com>
Builds on PR opensearch-project#21795 (Decorrelate RexSubQuery before marking). ITs ported from opensearch-project/sql: * InSubqueryCommandIT (13 tests) * ScalarSubqueryCommandIT (13 tests) * ExistsSubqueryCommandIT (11 tests) * worker / work_information / occupation datasets OpenSearchJoinRule.matches() relaxed to accept mixed equi+non-equi conditions, IS_NOT_DISTINCT_FROM (null-safe equi), and equi conjuncts where one operand is a RexCall expression (e.g. uid = salary + 1000) - DataFusion hash-join evaluates each operand to derive the key. RelDecorrelator's output for streamstats reset's directly-built LogicalCorrelate carries an AND(<=, =, =) condition; without this relaxation the rule rejected it. StreamstatsCommandIT.testStreamstatsReset flipped to positive (the other two reset_* variants are flipped in the follow-up commit that adds LITERAL_AGG lowering). Signed-off-by: Songkan Tang <songkant@amazon.com>
Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com>
CI surfaced four StreamstatsCommandIT failures and two InSubqueryCommandIT failures. All six are reconciled here. Streamstats — testStreamstatsResetWithNull / GlobalWithNull / ResetWithNullBucket / GlobalWithNullBucket previously asserted the query *must error* via assertErrorAny. After PR opensearch-project#21795's decorrelation plus this branch's marking-phase fixes (LITERAL_AGG lowering and OpenSearchJoinRule relaxation), these PPL forms now plan and execute end-to-end. Flip the four assertions to executePpl + assertNotNull, matching how testStreamstatsReset is already structured. InSubquery — testTwoExpressionsInSubquery's exact-row assertion was fragile across hash-join output ordering for two rows sharing salary=120000. Add 'id' as a secondary sort key so the order is fully determined. InSubquery — testTwoExpressionsNotInSubquery hits an independent OpenSearchAggregateSplitRule bug ("filter must be BOOLEAN NOT NULL") unrelated to subquery decorrelation. Mark @AwaitsFix with a note pointing at the split-rule limitation. Local verification: * StreamstatsCommandIT — all tests pass. * InSubqueryCommandIT — 12/13 pass, the @AwaitsFix-skipped one is the split-rule case. Signed-off-by: Songkan Tang <songkant@amazon.com>
…apes (#21804) * Add subquery ITs and relax OpenSearchJoinRule for mixed conditions Builds on PR #21795 (Decorrelate RexSubQuery before marking). ITs ported from opensearch-project/sql: * InSubqueryCommandIT (13 tests) * ScalarSubqueryCommandIT (13 tests) * ExistsSubqueryCommandIT (11 tests) * worker / work_information / occupation datasets OpenSearchJoinRule.matches() relaxed to accept mixed equi+non-equi conditions, IS_NOT_DISTINCT_FROM (null-safe equi), and equi conjuncts where one operand is a RexCall expression (e.g. uid = salary + 1000) - DataFusion hash-join evaluates each operand to derive the key. RelDecorrelator's output for streamstats reset's directly-built LogicalCorrelate carries an AND(<=, =, =) condition; without this relaxation the rule rejected it. StreamstatsCommandIT.testStreamstatsReset flipped to positive (the other two reset_* variants are flipped in the follow-up commit that adds LITERAL_AGG lowering). Signed-off-by: Songkan Tang <songkant@amazon.com> * Lower LITERAL_AGG and accept all join condition shapes in marking Two complementary marking-phase fixes layered on top of PR #21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR #21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com> * Reconcile streamstats + InSubquery ITs with newly-supported plans CI surfaced four StreamstatsCommandIT failures and two InSubqueryCommandIT failures. All six are reconciled here. Streamstats — testStreamstatsResetWithNull / GlobalWithNull / ResetWithNullBucket / GlobalWithNullBucket previously asserted the query *must error* via assertErrorAny. After PR #21795's decorrelation plus this branch's marking-phase fixes (LITERAL_AGG lowering and OpenSearchJoinRule relaxation), these PPL forms now plan and execute end-to-end. Flip the four assertions to executePpl + assertNotNull, matching how testStreamstatsReset is already structured. InSubquery — testTwoExpressionsInSubquery's exact-row assertion was fragile across hash-join output ordering for two rows sharing salary=120000. Add 'id' as a secondary sort key so the order is fully determined. InSubquery — testTwoExpressionsNotInSubquery hits an independent OpenSearchAggregateSplitRule bug ("filter must be BOOLEAN NOT NULL") unrelated to subquery decorrelation. Mark @AwaitsFix with a note pointing at the split-rule limitation. Local verification: * StreamstatsCommandIT — all tests pass. * InSubqueryCommandIT — 12/13 pass, the @AwaitsFix-skipped one is the split-rule case. Signed-off-by: Songkan Tang <songkant@amazon.com> * Trim subquery ITs and simplify OpenSearchJoinRule.matches Two scope reductions following review feedback: * Remove ExistsSubqueryCommandIT, InSubqueryCommandIT, ScalarSubqueryCommandIT and their worker / work_information / occupation datasets. These are subquery-decorrelation coverage ports from the upstream sql repo and don't directly verify the marking-phase fixes layered here. They can be re-added in a dedicated subquery-IT PR if/when the analytics-engine route wants to track that suite. * Simplify OpenSearchJoinRule.matches() to a plain JoinRelType whitelist (INNER / LEFT / RIGHT / FULL / SEMI / ANTI). The condition-shape acceptance is implicit — DataFusion's join planner picks the physical strategy (HashJoin / NestedLoopJoin / CrossJoin) downstream, so there is no need for the rule to inspect the condition. JoinCommandIT.testAppendcol is no longer @AwaitsFix — appendcol's ROW_NUMBER + FULL OUTER LogicalJoin lowering now planes and executes end-to-end on calcs (12/12 JoinCommandIT tests green locally). Signed-off-by: Songkan Tang <songkant@amazon.com> * Address review comments * PlannerImpl.runAllOptimizations: drop the post-removeSubQueries LOGGER.info dump — the per-phase RuleProfilingListener output already covers what the log line gave. * PlannerImpl.extractLiteralAgg: import ExtractLiteralAggRule and use the simple class name at the call site (and in the @link reference in javadoc). * ExtractLiteralAggRule: import java.util.Objects and use Objects::nonNull instead of the fully-qualified java.util.Objects::nonNull. Signed-off-by: Songkan Tang <songkant@amazon.com> * HepPhase: distinguish RuleInstance from RuleCollection The previous HepPhase API exposed only addRules(...) and chose addRuleInstance vs addRuleCollection at run() time based on whether the accumulated list had one element or many. That collapsed two Hep instructions with different fixpoint semantics into one branch: * addRuleInstance(R) appends a HepInstruction.RuleInstance — R runs to fixpoint, then the next instruction starts. * addRuleCollection([R1, R2, ...]) appends a RuleCollection — every rule in the group is tried at each vertex within the same fixpoint loop, so a rewrite by one rule can cascade into a match by another in the same pass. The two are not interchangeable. PlannerImpl on main already relied on the distinction in pushdownRules: three transposes were grouped in a RuleCollection so they cascade-converge, then FILTER_MERGE was a separate RuleInstance so it only fired after the transposes settled (otherwise auto-injected NOT NULL would merge with a half- pushed intermediate filter rather than the user's WHERE on the post-pushdown shape). This commit: * Renames the HepPhase API to addRuleInstance / addRuleCollection, mirroring HepProgramBuilder one-to-one. addRuleCollection takes Collection<? extends RelOptRule>. * Stores instructions as List<Consumer<HepProgramBuilder>> and applies them in declared order at run() — no more size-based branching. * Restores PlannerImpl call sites to the original instruction structure: pushdownRules splits back into a transpose collection + a FILTER_MERGE instance, and the single-rule phases use addRuleInstance. Local verification: analytics-engine unit tests, JoinCommandIT, StreamstatsCommandIT all green. Signed-off-by: Songkan Tang <songkant@amazon.com> --------- Signed-off-by: Songkan Tang <songkant@amazon.com>
…mpl (opensearch-project#21795) * Decorrelate RexSubQuery before marking in PlannerImpl PPL queries that contain EXISTS / IN / SOME / ANY subqueries (and the `subsearch` shapes the frontend lowers to them) reach the analytics engine as a LogicalFilter / LogicalProject / LogicalJoin holding a RexSubQuery. The downstream marking rules — particularly OpenSearchFilterRule, which resolves leaf predicates through a ScalarFunction table that doesn't (and shouldn't) cover EXISTS — reject the operator at run time: IllegalStateException: Unrecognized filter operator [EXISTS / EXISTS] Add a 'subquery-remove' phase at the top of PlannerImpl.runAllOptimizations that runs Calcite's three SUB_QUERY_TO_CORRELATE rules to lower every RexSubQuery into a LogicalCorrelate, then decorrelates the result back to a straight join shape via RelDecorrelator.decorrelateQuery. Every later phase (reduce / pushdown / decompose / marking / CBO) sees a subquery-free tree. Placement: per the Component Responsibilities table in opensearch-project/sql#5246, logical-plan rewrites are an SQL-plugin concern long-term — but the unified path has no logical-optimizer stage yet, and AE already owns RelNode → physical-execution. Putting the rewrite here lets every AE client (PPL today, ANSI SQL / Spark / future frontends) get correct subquery handling without each frontend having to remember to run SubQueryRemoveRule + RelDecorrelator before handoff. When a UnifiedQueryOptimizer lands SQL-side, this phase can migrate upward. After: CalcitePPLExistsSubqueryIT goes from 19/19 fail (all IllegalStateException at the filter rule) to 6/19 pass + 13/19 AssertionError result mismatches. The remaining 13 failures are downstream analytics-engine semantics for bare-group-by Aggregate inside the decorrelated join — unrelated to subquery removal, tracked separately. Tests: * SubQueryPlanShapeTests — 3 new tests covering uncorrelated EXISTS, correlated EXISTS, and IN-subquery; each asserts no RexSubQuery survives the planner pipeline. * RuleProfilingListenerTests — EXPECTED_PHASES extended with 'subquery-remove' so the profile-listener phase-list assertion still matches the actual phase ordering. * Full :sandbox:plugins:analytics-engine:test — 311 tests, 0 failures. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> * spotlessApply Inline the RelDecorrelator.decorrelateQuery call and reorder the RelHomogeneousShuttle import — fixes the spotlessJavaCheck failure sandbox-check flagged on the prior commit. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Flip StreamstatsCommandIT.testWhereInWithStreamstatsSubquery to positive assertion The subquery-remove phase introduced in PlannerImpl makes the where-in-with-streamstats-subquery shape reachable on the analytics-engine route — RexSubQuery lowers to LogicalCorrelate, then decorrelates into a semi-join that the marking + capability rules already support. The test was previously pinned as assertErrorAny(...) because the query crashed at the filter rule before this phase existed. Flip it to executePpl(...) + assertRowCount(1) — `| head 1` over a non-empty matching set returns one row — and update the class-level 'known limitations' comment so testWhereInWithStreamstatsSubquery is no longer listed alongside testLeftJoinWithStreamstats. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Revert testWhereInWithStreamstatsSubquery flip — keep assertErrorAny The previous flip to executePpl + assertRowCount(1) was based on a single-node local run where the query succeeded. On the 4-node testcluster that sandbox-check uses, the decorrelated streamstats-inside-correlate shape errors with 'Stage 0 sink feed failed: partition stream receiver dropped before send' rather than producing rows. The subquery-remove phase moves the failure point from plan-time (IllegalStateException at OpenSearchFilterRule) to runtime (sink-feed failure during distributed execution) — same overall outcome from the test's perspective (HTTP 500), just a different message. Restore the original assertErrorAny(...) assertion: it catches any ResponseException so both the old and new error shapes satisfy it. Keep the test on the broad-failure pin until the downstream multi-node correctness gap is closed. Locally verified: integTest --tests testWhereInWithStreamstatsSubquery passes 1/1 against a 4-node testcluster on this commit. Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> * Skip testWhereInWithStreamstatsSubquery with @AwaitsFix — nondeterministic on multi-node The subquery-remove phase makes this where-in-with-streamstats shape reachable on the analytics-engine route, but the streamstats-inside- decorrelated-correlate execution path races on multi-node clusters: sometimes the Stage 0 sender hits a 'partition stream receiver dropped before send' error, sometimes it returns one row successfully. Neither a positive assertion (executePpl + assertRowCount) nor the prior broad-failure assertion (assertErrorAny) is stable across CI runs. Pin with @AwaitsFix until the downstream multi-node race is closed — re-enable by removing the annotation. The annotation message names the specific failure signature so the next person picking this up knows which subsystem to investigate. Locally verified: integTest --tests testWhereInWithStreamstatsSubquery reports the test as skipped (tests=1 skipped=1 failures=0 errors=0). Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com> --------- Signed-off-by: Jialiang Liang <jialianl@amazon.com> Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…apes (opensearch-project#21804) * Add subquery ITs and relax OpenSearchJoinRule for mixed conditions Builds on PR opensearch-project#21795 (Decorrelate RexSubQuery before marking). ITs ported from opensearch-project/sql: * InSubqueryCommandIT (13 tests) * ScalarSubqueryCommandIT (13 tests) * ExistsSubqueryCommandIT (11 tests) * worker / work_information / occupation datasets OpenSearchJoinRule.matches() relaxed to accept mixed equi+non-equi conditions, IS_NOT_DISTINCT_FROM (null-safe equi), and equi conjuncts where one operand is a RexCall expression (e.g. uid = salary + 1000) - DataFusion hash-join evaluates each operand to derive the key. RelDecorrelator's output for streamstats reset's directly-built LogicalCorrelate carries an AND(<=, =, =) condition; without this relaxation the rule rejected it. StreamstatsCommandIT.testStreamstatsReset flipped to positive (the other two reset_* variants are flipped in the follow-up commit that adds LITERAL_AGG lowering). Signed-off-by: Songkan Tang <songkant@amazon.com> * Lower LITERAL_AGG and accept all join condition shapes in marking Two complementary marking-phase fixes layered on top of PR opensearch-project#21795, both keeping Calcite as the source of truth: 1. PlannerImpl.removeSubQueries — after RelDecorrelator, run a Hep pass with ExtractLiteralAggRule (ported from Apache Impala) to lower LITERAL_AGG(literal) aggregate calls into Aggregate + Project. SubQueryRemoveRule.rewriteIn emits LITERAL_AGG for NOT IN's null-aware semantics; Calcite has no rule to remove it; each downstream engine handles it differently (Druid registers a native impl, Ignite wires an accumulator, Impala lowers to Aggregate + Project, Flink forks SubQueryRemoveRule). The Impala approach is a clean Calcite-rule-style rewrite — port it with attribution. 2. OpenSearchJoinRule.matches() — accept all join condition shapes, not just equi or mixed equi+non-equi. RelDecorrelator's output for correlated subqueries with non-equi correlation predicates (e.g. WHERE id > uid) is a pure non-equi LogicalJoin. DataFusion handles non-equi via NestedLoopJoin downstream, so there is no capability-level reason to reject these at the marking phase. End-to-end IT results on this branch (PR opensearch-project#21795 + these two fixes): ExistsSubqueryCommandIT : 11/11 InSubqueryCommandIT : 11/13 (1 row-order, 1 unrelated AggregateSplitRule bug) ScalarSubqueryCommandIT : 13/13 StreamstatsCommandIT : testStreamstatsReset passes Total : 36/38 = 95% The two remaining failures are unrelated to subquery decorrelation: * testTwoExpressionsInSubquery — result row ordering between hash join's bucketing and the IT's exact-row assertion. Test issue. * testTwoExpressionsNotInSubquery — OpenSearchAggregateSplitRule rejects nullable filter expressions ("filter must be BOOLEAN NOT NULL") on a multi-column NOT IN's COUNT FILTER. Independent AggregateSplitRule bug. Signed-off-by: Songkan Tang <songkant@amazon.com> * Reconcile streamstats + InSubquery ITs with newly-supported plans CI surfaced four StreamstatsCommandIT failures and two InSubqueryCommandIT failures. All six are reconciled here. Streamstats — testStreamstatsResetWithNull / GlobalWithNull / ResetWithNullBucket / GlobalWithNullBucket previously asserted the query *must error* via assertErrorAny. After PR opensearch-project#21795's decorrelation plus this branch's marking-phase fixes (LITERAL_AGG lowering and OpenSearchJoinRule relaxation), these PPL forms now plan and execute end-to-end. Flip the four assertions to executePpl + assertNotNull, matching how testStreamstatsReset is already structured. InSubquery — testTwoExpressionsInSubquery's exact-row assertion was fragile across hash-join output ordering for two rows sharing salary=120000. Add 'id' as a secondary sort key so the order is fully determined. InSubquery — testTwoExpressionsNotInSubquery hits an independent OpenSearchAggregateSplitRule bug ("filter must be BOOLEAN NOT NULL") unrelated to subquery decorrelation. Mark @AwaitsFix with a note pointing at the split-rule limitation. Local verification: * StreamstatsCommandIT — all tests pass. * InSubqueryCommandIT — 12/13 pass, the @AwaitsFix-skipped one is the split-rule case. Signed-off-by: Songkan Tang <songkant@amazon.com> * Trim subquery ITs and simplify OpenSearchJoinRule.matches Two scope reductions following review feedback: * Remove ExistsSubqueryCommandIT, InSubqueryCommandIT, ScalarSubqueryCommandIT and their worker / work_information / occupation datasets. These are subquery-decorrelation coverage ports from the upstream sql repo and don't directly verify the marking-phase fixes layered here. They can be re-added in a dedicated subquery-IT PR if/when the analytics-engine route wants to track that suite. * Simplify OpenSearchJoinRule.matches() to a plain JoinRelType whitelist (INNER / LEFT / RIGHT / FULL / SEMI / ANTI). The condition-shape acceptance is implicit — DataFusion's join planner picks the physical strategy (HashJoin / NestedLoopJoin / CrossJoin) downstream, so there is no need for the rule to inspect the condition. JoinCommandIT.testAppendcol is no longer @AwaitsFix — appendcol's ROW_NUMBER + FULL OUTER LogicalJoin lowering now planes and executes end-to-end on calcs (12/12 JoinCommandIT tests green locally). Signed-off-by: Songkan Tang <songkant@amazon.com> * Address review comments * PlannerImpl.runAllOptimizations: drop the post-removeSubQueries LOGGER.info dump — the per-phase RuleProfilingListener output already covers what the log line gave. * PlannerImpl.extractLiteralAgg: import ExtractLiteralAggRule and use the simple class name at the call site (and in the @link reference in javadoc). * ExtractLiteralAggRule: import java.util.Objects and use Objects::nonNull instead of the fully-qualified java.util.Objects::nonNull. Signed-off-by: Songkan Tang <songkant@amazon.com> * HepPhase: distinguish RuleInstance from RuleCollection The previous HepPhase API exposed only addRules(...) and chose addRuleInstance vs addRuleCollection at run() time based on whether the accumulated list had one element or many. That collapsed two Hep instructions with different fixpoint semantics into one branch: * addRuleInstance(R) appends a HepInstruction.RuleInstance — R runs to fixpoint, then the next instruction starts. * addRuleCollection([R1, R2, ...]) appends a RuleCollection — every rule in the group is tried at each vertex within the same fixpoint loop, so a rewrite by one rule can cascade into a match by another in the same pass. The two are not interchangeable. PlannerImpl on main already relied on the distinction in pushdownRules: three transposes were grouped in a RuleCollection so they cascade-converge, then FILTER_MERGE was a separate RuleInstance so it only fired after the transposes settled (otherwise auto-injected NOT NULL would merge with a half- pushed intermediate filter rather than the user's WHERE on the post-pushdown shape). This commit: * Renames the HepPhase API to addRuleInstance / addRuleCollection, mirroring HepProgramBuilder one-to-one. addRuleCollection takes Collection<? extends RelOptRule>. * Stores instructions as List<Consumer<HepProgramBuilder>> and applies them in declared order at run() — no more size-based branching. * Restores PlannerImpl call sites to the original instruction structure: pushdownRules splits back into a transpose collection + a FILTER_MERGE instance, and the single-rule phases use addRuleInstance. Local verification: analytics-engine unit tests, JoinCommandIT, StreamstatsCommandIT all green. Signed-off-by: Songkan Tang <songkant@amazon.com> --------- Signed-off-by: Songkan Tang <songkant@amazon.com>
Description
PPL queries that contain
EXISTS/NOT EXISTS/IN/SOME/ANYsubqueries — and thesubsearchshapes that lower to them — currently crash on the analytics-engine route with:The whole class
CalcitePPLExistsSubqueryIT(19 tests) fails this way against an analytics-engine-routed cluster.Root cause
The full failure path:
Where:
OpenSearchFilterRule.java:155-160— resolves every leaf predicate throughScalarFunction.fromSqlOperatorWithFallback, which doesn't (and shouldn't) coverRexSubQueryoperators.RexSubQueryextendsRexCall, so the upstreaminstanceof RexCallcheck waves it through.RelRunner.prepareStatement(rel), which internally runsSubQueryRemoveRule+ decorrelation inside Calcite's Volcano-driven physical conversion. The analytics-engine path goesAnalyticsExecutionEngine → QueryPlanExecutor.execute → PlannerImpl.runAllOptimizations, bypassing that pipeline.PlannerImplalready runs four HEP phases (reduce-expressions→pushdown-rules→aggregate-decompose→marking) plus a Volcano CBO pass. Subquery removal is a logical-plan rewrite — it belongs in this pipeline, before marking lowers the surroundingLogical*nodes intoOpenSearch*shapes that the capability rules expect.Fix
Add a new
subquery-removephase at the top ofPlannerImpl.runAllOptimizations:Wired in as:
The three
*_SUB_QUERY_TO_CORRELATErules rewriteRexSubQuerys intoLogicalCorrelate.RelDecorrelator.decorrelateQuerythen converts the correlates into straight joins. Every later phase observes a subquery-free tree.Placement discussion
Per the Component Responsibilities table in opensearch-project/sql#5246, logical-plan rewrites are an SQL-plugin concern in the long-term design. The unified-query path on the SQL side, however, doesn't have a logical-optimizer stage yet —
CalciteToolsHelper.HEP_PROGRAM(FilterMerge + PPLSimplifyDedup) runs only on the legacy engine path, andUnifiedQueryPlanner.plan()returns the RelNode afterpostAnalysisRules()shuttles. AE already owns RelNode → physical-execution.Placing the rewrite here:
UnifiedQueryOptimizerlands SQL-side per Revert changes in AbstractPointGeometryFieldMapper #5246, this phase can migrate upward — same code, different host.After-fix verification
Same
CalcitePPLExistsSubqueryITclass, same cluster, same routing (-Dtests.analytics.parquet_indices=true), with a vanilla-main SQL plugin (no SQL-side rewrite — isolates the AE change):IllegalStateException: Unrecognized filter operator [EXISTS / EXISTS]AssertionError(result-row mismatches) — zero remainingIllegalStateExceptionsThe "Unrecognized filter operator [EXISTS / EXISTS]" signature is gone from every test — the RCA is fully addressed.
Per-test breakdown after the fix:
testEmptyExistsSubquery,testExistsSubqueryWithConjunction,testNotExistsSubquery,testNotExistsSubqueryInFilter,testSubsearchMaxOut2,testSubsearchMaxOut4testSimpleExistsSubquery,testSimpleExistsSubqueryInFilter,testUncorrelatedExistsSubquery,testUncorrelatedExistsSubqueryCheckTheReturnContentOfInnerTableIsEmptyOrNot,testNestedExistsSubquery,testExistsSubqueryAndAggregation,testIssue3566,testSubsearchMaxOut1,testSubsearchMaxOut3,testSubsearchMaxOutNegativeMeansUnlimited,testSubsearchMaxOutUncorrelated,testCorrelatedSubsearchMaxOutZeroMeansUnlimited,testUncorrelatedSubsearchMaxOutZeroMeansUnlimitedThe decorrelated plan for the failing tests looks correct on inspection — e.g.
testSimpleExistsSubqueryproduces:— textbook EXISTS semantics (INNER join with the inner side group-by-deduplicated). The 2× row multiplication that's observed suggests the bare-group-by
LogicalAggregate(group=[{0}])isn't preserving its dedup semantic through the analytics-engine's join + aggregate execution path. That's a separate downstream issue from the subquery-removal RCA and will be tracked under its own change.Out of scope
Anything beyond the EXISTS / IN / SOME / ANY subquery-removal RCA:
Aggregatededup semantics inside a decorrelated join — needs analytics-engine planner / executor changes. Tracked separately.RelDecorrelatorwill still raise for correlated subqueries Calcite can't decorrelate (e.g. correlated nondeterministic functions). Those queries used to crash with "Unrecognized filter operator"; with this PR they'll crash with a Calcite decorrelation message — net same outcome (failure → failure), different error string.Tests
SubQueryPlanShapeTests— 3 tests covering uncorrelated EXISTS, correlated EXISTS, and IN-subquery. Each parses real SQL throughSqlPlannerTestFixture, runs the full planner, and asserts noRexSubQuerysurvives.RuleProfilingListenerTests—EXPECTED_PHASESextended withsubquery-removeso the profile-listener phase-list assertion still matches the actual phase ordering. All 5 tests in that class still green.:sandbox:plugins:analytics-engine:test— 311 tests, 0 failures, 0 errors.CalcitePPLExistsSubqueryITagainst an analytics-engine-routed cluster — 6/19 pass, 13/19 fail with downstream result-mismatch (see verification table above).Companion / supersedes
This PR is the AE-side alternative to opensearch-project/sql#5460, which applied the same rewrite in the SQL plugin's
AnalyticsExecutionEngine. After alignment in the #5460 thread, the rewrite is being moved here per the Component Responsibilities placement in opensearch-project/sql#5246 (subquery removal is logical optimization → SQL-plugin column long-term, but AE today is the actual consumer with the limitation, and every AE client benefits from a single fix here). #5460 will close as superseded once this lands.