Skip to content

[Analytics-Engine] Decorrelate RexSubQuery before marking in PlannerImpl#21795

Merged
mch2 merged 9 commits into
opensearch-project:mainfrom
RyanL1997:ae-decorrelate-subqueries
May 27, 2026
Merged

[Analytics-Engine] Decorrelate RexSubQuery before marking in PlannerImpl#21795
mch2 merged 9 commits into
opensearch-project:mainfrom
RyanL1997:ae-decorrelate-subqueries

Conversation

@RyanL1997

Copy link
Copy Markdown
Contributor

Description

PPL queries that contain EXISTS / NOT EXISTS / IN / SOME / ANY subqueries — and the subsearch shapes that lower to them — currently crash on the analytics-engine route with:

HTTP/1.1 500 Internal Server Error
{
  "error": {
    "reason": "There was internal problem at backend",
    "details": "Unrecognized filter operator [EXISTS / EXISTS]",
    "type": "IllegalStateException"
  }
}

The whole class CalcitePPLExistsSubqueryIT (19 tests) fails this way against an analytics-engine-routed cluster.

Root cause

The full failure path:

PPL `... | where [exists ...]`
  → SQL plugin's CalciteRelNodeVisitor.analyze(...)
  → LogicalFilter(condition=[EXISTS({...subquery...})])           ← RexSubQuery of SqlKind.EXISTS
  → AnalyticsExecutionEngine → analytics-engine PlannerImpl
  → marking phase → OpenSearchFilterRule.resolveViableBackends(predicate)
  → ScalarFunction.fromSqlOperatorWithFallback(EXISTS) returns null
  → throw IllegalStateException("Unrecognized filter operator [EXISTS / EXISTS]")

Where:

  • Error site: OpenSearchFilterRule.java:155-160 — resolves every leaf predicate through ScalarFunction.fromSqlOperatorWithFallback, which doesn't (and shouldn't) cover RexSubQuery operators. RexSubQuery extends RexCall, so the upstream instanceof RexCall check waves it through.
  • Why the SQL legacy engine doesn't see this: the legacy path goes through RelRunner.prepareStatement(rel), which internally runs SubQueryRemoveRule + decorrelation inside Calcite's Volcano-driven physical conversion. The analytics-engine path goes AnalyticsExecutionEngine → QueryPlanExecutor.execute → PlannerImpl.runAllOptimizations, bypassing that pipeline.
  • Where the rewrite belongs: the analytics-engine's PlannerImpl already runs four HEP phases (reduce-expressionspushdown-rulesaggregate-decomposemarking) plus a Volcano CBO pass. Subquery removal is a logical-plan rewrite — it belongs in this pipeline, before marking lowers the surrounding Logical* nodes into OpenSearch* shapes that the capability rules expect.

Fix

Add a new subquery-remove phase at the top of PlannerImpl.runAllOptimizations:

private static RelNode removeSubQueries(RelNode input, RuleProfilingListener listener) {
    HepProgramBuilder builder = new HepProgramBuilder();
    builder.addRuleCollection(
        List.of(
            CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
            CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
            CoreRules.JOIN_SUB_QUERY_TO_CORRELATE
        )
    );
    HepPlanner planner = new HepPlanner(builder.build());
    // ... profiling-listener wiring identical to other phases ...
    planner.setRoot(input);
    RelNode withCorrelates = planner.findBestExp();
    return RelDecorrelator.decorrelateQuery(
        withCorrelates,
        RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null)
    );
}

Wired in as:

RelNode modifiedRelNode = rawRelNode;
modifiedRelNode = removeSubQueries(modifiedRelNode, listener);    // NEW: Phase 0
modifiedRelNode = reduceExpressions(modifiedRelNode, listener);
modifiedRelNode = pushdownRules(modifiedRelNode, listener);
modifiedRelNode = decomposeAggregates(modifiedRelNode, listener);
modifiedRelNode = mark(modifiedRelNode, context, listener);
modifiedRelNode = cbo(modifiedRelNode, rawRelNode, context, listener);

The three *_SUB_QUERY_TO_CORRELATE rules rewrite RexSubQuerys into LogicalCorrelate. RelDecorrelator.decorrelateQuery then 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, and UnifiedQueryPlanner.plan() returns the RelNode after postAnalysisRules() shuttles. AE already owns RelNode → physical-execution.

Placing the rewrite here:

  1. Every AE client (PPL today, ANSI SQL / Spark / future frontends) gets correct subquery handling without each frontend having to remember to run SubQueryRemoveRule + RelDecorrelator before handoff.
  2. The fix is local to the consumer that actually had the limitation (analytics-engine), not spread across N callers.
  3. When a UnifiedQueryOptimizer lands SQL-side per Revert changes in AbstractPointGeometryFieldMapper #5246, this phase can migrate upward — same code, different host.

