[QA] Add AppendPipeCommandIT and TableCommandIT for the analytics-engine REST path - #21526
Conversation
PPL `appendpipe` already passes 4/4 of its v2-side `CalcitePPLAppendPipeCommandIT` cases on the analytics-engine route under `tests.analytics.force_routing=true` — the existing capability surface (LogicalUnion + LogicalAggregate over SUM, plus the SchemaUnifier type-conflict path) is sufficient. No code changes in core; this PR just lands a self-contained QA IT so the analytics-engine path can be verified inside core without cross-plugin dependencies. Three tests, mirroring the v2-side surface that exercises the three distinct shapes of `appendpipe`: * `testAppendPipeSort` — duplicate the post-stats stream and re-sort the duplicate inline. Exercises Union over identical schemas. 5 / 6 rows kept by `head 5` — multiset overlap is fine because the outer `sort str0` pins the original branch's order, and the duplicate's `sort -sum_int0_by_str0` pins the inner branch's order. * `testAppendPipeWithMergedColumn` — duplicate the post-stats stream and collapse it via an inner `stats sum(sum) as sum`. Exercises SchemaUnifier merging the str0-bearing original branch with the inner-branch single-row that has only `sum`. The two branches arrive at the coordinator's Union in non-deterministic order, so multiset comparison. * `testAppendPipeWithConflictTypeColumn` — inner pipeline rewrites the same-named column to a different type. Exercises the SchemaUnifier validation path that surfaces "due to incompatible types" before execution. Reuses the existing `calcs` parquet-backed dataset via `DatasetProvisioner` (no new fixtures). `testAppendDifferentIndex` from the v2-side IT is intentionally not ported — it exercises `append` (separate `source=...` sub-search), already covered by `AppendCommandIT`. ## Test plan * `./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*AppendPipeCommandIT"` — 3/3 green * `./gradlew :sandbox:qa:analytics-engine-rest:check -Dsandbox.enabled=true` — green * Verified end-to-end: 56 force_routing transitions, 63 analytics-engine PlannerImpl entries, 0 v2 PPLService.Incoming entries during a sweep including this IT. ## Out of scope * PPL `multisearch` was originally bundled with this PR. Triage showed ~12 of its 15 analytics-route failures are blocked on a Substrait-side issue: DataFusion's substrait consumer rejects the Plan emitted for `LogicalUnion(StageInputScan, StageInputScan)` with `Names list must match exactly to nested schema, but found N uses for M names`. Root cause not diagnosed yet (the registered child-stage schema width vs. `Plan.Root.names` width disagree somewhere between `LocalStageScheduler.buildChildInputs` and DataFusion's `make_renamed_schema`). Out of scope here; tracked for a follow-up PR after deeper investigation. * Aggregation, TIMESTAMP/DATE type-system, eval-predicate scalars, and PPL `span` follow the same scope discipline as opensearch-project#21521 — each is its own work track and not addressed here. Signed-off-by: Kai Huang <ahkcs@amazon.com>
PR Reviewer Guide 🔍(Review updated until commit 94245b8)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 94245b8
Previous suggestionsSuggestions up to commit 55f4cae
|
`table` is a syntactic alias of `fields` — the v2 AstBuilder dispatches both through `buildProjectCommand` once `plugins.calcite.enabled=true` is visible to the AstBuilder via the UnifiedQueryContext. The mirror fix landed in opensearch-project/sql#5413; this commit closes the gap on the test-ppl-frontend side, where the UnifiedQueryContext is constructed locally rather than coming from the SQL plugin. Two changes: - `UnifiedQueryService` now sets `plugins.calcite.enabled=true` on the context. The unified path is Calcite-based by definition; without this flag the AstBuilder rejects table/regex/rex/convert. - New `TableCommandIT` covering the surfaces specific to the `table` keyword: comma-delimited, space-delimited, suffix wildcard, leading-`-` exclusion, and `fields` ↔ `table` equivalence on identical inputs. Plain projection semantics already covered by FieldsCommandIT are not duplicated. Validates: 5/5 TableCommandIT pass, 3/3 AppendPipeCommandIT still pass. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 94245b8 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21526 +/- ##
============================================
- Coverage 73.44% 73.41% -0.04%
+ Complexity 74426 74396 -30
============================================
Files 5970 5970
Lines 338267 338267
Branches 48753 48753
============================================
- Hits 248451 248345 -106
- Misses 70042 70108 +66
- Partials 19774 19814 +40 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Lands a self-contained QA IT covering PPL `multisearch` so the analytics-route fix in this PR is exercised inside core without cross-plugin dependencies on the SQL plugin. Three tests, scoped to the surface analytics already supports end-to-end: | Test | Shape | |---|---| | `testMultisearchTwoBranchesByCategory` | Basic 2-way Union over int0 buckets — `Union(Filter+Eval+Project, Filter+Eval+Project)` followed by `Aggregate(count by) | sort`. Exercises the same convertReduceFragment chain (`attachFragmentOnTop(Sort, attachFragmentOnTop(Aggregate, convertFinalAggFragment(Union)))`) that the rewire fix targets. | | `testMultisearchThreeBranchesByStr0` | 3-way Union — the exact `Union(ER, ER, ER)` shape that surfaced the residual "2 uses for 6 names" failure I'd flagged as a follow-up in an earlier draft of the PR description; the rewire fix already covers it on a fresh cluster. | | `testMultisearchSingleSubsearchRejected` | Arity check — pinned at the parser layer (AstBuilder.visitMultisearchCommand rejects <2 subsearches with `SyntaxCheckException`). Regression-pin against accidental relaxation of that guard. | Each branch projects to a scalar-only field set (`fields int0, class` / `fields str0, bucket`) so the union row type sidesteps the calcs dataset's date/time/datetime columns — `ArrowSchemaFromCalcite.toArrowType` doesn't yet handle TIMESTAMP, tracked separately. Bumps `test-ppl-frontend`'s `unified-query-*` dependency from 3.6.0.0-SNAPSHOT to 3.7.0.0-SNAPSHOT so the bundled PPL grammar exposes the `multisearch` keyword (along with table/regex/rex/convert added since 3.6). The SQL Snapshots repo (already declared in the build) carries the published 3.7 artifacts; for local sql-repo HEAD development, run `./gradlew :ppl:publishUnifiedQueryPublicationToMavenLocal` from the sql repo. The version bump is independent of the in-flight test-ppl-frontend UnifiedQueryService.setting() change in opensearch-project#21526 — different files, no conflict. Validates: 3/3 MultisearchCommandIT pass; full `:sandbox:qa:analytics-engine-rest:integTest` suite still green (110 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com>
Lands a self-contained QA IT covering PPL `multisearch` so the analytics-route fix in this PR is exercised inside core without cross-plugin dependencies on the SQL plugin. Three tests, scoped to the surface analytics already supports end-to-end: | Test | Shape | |---|---| | `testMultisearchTwoBranchesByCategory` | Basic 2-way Union over int0 buckets — `Union(Filter+Eval+Project, Filter+Eval+Project)` followed by `Aggregate(count by) | sort`. Exercises the same convertReduceFragment chain (`attachFragmentOnTop(Sort, attachFragmentOnTop(Aggregate, convertFinalAggFragment(Union)))`) that the rewire fix targets. | | `testMultisearchThreeBranchesByStr0` | 3-way Union — the exact `Union(ER, ER, ER)` shape that surfaced the residual "2 uses for 6 names" failure I'd flagged as a follow-up in an earlier draft of the PR description; the rewire fix already covers it on a fresh cluster. | | `testMultisearchSingleSubsearchRejected` | Arity check — pinned at the parser layer (AstBuilder.visitMultisearchCommand rejects <2 subsearches with `SyntaxCheckException`). Regression-pin against accidental relaxation of that guard. | Each branch projects to a scalar-only field set (`fields int0, class` / `fields str0, bucket`) so the union row type sidesteps the calcs dataset's date/time/datetime columns — `ArrowSchemaFromCalcite.toArrowType` doesn't yet handle TIMESTAMP, tracked separately. Bumps `test-ppl-frontend`'s `unified-query-*` dependency from 3.6.0.0-SNAPSHOT to 3.7.0.0-SNAPSHOT so the bundled PPL grammar exposes the `multisearch` keyword (along with table/regex/rex/convert added since 3.6). The SQL Snapshots repo (already declared in the build) carries the published 3.7 artifacts; for local sql-repo HEAD development, run `./gradlew :ppl:publishUnifiedQueryPublicationToMavenLocal` from the sql repo. The version bump is independent of the in-flight test-ppl-frontend UnifiedQueryService.setting() change in opensearch-project#21526 — different files, no conflict. Validates: 3/3 MultisearchCommandIT pass; full `:sandbox:qa:analytics-engine-rest:integTest` suite still green (110 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ntyped-NULL fixes for multisearch (#21528) * [Analytics Backend / DataFusion] Fix Plan.Root.names mismatch for schema-reshaping wrappers `DataFusionFragmentConvertor.rewire` always populated the new `Plan.Root.names` list with the *inner* plan's names. For schema-preserving wrappers (Sort, Filter, Fetch) those happen to coincide with the wrapper's output schema, so the bug was hidden. For schema-reshaping wrappers (Aggregate, Project) the wrapper's output width differs from the inner's, and DataFusion's substrait consumer rejects the plan in `make_renamed_schema` with: Substrait error: Names list must match exactly to nested schema, but found {wrapper-width} uses for {inner-width} names This shape is hit by every PPL `multisearch` query whose coordinator stage is `Sort(Aggregate(Union(StageInputScan, StageInputScan)))` — the Aggregate narrows the wide Union row type, and the inner-names override surfaced the mismatch as a 500. Fix: derive the new `Plan.Root.names` from the wrapper RelNode's row type (`fragment.getRowType().getFieldList()`), not the inner plan. Both `attachFragmentOnTop` and `attachPartialAggOnTop` already have the wrapper RelNode in scope, so this is a local change with no signature ripple beyond adding a `List<String> wrapperNames` parameter to `rewire`. Test coverage: - `testAttachPartialAggOnTop_PlanRootNamesMatchWrapperOutput` — the partial-agg path with a 3-column inner scan and a 1-column wrapper aggregate; pins names to the wrapper's output. - `testAttachFragmentOnTop_AggregateOverMultiColumnInner_PlanRootNamesMatchWrapperOutput` — the multisearch coordinator-stage shape (`Aggregate(Union)`). - `testMultisearchShape_SortOverAggregateOverThreeWayUnion_PlanRootNamesMatchTopOutput` — full chain `Sort → Aggregate → Union(Sin × 3)` modeling the `testMultisearchWithThreeSubsearches` query plan. - `testMultisearchShape_SystemLimitOverSortOverAggregateOverUnion_NamesMatchTopOutput` — adds the implicit `LogicalSystemLimit` wrapper that `QueryService.convertToCalcitePlan` injects at the top of every analytics-engine plan, lowered to a Substrait `Fetch`. End-to-end validation against `:integ-test:integTestRemote --tests 'org.opensearch.sql.calcite.remote.CalciteMultisearchCommandIT'` with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`: 21 tests, **5 pass** (was 0/21 before this change). The remaining 16 are blocked on orthogonal issues that are out of scope here: - `Field [...] not found.` analyzer errors (8 tests) — pre-existing parquet-backed-index field-resolution gap. - TIMESTAMP / SPAN scalar functions unsupported (4 tests). - AssertionError on error-message format (2 tests). - One residual `Names list must match exactly to nested schema, but found 2 uses for 6 names` on `testMultisearchWithThreeSubsearches` — likely a different code path (PARTIAL/FINAL split via `OpenSearchAggregateSplitRule`) that the convertor unit tests don't exercise; tracked for follow-up. - Two timeouts/long-running on the largest queries. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add MultisearchCommandIT for the analytics-engine REST path Lands a self-contained QA IT covering PPL `multisearch` so the analytics-route fix in this PR is exercised inside core without cross-plugin dependencies on the SQL plugin. Three tests, scoped to the surface analytics already supports end-to-end: | Test | Shape | |---|---| | `testMultisearchTwoBranchesByCategory` | Basic 2-way Union over int0 buckets — `Union(Filter+Eval+Project, Filter+Eval+Project)` followed by `Aggregate(count by) | sort`. Exercises the same convertReduceFragment chain (`attachFragmentOnTop(Sort, attachFragmentOnTop(Aggregate, convertFinalAggFragment(Union)))`) that the rewire fix targets. | | `testMultisearchThreeBranchesByStr0` | 3-way Union — the exact `Union(ER, ER, ER)` shape that surfaced the residual "2 uses for 6 names" failure I'd flagged as a follow-up in an earlier draft of the PR description; the rewire fix already covers it on a fresh cluster. | | `testMultisearchSingleSubsearchRejected` | Arity check — pinned at the parser layer (AstBuilder.visitMultisearchCommand rejects <2 subsearches with `SyntaxCheckException`). Regression-pin against accidental relaxation of that guard. | Each branch projects to a scalar-only field set (`fields int0, class` / `fields str0, bucket`) so the union row type sidesteps the calcs dataset's date/time/datetime columns — `ArrowSchemaFromCalcite.toArrowType` doesn't yet handle TIMESTAMP, tracked separately. Bumps `test-ppl-frontend`'s `unified-query-*` dependency from 3.6.0.0-SNAPSHOT to 3.7.0.0-SNAPSHOT so the bundled PPL grammar exposes the `multisearch` keyword (along with table/regex/rex/convert added since 3.6). The SQL Snapshots repo (already declared in the build) carries the published 3.7 artifacts; for local sql-repo HEAD development, run `./gradlew :ppl:publishUnifiedQueryPublicationToMavenLocal` from the sql repo. The version bump is independent of the in-flight test-ppl-frontend UnifiedQueryService.setting() change in #21526 — different files, no conflict. Validates: 3/3 MultisearchCommandIT pass; full `:sandbox:qa:analytics-engine-rest:integTest` suite still green (110 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Spotless reformat of testFinalAggInnerStageScanRowType Single-line spotless reformat in DataFusionFragmentConvertorTests: the OpenSearchStageInputScan constructor call now fits on one line (it was previously broken across multiple lines). Originally bumped sandbox/plugins/analytics-backend-datafusion's sqlUnifiedQueryVersion 3.6 -> 3.7 to align with test-ppl-frontend, but the entire internalClusterTest classpath block (including that pin) was removed upstream by #21555 (dbe4a42, "Enable Lucene Filter delegation from Datafusion for Correctness"). The build.gradle hunk dropped during rebase; only the spotless reformat survives. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Register CASE in project capabilities + QA IT Calcite emits SqlKind.CASE for any conditional expression — explicit `eval x = case(cond, val, …)` in PPL, plus the `count(eval(predicate))` conditional-count idiom (lowered to COUNT(CASE WHEN predicate THEN … END)) and several other shapes. Without CASE in `STANDARD_PROJECT_OPS`, the analytics planner rejected the operator with `No backend supports scalar function [CASE] among [datafusion]` before substrait emission. CASE doesn't need a backend adapter: isthmus translates SqlKind.CASE structurally to a Substrait IfThen rel, and DataFusion's substrait consumer handles IfThen natively. Just registering the capability is enough. Adds `testMultisearchEvalCaseProjection` to MultisearchCommandIT to pin the end-to-end path — multisearch + `eval bucket = case(cond, val else default)` + stats. Uses an explicit `else` arm so isthmus doesn't have to convert an untyped NULL literal; the implicit-else `count(eval(…))` shape that the v2-side testMultisearchSuccessRatePattern uses still hits a separate isthmus limitation (`Unable to convert the type NULL` from `TypeConverter` on a SqlTypeName.NULL literal — tracked separately, out of scope here). Validates: 4/4 MultisearchCommandIT pass; full :sandbox:qa:analytics-engine-rest:integTest suite still green (111 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Pre-isthmus untyped-NULL rewriter for CASE arms Calcite emits a `RexLiteral` with `SqlTypeName.NULL` for the implicit ELSE arm of `CASE WHEN cond THEN val END` — exactly the shape PPL `count(eval(predicate))` lowers to (`COUNT(CASE WHEN predicate THEN <projected> END)`). Isthmus' `TypeConverter.toSubstrait` rejects `SqlTypeName.NULL` with `Unable to convert the type NULL`, blocking the analytics path before substrait emission. Adds `UntypedNullPreprocessor` — a `RelHomogeneousShuttle` + `RexShuttle` pass applied in `convertToSubstrait` and `convertStandalone` *before* the SubstraitRelVisitor sees the plan. Walks every CASE call's value operands (THEN arms and the ELSE arm) and substitutes any `SqlTypeName.NULL` literal with a typed null literal matching the CASE's resolved return type. Calcite already widens the CASE's return type to the leastRestrictive of branches, so the substituted type is correct by construction. Scope is intentionally narrow: only CASE call operands are rewritten today. Other untyped-NULL contexts (function arguments, comparison RHS) are rare in PPL-generated plans and would need per-operator type inference to do safely; defer until a concrete test surfaces one. Test coverage: - `UntypedNullPreprocessorTests` (4 new): * `testCountEvalCaseRewritesElseNullToTypedNull` — the motivating shape `COUNT(CASE WHEN cond THEN 1 ELSE null END)`. * `testCaseWithThenNullIsAlsoRewritten` — null in the THEN arm. * `testCaseConditionOperandUnchanged` — even-index condition operands left alone. * `testCountOverRewrittenCaseProjectionTypechecks` — Aggregate(Project(CASE)) with the rewriter applied still type-checks end-to-end. - New `testMultisearchCountEvalConditionalCount` in MultisearchCommandIT — mirrors the v2-side `CalciteMultisearchCommandIT.testMultisearchSuccessRatePattern` shape (`count(eval(predicate))`) end-to-end on the analytics-engine REST path. Validates: 5/5 MultisearchCommandIT pass; 4/4 new + 12/12 existing FragmentConvertor unit tests; full :sandbox:qa:analytics-engine-rest:integTest suite still green (112 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Simplify testCaseConditionOperandUnchanged for spotless The original assertion wrapped a no-op single-iteration loop around an empty- body RexShuttle whose accept() result is just caseExpr.toString(). Spotless flagged the empty class body (`new RexShuttle() {}`) as a formatting violation and tried to wrap it across two lines, which read worse than the underlying intent — comparing the input CASE expression to the rewriter's output to prove no-op behavior when no untyped nulls are present. Replace the loop+shuttle with a direct `assertEquals(caseExpr.toString(), rewrittenCase.toString())` — same semantics, cleaner code, no awkward formatting. The test still asserts the rewriter doesn't touch CASE expressions whose operands are already typed. Sandbox check (`./gradlew check -p sandbox -Dsandbox.enabled=true`) now passes end-to-end. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ine REST path (opensearch-project#21526) * [QA] Add AppendPipeCommandIT for the analytics-engine REST path PPL `appendpipe` already passes 4/4 of its v2-side `CalcitePPLAppendPipeCommandIT` cases on the analytics-engine route under `tests.analytics.force_routing=true` — the existing capability surface (LogicalUnion + LogicalAggregate over SUM, plus the SchemaUnifier type-conflict path) is sufficient. No code changes in core; this PR just lands a self-contained QA IT so the analytics-engine path can be verified inside core without cross-plugin dependencies. Three tests, mirroring the v2-side surface that exercises the three distinct shapes of `appendpipe`: * `testAppendPipeSort` — duplicate the post-stats stream and re-sort the duplicate inline. Exercises Union over identical schemas. 5 / 6 rows kept by `head 5` — multiset overlap is fine because the outer `sort str0` pins the original branch's order, and the duplicate's `sort -sum_int0_by_str0` pins the inner branch's order. * `testAppendPipeWithMergedColumn` — duplicate the post-stats stream and collapse it via an inner `stats sum(sum) as sum`. Exercises SchemaUnifier merging the str0-bearing original branch with the inner-branch single-row that has only `sum`. The two branches arrive at the coordinator's Union in non-deterministic order, so multiset comparison. * `testAppendPipeWithConflictTypeColumn` — inner pipeline rewrites the same-named column to a different type. Exercises the SchemaUnifier validation path that surfaces "due to incompatible types" before execution. Reuses the existing `calcs` parquet-backed dataset via `DatasetProvisioner` (no new fixtures). `testAppendDifferentIndex` from the v2-side IT is intentionally not ported — it exercises `append` (separate `source=...` sub-search), already covered by `AppendCommandIT`. ## Test plan * `./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*AppendPipeCommandIT"` — 3/3 green * `./gradlew :sandbox:qa:analytics-engine-rest:check -Dsandbox.enabled=true` — green * Verified end-to-end: 56 force_routing transitions, 63 analytics-engine PlannerImpl entries, 0 v2 PPLService.Incoming entries during a sweep including this IT. ## Out of scope * PPL `multisearch` was originally bundled with this PR. Triage showed ~12 of its 15 analytics-route failures are blocked on a Substrait-side issue: DataFusion's substrait consumer rejects the Plan emitted for `LogicalUnion(StageInputScan, StageInputScan)` with `Names list must match exactly to nested schema, but found N uses for M names`. Root cause not diagnosed yet (the registered child-stage schema width vs. `Plan.Root.names` width disagree somewhere between `LocalStageScheduler.buildChildInputs` and DataFusion's `make_renamed_schema`). Out of scope here; tracked for a follow-up PR after deeper investigation. * Aggregation, TIMESTAMP/DATE type-system, eval-predicate scalars, and PPL `span` follow the same scope discipline as opensearch-project#21521 — each is its own work track and not addressed here. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add TableCommandIT for the analytics-engine REST path `table` is a syntactic alias of `fields` — the v2 AstBuilder dispatches both through `buildProjectCommand` once `plugins.calcite.enabled=true` is visible to the AstBuilder via the UnifiedQueryContext. The mirror fix landed in opensearch-project/sql#5413; this commit closes the gap on the test-ppl-frontend side, where the UnifiedQueryContext is constructed locally rather than coming from the SQL plugin. Two changes: - `UnifiedQueryService` now sets `plugins.calcite.enabled=true` on the context. The unified path is Calcite-based by definition; without this flag the AstBuilder rejects table/regex/rex/convert. - New `TableCommandIT` covering the surfaces specific to the `table` keyword: comma-delimited, space-delimited, suffix wildcard, leading-`-` exclusion, and `fields` ↔ `table` equivalence on identical inputs. Plain projection semantics already covered by FieldsCommandIT are not duplicated. Validates: 5/5 TableCommandIT pass, 3/3 AppendPipeCommandIT still pass. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ntyped-NULL fixes for multisearch (opensearch-project#21528) * [Analytics Backend / DataFusion] Fix Plan.Root.names mismatch for schema-reshaping wrappers `DataFusionFragmentConvertor.rewire` always populated the new `Plan.Root.names` list with the *inner* plan's names. For schema-preserving wrappers (Sort, Filter, Fetch) those happen to coincide with the wrapper's output schema, so the bug was hidden. For schema-reshaping wrappers (Aggregate, Project) the wrapper's output width differs from the inner's, and DataFusion's substrait consumer rejects the plan in `make_renamed_schema` with: Substrait error: Names list must match exactly to nested schema, but found {wrapper-width} uses for {inner-width} names This shape is hit by every PPL `multisearch` query whose coordinator stage is `Sort(Aggregate(Union(StageInputScan, StageInputScan)))` — the Aggregate narrows the wide Union row type, and the inner-names override surfaced the mismatch as a 500. Fix: derive the new `Plan.Root.names` from the wrapper RelNode's row type (`fragment.getRowType().getFieldList()`), not the inner plan. Both `attachFragmentOnTop` and `attachPartialAggOnTop` already have the wrapper RelNode in scope, so this is a local change with no signature ripple beyond adding a `List<String> wrapperNames` parameter to `rewire`. Test coverage: - `testAttachPartialAggOnTop_PlanRootNamesMatchWrapperOutput` — the partial-agg path with a 3-column inner scan and a 1-column wrapper aggregate; pins names to the wrapper's output. - `testAttachFragmentOnTop_AggregateOverMultiColumnInner_PlanRootNamesMatchWrapperOutput` — the multisearch coordinator-stage shape (`Aggregate(Union)`). - `testMultisearchShape_SortOverAggregateOverThreeWayUnion_PlanRootNamesMatchTopOutput` — full chain `Sort → Aggregate → Union(Sin × 3)` modeling the `testMultisearchWithThreeSubsearches` query plan. - `testMultisearchShape_SystemLimitOverSortOverAggregateOverUnion_NamesMatchTopOutput` — adds the implicit `LogicalSystemLimit` wrapper that `QueryService.convertToCalcitePlan` injects at the top of every analytics-engine plan, lowered to a Substrait `Fetch`. End-to-end validation against `:integ-test:integTestRemote --tests 'org.opensearch.sql.calcite.remote.CalciteMultisearchCommandIT'` with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`: 21 tests, **5 pass** (was 0/21 before this change). The remaining 16 are blocked on orthogonal issues that are out of scope here: - `Field [...] not found.` analyzer errors (8 tests) — pre-existing parquet-backed-index field-resolution gap. - TIMESTAMP / SPAN scalar functions unsupported (4 tests). - AssertionError on error-message format (2 tests). - One residual `Names list must match exactly to nested schema, but found 2 uses for 6 names` on `testMultisearchWithThreeSubsearches` — likely a different code path (PARTIAL/FINAL split via `OpenSearchAggregateSplitRule`) that the convertor unit tests don't exercise; tracked for follow-up. - Two timeouts/long-running on the largest queries. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add MultisearchCommandIT for the analytics-engine REST path Lands a self-contained QA IT covering PPL `multisearch` so the analytics-route fix in this PR is exercised inside core without cross-plugin dependencies on the SQL plugin. Three tests, scoped to the surface analytics already supports end-to-end: | Test | Shape | |---|---| | `testMultisearchTwoBranchesByCategory` | Basic 2-way Union over int0 buckets — `Union(Filter+Eval+Project, Filter+Eval+Project)` followed by `Aggregate(count by) | sort`. Exercises the same convertReduceFragment chain (`attachFragmentOnTop(Sort, attachFragmentOnTop(Aggregate, convertFinalAggFragment(Union)))`) that the rewire fix targets. | | `testMultisearchThreeBranchesByStr0` | 3-way Union — the exact `Union(ER, ER, ER)` shape that surfaced the residual "2 uses for 6 names" failure I'd flagged as a follow-up in an earlier draft of the PR description; the rewire fix already covers it on a fresh cluster. | | `testMultisearchSingleSubsearchRejected` | Arity check — pinned at the parser layer (AstBuilder.visitMultisearchCommand rejects <2 subsearches with `SyntaxCheckException`). Regression-pin against accidental relaxation of that guard. | Each branch projects to a scalar-only field set (`fields int0, class` / `fields str0, bucket`) so the union row type sidesteps the calcs dataset's date/time/datetime columns — `ArrowSchemaFromCalcite.toArrowType` doesn't yet handle TIMESTAMP, tracked separately. Bumps `test-ppl-frontend`'s `unified-query-*` dependency from 3.6.0.0-SNAPSHOT to 3.7.0.0-SNAPSHOT so the bundled PPL grammar exposes the `multisearch` keyword (along with table/regex/rex/convert added since 3.6). The SQL Snapshots repo (already declared in the build) carries the published 3.7 artifacts; for local sql-repo HEAD development, run `./gradlew :ppl:publishUnifiedQueryPublicationToMavenLocal` from the sql repo. The version bump is independent of the in-flight test-ppl-frontend UnifiedQueryService.setting() change in opensearch-project#21526 — different files, no conflict. Validates: 3/3 MultisearchCommandIT pass; full `:sandbox:qa:analytics-engine-rest:integTest` suite still green (110 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Spotless reformat of testFinalAggInnerStageScanRowType Single-line spotless reformat in DataFusionFragmentConvertorTests: the OpenSearchStageInputScan constructor call now fits on one line (it was previously broken across multiple lines). Originally bumped sandbox/plugins/analytics-backend-datafusion's sqlUnifiedQueryVersion 3.6 -> 3.7 to align with test-ppl-frontend, but the entire internalClusterTest classpath block (including that pin) was removed upstream by opensearch-project#21555 (dbe4a42, "Enable Lucene Filter delegation from Datafusion for Correctness"). The build.gradle hunk dropped during rebase; only the spotless reformat survives. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Register CASE in project capabilities + QA IT Calcite emits SqlKind.CASE for any conditional expression — explicit `eval x = case(cond, val, …)` in PPL, plus the `count(eval(predicate))` conditional-count idiom (lowered to COUNT(CASE WHEN predicate THEN … END)) and several other shapes. Without CASE in `STANDARD_PROJECT_OPS`, the analytics planner rejected the operator with `No backend supports scalar function [CASE] among [datafusion]` before substrait emission. CASE doesn't need a backend adapter: isthmus translates SqlKind.CASE structurally to a Substrait IfThen rel, and DataFusion's substrait consumer handles IfThen natively. Just registering the capability is enough. Adds `testMultisearchEvalCaseProjection` to MultisearchCommandIT to pin the end-to-end path — multisearch + `eval bucket = case(cond, val else default)` + stats. Uses an explicit `else` arm so isthmus doesn't have to convert an untyped NULL literal; the implicit-else `count(eval(…))` shape that the v2-side testMultisearchSuccessRatePattern uses still hits a separate isthmus limitation (`Unable to convert the type NULL` from `TypeConverter` on a SqlTypeName.NULL literal — tracked separately, out of scope here). Validates: 4/4 MultisearchCommandIT pass; full :sandbox:qa:analytics-engine-rest:integTest suite still green (111 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Pre-isthmus untyped-NULL rewriter for CASE arms Calcite emits a `RexLiteral` with `SqlTypeName.NULL` for the implicit ELSE arm of `CASE WHEN cond THEN val END` — exactly the shape PPL `count(eval(predicate))` lowers to (`COUNT(CASE WHEN predicate THEN <projected> END)`). Isthmus' `TypeConverter.toSubstrait` rejects `SqlTypeName.NULL` with `Unable to convert the type NULL`, blocking the analytics path before substrait emission. Adds `UntypedNullPreprocessor` — a `RelHomogeneousShuttle` + `RexShuttle` pass applied in `convertToSubstrait` and `convertStandalone` *before* the SubstraitRelVisitor sees the plan. Walks every CASE call's value operands (THEN arms and the ELSE arm) and substitutes any `SqlTypeName.NULL` literal with a typed null literal matching the CASE's resolved return type. Calcite already widens the CASE's return type to the leastRestrictive of branches, so the substituted type is correct by construction. Scope is intentionally narrow: only CASE call operands are rewritten today. Other untyped-NULL contexts (function arguments, comparison RHS) are rare in PPL-generated plans and would need per-operator type inference to do safely; defer until a concrete test surfaces one. Test coverage: - `UntypedNullPreprocessorTests` (4 new): * `testCountEvalCaseRewritesElseNullToTypedNull` — the motivating shape `COUNT(CASE WHEN cond THEN 1 ELSE null END)`. * `testCaseWithThenNullIsAlsoRewritten` — null in the THEN arm. * `testCaseConditionOperandUnchanged` — even-index condition operands left alone. * `testCountOverRewrittenCaseProjectionTypechecks` — Aggregate(Project(CASE)) with the rewriter applied still type-checks end-to-end. - New `testMultisearchCountEvalConditionalCount` in MultisearchCommandIT — mirrors the v2-side `CalciteMultisearchCommandIT.testMultisearchSuccessRatePattern` shape (`count(eval(predicate))`) end-to-end on the analytics-engine REST path. Validates: 5/5 MultisearchCommandIT pass; 4/4 new + 12/12 existing FragmentConvertor unit tests; full :sandbox:qa:analytics-engine-rest:integTest suite still green (112 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Simplify testCaseConditionOperandUnchanged for spotless The original assertion wrapped a no-op single-iteration loop around an empty- body RexShuttle whose accept() result is just caseExpr.toString(). Spotless flagged the empty class body (`new RexShuttle() {}`) as a formatting violation and tried to wrap it across two lines, which read worse than the underlying intent — comparing the input CASE expression to the rewriter's output to prove no-op behavior when no untyped nulls are present. Replace the loop+shuttle with a direct `assertEquals(caseExpr.toString(), rewrittenCase.toString())` — same semantics, cleaner code, no awkward formatting. The test still asserts the rewriter doesn't touch CASE expressions whose operands are already typed. Sandbox check (`./gradlew check -p sandbox -Dsandbox.enabled=true`) now passes end-to-end. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ine REST path (opensearch-project#21526) * [QA] Add AppendPipeCommandIT for the analytics-engine REST path PPL `appendpipe` already passes 4/4 of its v2-side `CalcitePPLAppendPipeCommandIT` cases on the analytics-engine route under `tests.analytics.force_routing=true` — the existing capability surface (LogicalUnion + LogicalAggregate over SUM, plus the SchemaUnifier type-conflict path) is sufficient. No code changes in core; this PR just lands a self-contained QA IT so the analytics-engine path can be verified inside core without cross-plugin dependencies. Three tests, mirroring the v2-side surface that exercises the three distinct shapes of `appendpipe`: * `testAppendPipeSort` — duplicate the post-stats stream and re-sort the duplicate inline. Exercises Union over identical schemas. 5 / 6 rows kept by `head 5` — multiset overlap is fine because the outer `sort str0` pins the original branch's order, and the duplicate's `sort -sum_int0_by_str0` pins the inner branch's order. * `testAppendPipeWithMergedColumn` — duplicate the post-stats stream and collapse it via an inner `stats sum(sum) as sum`. Exercises SchemaUnifier merging the str0-bearing original branch with the inner-branch single-row that has only `sum`. The two branches arrive at the coordinator's Union in non-deterministic order, so multiset comparison. * `testAppendPipeWithConflictTypeColumn` — inner pipeline rewrites the same-named column to a different type. Exercises the SchemaUnifier validation path that surfaces "due to incompatible types" before execution. Reuses the existing `calcs` parquet-backed dataset via `DatasetProvisioner` (no new fixtures). `testAppendDifferentIndex` from the v2-side IT is intentionally not ported — it exercises `append` (separate `source=...` sub-search), already covered by `AppendCommandIT`. ## Test plan * `./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*AppendPipeCommandIT"` — 3/3 green * `./gradlew :sandbox:qa:analytics-engine-rest:check -Dsandbox.enabled=true` — green * Verified end-to-end: 56 force_routing transitions, 63 analytics-engine PlannerImpl entries, 0 v2 PPLService.Incoming entries during a sweep including this IT. ## Out of scope * PPL `multisearch` was originally bundled with this PR. Triage showed ~12 of its 15 analytics-route failures are blocked on a Substrait-side issue: DataFusion's substrait consumer rejects the Plan emitted for `LogicalUnion(StageInputScan, StageInputScan)` with `Names list must match exactly to nested schema, but found N uses for M names`. Root cause not diagnosed yet (the registered child-stage schema width vs. `Plan.Root.names` width disagree somewhere between `LocalStageScheduler.buildChildInputs` and DataFusion's `make_renamed_schema`). Out of scope here; tracked for a follow-up PR after deeper investigation. * Aggregation, TIMESTAMP/DATE type-system, eval-predicate scalars, and PPL `span` follow the same scope discipline as opensearch-project#21521 — each is its own work track and not addressed here. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add TableCommandIT for the analytics-engine REST path `table` is a syntactic alias of `fields` — the v2 AstBuilder dispatches both through `buildProjectCommand` once `plugins.calcite.enabled=true` is visible to the AstBuilder via the UnifiedQueryContext. The mirror fix landed in opensearch-project/sql#5413; this commit closes the gap on the test-ppl-frontend side, where the UnifiedQueryContext is constructed locally rather than coming from the SQL plugin. Two changes: - `UnifiedQueryService` now sets `plugins.calcite.enabled=true` on the context. The unified path is Calcite-based by definition; without this flag the AstBuilder rejects table/regex/rex/convert. - New `TableCommandIT` covering the surfaces specific to the `table` keyword: comma-delimited, space-delimited, suffix wildcard, leading-`-` exclusion, and `fields` ↔ `table` equivalence on identical inputs. Plain projection semantics already covered by FieldsCommandIT are not duplicated. Validates: 5/5 TableCommandIT pass, 3/3 AppendPipeCommandIT still pass. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
…ntyped-NULL fixes for multisearch (opensearch-project#21528) * [Analytics Backend / DataFusion] Fix Plan.Root.names mismatch for schema-reshaping wrappers `DataFusionFragmentConvertor.rewire` always populated the new `Plan.Root.names` list with the *inner* plan's names. For schema-preserving wrappers (Sort, Filter, Fetch) those happen to coincide with the wrapper's output schema, so the bug was hidden. For schema-reshaping wrappers (Aggregate, Project) the wrapper's output width differs from the inner's, and DataFusion's substrait consumer rejects the plan in `make_renamed_schema` with: Substrait error: Names list must match exactly to nested schema, but found {wrapper-width} uses for {inner-width} names This shape is hit by every PPL `multisearch` query whose coordinator stage is `Sort(Aggregate(Union(StageInputScan, StageInputScan)))` — the Aggregate narrows the wide Union row type, and the inner-names override surfaced the mismatch as a 500. Fix: derive the new `Plan.Root.names` from the wrapper RelNode's row type (`fragment.getRowType().getFieldList()`), not the inner plan. Both `attachFragmentOnTop` and `attachPartialAggOnTop` already have the wrapper RelNode in scope, so this is a local change with no signature ripple beyond adding a `List<String> wrapperNames` parameter to `rewire`. Test coverage: - `testAttachPartialAggOnTop_PlanRootNamesMatchWrapperOutput` — the partial-agg path with a 3-column inner scan and a 1-column wrapper aggregate; pins names to the wrapper's output. - `testAttachFragmentOnTop_AggregateOverMultiColumnInner_PlanRootNamesMatchWrapperOutput` — the multisearch coordinator-stage shape (`Aggregate(Union)`). - `testMultisearchShape_SortOverAggregateOverThreeWayUnion_PlanRootNamesMatchTopOutput` — full chain `Sort → Aggregate → Union(Sin × 3)` modeling the `testMultisearchWithThreeSubsearches` query plan. - `testMultisearchShape_SystemLimitOverSortOverAggregateOverUnion_NamesMatchTopOutput` — adds the implicit `LogicalSystemLimit` wrapper that `QueryService.convertToCalcitePlan` injects at the top of every analytics-engine plan, lowered to a Substrait `Fetch`. End-to-end validation against `:integ-test:integTestRemote --tests 'org.opensearch.sql.calcite.remote.CalciteMultisearchCommandIT'` with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`: 21 tests, **5 pass** (was 0/21 before this change). The remaining 16 are blocked on orthogonal issues that are out of scope here: - `Field [...] not found.` analyzer errors (8 tests) — pre-existing parquet-backed-index field-resolution gap. - TIMESTAMP / SPAN scalar functions unsupported (4 tests). - AssertionError on error-message format (2 tests). - One residual `Names list must match exactly to nested schema, but found 2 uses for 6 names` on `testMultisearchWithThreeSubsearches` — likely a different code path (PARTIAL/FINAL split via `OpenSearchAggregateSplitRule`) that the convertor unit tests don't exercise; tracked for follow-up. - Two timeouts/long-running on the largest queries. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [QA] Add MultisearchCommandIT for the analytics-engine REST path Lands a self-contained QA IT covering PPL `multisearch` so the analytics-route fix in this PR is exercised inside core without cross-plugin dependencies on the SQL plugin. Three tests, scoped to the surface analytics already supports end-to-end: | Test | Shape | |---|---| | `testMultisearchTwoBranchesByCategory` | Basic 2-way Union over int0 buckets — `Union(Filter+Eval+Project, Filter+Eval+Project)` followed by `Aggregate(count by) | sort`. Exercises the same convertReduceFragment chain (`attachFragmentOnTop(Sort, attachFragmentOnTop(Aggregate, convertFinalAggFragment(Union)))`) that the rewire fix targets. | | `testMultisearchThreeBranchesByStr0` | 3-way Union — the exact `Union(ER, ER, ER)` shape that surfaced the residual "2 uses for 6 names" failure I'd flagged as a follow-up in an earlier draft of the PR description; the rewire fix already covers it on a fresh cluster. | | `testMultisearchSingleSubsearchRejected` | Arity check — pinned at the parser layer (AstBuilder.visitMultisearchCommand rejects <2 subsearches with `SyntaxCheckException`). Regression-pin against accidental relaxation of that guard. | Each branch projects to a scalar-only field set (`fields int0, class` / `fields str0, bucket`) so the union row type sidesteps the calcs dataset's date/time/datetime columns — `ArrowSchemaFromCalcite.toArrowType` doesn't yet handle TIMESTAMP, tracked separately. Bumps `test-ppl-frontend`'s `unified-query-*` dependency from 3.6.0.0-SNAPSHOT to 3.7.0.0-SNAPSHOT so the bundled PPL grammar exposes the `multisearch` keyword (along with table/regex/rex/convert added since 3.6). The SQL Snapshots repo (already declared in the build) carries the published 3.7 artifacts; for local sql-repo HEAD development, run `./gradlew :ppl:publishUnifiedQueryPublicationToMavenLocal` from the sql repo. The version bump is independent of the in-flight test-ppl-frontend UnifiedQueryService.setting() change in opensearch-project#21526 — different files, no conflict. Validates: 3/3 MultisearchCommandIT pass; full `:sandbox:qa:analytics-engine-rest:integTest` suite still green (110 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Spotless reformat of testFinalAggInnerStageScanRowType Single-line spotless reformat in DataFusionFragmentConvertorTests: the OpenSearchStageInputScan constructor call now fits on one line (it was previously broken across multiple lines). Originally bumped sandbox/plugins/analytics-backend-datafusion's sqlUnifiedQueryVersion 3.6 -> 3.7 to align with test-ppl-frontend, but the entire internalClusterTest classpath block (including that pin) was removed upstream by opensearch-project#21555 (dbe4a42, "Enable Lucene Filter delegation from Datafusion for Correctness"). The build.gradle hunk dropped during rebase; only the spotless reformat survives. Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Register CASE in project capabilities + QA IT Calcite emits SqlKind.CASE for any conditional expression — explicit `eval x = case(cond, val, …)` in PPL, plus the `count(eval(predicate))` conditional-count idiom (lowered to COUNT(CASE WHEN predicate THEN … END)) and several other shapes. Without CASE in `STANDARD_PROJECT_OPS`, the analytics planner rejected the operator with `No backend supports scalar function [CASE] among [datafusion]` before substrait emission. CASE doesn't need a backend adapter: isthmus translates SqlKind.CASE structurally to a Substrait IfThen rel, and DataFusion's substrait consumer handles IfThen natively. Just registering the capability is enough. Adds `testMultisearchEvalCaseProjection` to MultisearchCommandIT to pin the end-to-end path — multisearch + `eval bucket = case(cond, val else default)` + stats. Uses an explicit `else` arm so isthmus doesn't have to convert an untyped NULL literal; the implicit-else `count(eval(…))` shape that the v2-side testMultisearchSuccessRatePattern uses still hits a separate isthmus limitation (`Unable to convert the type NULL` from `TypeConverter` on a SqlTypeName.NULL literal — tracked separately, out of scope here). Validates: 4/4 MultisearchCommandIT pass; full :sandbox:qa:analytics-engine-rest:integTest suite still green (111 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Pre-isthmus untyped-NULL rewriter for CASE arms Calcite emits a `RexLiteral` with `SqlTypeName.NULL` for the implicit ELSE arm of `CASE WHEN cond THEN val END` — exactly the shape PPL `count(eval(predicate))` lowers to (`COUNT(CASE WHEN predicate THEN <projected> END)`). Isthmus' `TypeConverter.toSubstrait` rejects `SqlTypeName.NULL` with `Unable to convert the type NULL`, blocking the analytics path before substrait emission. Adds `UntypedNullPreprocessor` — a `RelHomogeneousShuttle` + `RexShuttle` pass applied in `convertToSubstrait` and `convertStandalone` *before* the SubstraitRelVisitor sees the plan. Walks every CASE call's value operands (THEN arms and the ELSE arm) and substitutes any `SqlTypeName.NULL` literal with a typed null literal matching the CASE's resolved return type. Calcite already widens the CASE's return type to the leastRestrictive of branches, so the substituted type is correct by construction. Scope is intentionally narrow: only CASE call operands are rewritten today. Other untyped-NULL contexts (function arguments, comparison RHS) are rare in PPL-generated plans and would need per-operator type inference to do safely; defer until a concrete test surfaces one. Test coverage: - `UntypedNullPreprocessorTests` (4 new): * `testCountEvalCaseRewritesElseNullToTypedNull` — the motivating shape `COUNT(CASE WHEN cond THEN 1 ELSE null END)`. * `testCaseWithThenNullIsAlsoRewritten` — null in the THEN arm. * `testCaseConditionOperandUnchanged` — even-index condition operands left alone. * `testCountOverRewrittenCaseProjectionTypechecks` — Aggregate(Project(CASE)) with the rewriter applied still type-checks end-to-end. - New `testMultisearchCountEvalConditionalCount` in MultisearchCommandIT — mirrors the v2-side `CalciteMultisearchCommandIT.testMultisearchSuccessRatePattern` shape (`count(eval(predicate))`) end-to-end on the analytics-engine REST path. Validates: 5/5 MultisearchCommandIT pass; 4/4 new + 12/12 existing FragmentConvertor unit tests; full :sandbox:qa:analytics-engine-rest:integTest suite still green (112 tests across 14 ITs). Signed-off-by: Kai Huang <ahkcs@amazon.com> * [Analytics Backend / DataFusion] Simplify testCaseConditionOperandUnchanged for spotless The original assertion wrapped a no-op single-iteration loop around an empty- body RexShuttle whose accept() result is just caseExpr.toString(). Spotless flagged the empty class body (`new RexShuttle() {}`) as a formatting violation and tried to wrap it across two lines, which read worse than the underlying intent — comparing the input CASE expression to the rewriter's output to prove no-op behavior when no untyped nulls are present. Replace the loop+shuttle with a direct `assertEquals(caseExpr.toString(), rewrittenCase.toString())` — same semantics, cleaner code, no awkward formatting. The test still asserts the rewriter doesn't touch CASE expressions whose operands are already typed. Sandbox check (`./gradlew check -p sandbox -Dsandbox.enabled=true`) now passes end-to-end. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
Summary
Lands two QA ITs against the analytics-engine REST path so PPL
appendpipeandtableare verified inside core without cross-plugin dependencies on the SQL plugin. Both reuse the existingcalcsparquet-backed dataset viaDatasetProvisioner; no new fixtures.AppendPipeCommandIT— 3 testsappendpipealready passes 4/4 of its v2-sideCalcitePPLAppendPipeCommandITcases on the analytics-engine route undertests.analytics.force_routing=true. The existing capability surface (LogicalUnion + LogicalAggregate over SUM + SchemaUnifier type-conflict) is sufficient — no code changes in core for this command.testAppendPipeSortsort str0and the innersort -sum_int0_by_str0pin both branches' orders, so positional row comparison works.testAppendPipeWithMergedColumnstats sum(sum) as sumsum. Branch arrival order at the coordinator's Union is non-deterministic, so multiset comparison.testAppendPipeWithConflictTypeColumn"due to incompatible types"before execution.testAppendDifferentIndexfrom the v2-side IT is intentionally not ported: it exercisesappend(separatesource=...sub-search), which is already covered byAppendCommandIT.TableCommandIT— 5 tests + 1 small fix intest-ppl-frontendtableis a syntactic alias offields— the v2AstBuilderdispatches both throughbuildProjectCommandonceplugins.calcite.enabled=trueis visible to the AstBuilder via theUnifiedQueryContext. The mirror fix on the SQL plugin side landed in opensearch-project/sql#5413; this PR closes the same gap on the test-ppl-frontend side, where theUnifiedQueryContextis constructed locally rather than coming from the SQL plugin.Two changes:
UnifiedQueryServicenow setsplugins.calcite.enabled=trueon the context. The unified path is Calcite-based by definition; without this flag the AstBuilder rejectstable/regex/rex/convert.TableCommandITcovering surfaces specific to thetablekeyword: comma-delimited, space-delimited, suffix wildcard, leading--exclusion, andfields↔tablerow-and-schema equivalence on identical inputs. Plain projection semantics already covered byFieldsCommandITare not duplicated.testTableCommaDelimitedtable str0, num0— same asfields a, b; sanity on thetablekeyword reachingbuildProjectCommand.testTableSpaceDelimitedtable str0 num0 int0— unique totable; lexer accepts whitespace as separator.testTableSuffixWildcardtable *0— wildcard expansion at parse time; analyzer-dependent column order, so set-equality.testTableMinusExclusiontable - num0, num1, num2, num3, num4— leading--exclusion form; analytics path retains exclusion semantics.testFieldsAndTableEquivalencefields a, b, cvstable a, b, c— identical schema and rows; alias claim made explicit at the response level.Test plan
./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*AppendPipeCommandIT"— 3 / 3 green./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*TableCommandIT"— 5 / 5 greenCalcitePPLAppendPipeCommandITagainst this branch with-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true— 4 / 4 (verified before this PR — analytics path was already fine, no code changes needed in core)Out of scope
PPL
multisearchwas originally bundled with this PR. Triage showed ~12 of its 15 analytics-route failures are blocked on a Substrait-side issue: DataFusion's substrait consumer rejects the Plan emitted forLogicalUnion(StageInputScan, StageInputScan)withThe registered child-stage Arrow schema width vs. the
Plan.Root.nameswidth disagree somewhere betweenLocalStageScheduler.buildChildInputsand DataFusion'smake_renamed_schema. Diagnostic logging confirms we send 12 names + a Substraitsetrel with tworead.named_tableinputs whosebase_schemaeach has 12 fields — DataFusion'smake_renamed_schemaonly consumes 2 of the 12 names against the deserialized Union output schema, suggesting the registered partition table forinput-<stageId>is exposing a 2-field schema instead of the expected 12-field one. Root cause not yet identified — out of scope here; tracked for a follow-up PR.Other categories are kept out of scope by the same scope discipline as #21521:
AVGSubstrait-isthmus binding)ArrowSchemaFromCalcite.toArrowTypedoesn't handleTIMESTAMP/DATE/TIME/TIMESTAMP_WITH_LOCAL_TIME_ZONE)AND/OR/NOT/IS_NULL/etc. in CASE projections)spantime-bucketingEach is its own focused PR.