After-fix verification

Same CalcitePPLExistsSubqueryIT class, same cluster, same routing (-Dtests.analytics.parquet_indices=true), with a vanilla-main SQL plugin (no SQL-side rewrite — isolates the AE change):

Run Pass Fail Failure signature
Before 0 / 19 19 / 19 All IllegalStateException: Unrecognized filter operator [EXISTS / EXISTS]
After (this PR) 6 / 19 13 / 19 All AssertionError (result-row mismatches) — zero remaining IllegalStateExceptions

The "Unrecognized filter operator [EXISTS / EXISTS]" signature is gone from every test — the RCA is fully addressed.

Per-test breakdown after the fix:

  • PASS (6): testEmptyExistsSubquery, testExistsSubqueryWithConjunction, testNotExistsSubquery, testNotExistsSubqueryInFilter, testSubsearchMaxOut2, testSubsearchMaxOut4
  • FAIL — result mismatch only (13): testSimpleExistsSubquery, testSimpleExistsSubqueryInFilter, testUncorrelatedExistsSubquery, testUncorrelatedExistsSubqueryCheckTheReturnContentOfInnerTableIsEmptyOrNot, testNestedExistsSubquery, testExistsSubqueryAndAggregation, testIssue3566, testSubsearchMaxOut1, testSubsearchMaxOut3, testSubsearchMaxOutNegativeMeansUnlimited, testSubsearchMaxOutUncorrelated, testCorrelatedSubsearchMaxOutZeroMeansUnlimited, testUncorrelatedSubsearchMaxOutZeroMeansUnlimited

The decorrelated plan for the failing tests looks correct on inspection — e.g. testSimpleExistsSubquery produces:

LogicalJoin(condition=[=(worker.id, $f0)], joinType=[inner])
  LogicalTableScan(worker)
  LogicalProject(uid=[$0], $f1=[true])
    LogicalAggregate(group=[{0}])         ← dedups inner side by uid
      LogicalProject(uid=[$3])
        LogicalFilter(condition=[IS NOT NULL($3)])
          LogicalTableScan(work_information)

— 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:

  • Bare-group-by Aggregate dedup semantics inside a decorrelated join — needs analytics-engine planner / executor changes. Tracked separately.
  • RelDecorrelator will 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

  • New unit-test class SubQueryPlanShapeTests — 3 tests covering uncorrelated EXISTS, correlated EXISTS, and IN-subquery. Each parses real SQL through SqlPlannerTestFixture, runs the full planner, and asserts no RexSubQuery survives.
  • RuleProfilingListenerTestsEXPECTED_PHASES extended with subquery-remove so the profile-listener phase-list assertion still matches the actual phase ordering. All 5 tests in that class still green.
  • :sandbox:plugins:analytics-engine:test311 tests, 0 failures, 0 errors.
  • End-to-end via CalcitePPLExistsSubqueryIT against 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.

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>
@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit a8b1151)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Possible Issue

RelDecorrelator.decorrelateQuery is called with a RelBuilder created from Contexts.empty(). If decorrelation requires access to schema metadata, type factories, or other context-dependent services that would normally be present in a properly configured context, this may cause failures. The empty context might lack necessary configuration that RelDecorrelator expects when rewriting complex correlated subqueries.

return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to a8b1151

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Handle decorrelation exceptions properly

The decorrelation step could fail with an exception, but the finally block will
still execute endPhase. If decorrelation throws, the phase timing will be recorded
as complete even though the operation failed. Consider wrapping the decorrelation in
proper error handling or moving endPhase before the return to ensure accurate
profiling.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [142-150]

 try {
     planner.setRoot(input);
     RelNode withCorrelates = planner.findBestExp();
     // RexSubQuery removal introduces LogicalCorrelate; decorrelate back to a
     // straight join shape that the marking + capability rules already handle.
-    return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
-} finally {
+    RelNode result = RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
     if (listener != null) listener.endPhase("subquery-remove");
+    return result;
+} catch (Exception e) {
+    if (listener != null) listener.endPhase("subquery-remove");
+    throw e;
 }
Suggestion importance[1-10]: 4

__

Why: While the concern about exception handling is valid, the suggestion changes the profiling semantics by calling endPhase twice in different paths. The current finally block ensures endPhase is always called, which is a common pattern. The improvement is marginal and the suggested approach is debatable.

Low

Previous suggestions

Suggestions up to commit fdf67ea
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate planner result before decorrelation

If planner.findBestExp() returns null or the decorrelation fails, the method will
throw an unchecked exception. Add validation to ensure withCorrelates is non-null
before decorrelation and handle potential decorrelation failures gracefully.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [143-147]

 planner.setRoot(input);
 RelNode withCorrelates = planner.findBestExp();
+if (withCorrelates == null) {
+    throw new IllegalStateException("Subquery removal phase failed to produce a valid plan");
+}
 // RexSubQuery removal introduces LogicalCorrelate; decorrelate back to a
 // straight join shape that the marking + capability rules already handle.
 return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
Suggestion importance[1-10]: 5

__

Why: Adding a null check for withCorrelates improves robustness by catching potential planner failures early. However, HepPlanner.findBestExp() typically doesn't return null in normal operation, making this a defensive check rather than addressing a critical issue.

Low
Suggestions up to commit 05c262a
CategorySuggestion                                                                                                                                    Impact
General
Add error handling for decorrelation

If decorrelateQuery throws an exception, the listener's phase will end but the
exception from decorrelation won't be properly logged or contextualized. Consider
wrapping the decorrelation call in a try-catch to provide better error context
before re-throwing.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [137-150]

 HepPlanner planner = new HepPlanner(builder.build());
 if (listener != null) {
     planner.addListener(listener);
     listener.beginPhase("subquery-remove");
 }
 try {
     planner.setRoot(input);
     RelNode withCorrelates = planner.findBestExp();
-    ...
-    return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
+    try {
+        return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
+    } catch (Exception e) {
+        LOGGER.error("Failed to decorrelate query after subquery removal", e);
+        throw e;
+    }
 } finally {
     if (listener != null) listener.endPhase("subquery-remove");
 }
Suggestion importance[1-10]: 4

__

Why: While adding error logging could marginally improve debugging, the finally block already ensures proper cleanup. The suggestion adds minimal value since exceptions will propagate naturally, and the additional try-catch only adds logging without changing behavior. The impact is minor for maintainability.

Low
Suggestions up to commit 9f49af5
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate correlate conversion succeeded

The decorrelation step may fail or produce incorrect results if withCorrelates still
contains unresolved correlations or if the planner didn't successfully convert all
subqueries. Add validation to check that withCorrelates is in the expected state
before decorrelation to prevent silent failures.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [143-147]

 planner.setRoot(input);
 RelNode withCorrelates = planner.findBestExp();
+if (withCorrelates == null) {
+    throw new IllegalStateException("Subquery-to-correlate conversion failed");
+}
 // RexSubQuery removal introduces LogicalCorrelate; decorrelate back to a
 // straight join shape that the marking + capability rules already handle.
 return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
Suggestion importance[1-10]: 7

__

Why: Adding a null check for withCorrelates is a reasonable defensive programming practice that could prevent silent failures if the planner fails to convert subqueries. This improves error handling and makes debugging easier, though findBestExp() likely doesn't return null in normal operation.

Medium
Pass proper context to RelBuilder

The RelBuilder is created with a null schema parameter, which may cause issues when
the decorrelator needs to resolve table references or perform schema-dependent
operations. Consider passing the appropriate schema context from the input node's
cluster to ensure proper decorrelation.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [147]

-return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
+RelBuilder relBuilder = RelBuilder.proto(input.getCluster().getPlanner().getContext()).create(input.getCluster(), null);
+return RelDecorrelator.decorrelateQuery(withCorrelates, relBuilder);
Suggestion importance[1-10]: 5

__

Why: The suggestion raises a valid concern about passing Contexts.empty() to RelBuilder.proto(), which might cause issues during decorrelation. However, the improved code assumes input.getCluster().getPlanner().getContext() exists and is the correct context, which may not be accurate without deeper knowledge of the codebase. The suggestion is reasonable but speculative.

Low
Suggestions up to commit 2632a5d
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add null check for optimization result

If planner.findBestExp() returns null or fails to find a valid plan, the subsequent
decorrelateQuery call will fail with a NullPointerException. Add a null check or
validation to handle cases where optimization doesn't produce a valid result.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [143-147]

 planner.setRoot(input);
 RelNode withCorrelates = planner.findBestExp();
+if (withCorrelates == null) {
+    throw new IllegalStateException("Subquery removal phase failed to produce a valid plan");
+}
 // RexSubQuery removal introduces LogicalCorrelate; decorrelate back to a
 // straight join shape that the marking + capability rules already handle.
 return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
Suggestion importance[1-10]: 6

__

Why: Valid defensive programming suggestion. Adding a null check for withCorrelates before calling decorrelateQuery would prevent potential NullPointerException and provide clearer error messaging if the optimization phase fails unexpectedly.

Low
Suggestions up to commit b37e0d2
CategorySuggestion                                                                                                                                    Impact
General
Validate subquery removal before decorrelation

The decorrelation step could fail or produce unexpected results if withCorrelates
contains unresolved correlations. Add validation to check if the plan after
findBestExp() still contains RexSubQuery nodes before attempting decorrelation, and
log a warning or throw an exception if subqueries remain.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [143-147]

 planner.setRoot(input);
 RelNode withCorrelates = planner.findBestExp();
+// Validate that all RexSubQuery nodes were converted to LogicalCorrelate
+if (containsRexSubQuery(withCorrelates)) {
+    LOGGER.warn("RexSubQuery nodes remain after correlation phase");
+}
 // RexSubQuery removal introduces LogicalCorrelate; decorrelate back to a
 // straight join shape that the marking + capability rules already handle.
 return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
Suggestion importance[1-10]: 5

__

Why: Adding validation to check for remaining RexSubQuery nodes before decorrelation is a reasonable defensive programming practice. However, the suggestion references a containsRexSubQuery() method that doesn't exist in the codebase, and the test file SubQueryPlanShapeTests.java already provides comprehensive validation that subqueries are removed, making this additional check somewhat redundant.

Low
Pass proper context to RelBuilder

The RelBuilder is created with a null schema parameter, which may cause issues when
decorrelating queries that reference specific schemas. Consider passing the
appropriate schema from the input node's cluster to ensure proper schema resolution
during decorrelation.

sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/planner/PlannerImpl.java [147]

-return RelDecorrelator.decorrelateQuery(withCorrelates, RelBuilder.proto(Contexts.empty()).create(input.getCluster(), null));
+RelBuilder relBuilder = RelBuilder.proto(input.getCluster().getPlanner().getContext()).create(input.getCluster(), null);
+return RelDecorrelator.decorrelateQuery(withCorrelates, relBuilder);
Suggestion importance[1-10]: 3

__

Why: The suggestion proposes using input.getCluster().getPlanner().getContext() instead of Contexts.empty(), but this assumes the planner context is more appropriate without evidence from the PR that the current implementation causes issues. The change is speculative and may not provide meaningful improvement.

Low

@RyanL1997 RyanL1997 marked this pull request as ready for review May 21, 2026 22:33
@RyanL1997 RyanL1997 requested a review from a team as a code owner May 21, 2026 22:33
@RyanL1997 RyanL1997 changed the title [analytics-engine] Decorrelate RexSubQuery before marking in PlannerImpl [Analytics-Engine] Decorrelate RexSubQuery before marking in PlannerImpl May 21, 2026
@RyanL1997 RyanL1997 added the analytics-engine Issues and PR for the new Analytics engine label May 21, 2026
RyanL1997 added 2 commits May 21, 2026 15:55
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>
@github-actions

Copy link
Copy Markdown
Contributor

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>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b37e0d2

@github-actions

Copy link
Copy Markdown
Contributor

❌ 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?

songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 22, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 22, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 22, 2026
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>
@songkant-aws

Copy link
Copy Markdown
Contributor

Thanks for landing the subquery decorrelation work, @ryanbogan! This is a solid foundation — RelDecorrelator running before marking unblocks a lot of correlated-subquery shapes that we couldn't plan before.

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 NOT IN shapes, and relaxing OpenSearchJoinRule to accept all join condition shapes so RelDecorrelator's pure non-equi join output reaches DataFusion). #21804 will stay draft until #21795 merges, then rebase onto main so only the layered diff shows. Looking forward to your review on this one.

songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 22, 2026
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>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 2632a5d

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for 2632a5d: SUCCESS

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.36%. Comparing base (0c60e50) to head (a8b1151).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 9f49af5

@github-actions

Copy link
Copy Markdown
Contributor

❕ 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>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 05c262a

@github-actions

Copy link
Copy Markdown
Contributor

❌ 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?

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit fdf67ea

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for fdf67ea: SUCCESS

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit a8b1151

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for a8b1151: SUCCESS

@mch2 mch2 merged commit 34a4217 into opensearch-project:main May 27, 2026
16 checks passed
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 27, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 27, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 27, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 30, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 30, 2026
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>
songkant-aws added a commit to songkant-aws/OpenSearch that referenced this pull request May 30, 2026
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>
mch2 pushed a commit that referenced this pull request May 30, 2026
…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>
KhishorekumarBS pushed a commit to KhishorekumarBS/OpenSearch that referenced this pull request Jul 3, 2026
…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>
KhishorekumarBS pushed a commit to KhishorekumarBS/OpenSearch that referenced this pull request Jul 3, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

analytics-engine Issues and PR for the new Analytics engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants