Default plugins.calcite.enabled=true on the unified query path#5413
Conversation
The unified query handler (`RestUnifiedQueryAction` → `TransportPPLQueryAction`
→ analytics-engine) builds its `UnifiedQueryContext` with an empty `Settings`
map; nothing wires the cluster setting through. PPL parsing is delegated to
the same `AstBuilder` the v2 path uses, and that builder gates `table` (and
other Calcite-only commands) on `Settings.Key.CALCITE_ENGINE_ENABLED`. With
no setting propagated, the gate sees `null`, fails the `Boolean.TRUE.equals`
check, and throws
UnsupportedOperationException: Table command is supported only when
plugins.calcite.enabled=true
even when the cluster setting is true, blocking every `table` query routed
through the analytics path under `tests.analytics.force_routing=true`.
The unified path is by definition Calcite-based — every query reaching
`UnifiedQueryContext` flows through Calcite's planner. Default
`CALCITE_ENGINE_ENABLED=true` in `buildSettings()` when the underlying map
doesn't have the key. This unblocks `table` and any other AstBuilder gate
that defends the same toggle, without changing v2 behavior (v2 constructs
`AstBuilder` with cluster `Settings`, not the unified context).
Also refresh the PPL analytics-engine routing dev doc to document the
unified-context dependency and switch the per-command verification recipe
from the bundle-branch `analyticsCompatibilityTest` task to the standard
`integTestRemote -Dtests.analytics.{force_routing,parquet_indices}=true` —
the bundle-branch task is purely for the daily coverage-report sweep.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 4ef37bd.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
PR Reviewer Guide 🔍(Review updated until commit 4ade92f)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 4ade92f
Previous suggestionsSuggestions up to commit 4ef37bd
|
| // {@code plugins.calcite.enabled} to true so AstBuilder gates (e.g. visitTableCommand) | ||
| // that protect v2-only code paths don't block valid analytics-engine queries when the | ||
| // underlying setting hasn't been propagated through Builder#setting. | ||
| if (key == Key.CALCITE_ENGINE_ENABLED && !settings.containsKey(key)) { |
There was a problem hiding this comment.
Either add it to default setting list or pass via setting() API since only unified PPL require this?
Ref:
…r surface Bucket-1 capability-registry expansion for the analytics-engine route. Pairs with opensearch-project#21498 (eval string concat / CAST / SAFE_CAST / `||`-resolver / ConcatFunctionAdapter) — independent surfaces, no overlap. After this PR, `CalciteFieldsCommandIT` 39/39, `CalciteRenameCommandIT` 2/2, `CalciteHeadCommandIT` 4/4 are 100% green under `tests.analytics.force_routing=true`, and the Sort suites pick up the bulk of their cast / abs / substring push-down gains. All changes are Bucket-1 in the routing-doc taxonomy: the DataFusion runtime already implements every operator listed; this PR just declares the capability so the Layer-2 planner stops rejecting the calls. Three additive layers: 1. ScalarFunction — add AND, OR, NOT enum constants. The filter rule structurally recurses into AND/OR/NOT and never looks them up, but the project rule does — these must appear in the enum + STANDARD_PROJECT_OPS for eval predicates like `count(eval(balance > 20000 and age < 35))` where AND is a sub-expression of CASE. 2. DataFusionAnalyticsBackendPlugin — - STANDARD_PROJECT_OPS additions: * FLOOR, ABS — sort-by `abs(balance)` push-down (SortCommandIT.testPushdownSortExpressionContainsNull). * IS_NULL, IS_NOT_NULL — sort-by `isnull(...)` and eval guards. * AND, OR, NOT — boolean ops in CASE predicates. * CASE, NULLIF — conditional projections in eval. * UPPER, LOWER, TRIM, SUBSTRING, CHAR_LENGTH, CONCAT — sort-by `substring(...)` push-down (CalcitePPLSortIT.testPushdownSortStringExpression), eval string transforms. - AGG_FUNCTIONS additions: STDDEV_POP, STDDEV_SAMP, VAR_POP, VAR_SAMP for `stats stddev_samp(...)` / `stats var_samp(...)`. - aggregateCapabilities() now dispatches on AggregateFunction.Type so a single mixed-category list works — the previous unconditional AggregateCapability.simple(...) asserts on non-SIMPLE inputs and crashes plugin init when STDDEV/VAR are added. 3. analytics-engine/build.gradle — bundle commons-text:1.11.0. Calcite's SqlFunctions.<clinit> eagerly references org.apache.commons.text.similarity.LevenshteinDistance (SOUNDEX/JARO_WINKLER); without bundling the jar, the first agg query that touches SqlFunctions kills the cluster with NoClassDefFoundError. The existing resolutionStrategy.force pin pins the version but doesn't bundle. Test plan: * `./gradlew :sandbox:libs:analytics-framework:check :sandbox:plugins:analytics-backend-datafusion:check :sandbox:plugins:analytics-engine:check -Dsandbox.enabled=true` green. * SQL-plugin ITs against this branch (cluster) + companion SQL plugin opensearch-project/sql#5413, with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`. Routing verified: 654 analytics-engine PlannerImpl entries, 0 v2 PPLService entries. Out of scope (separate follow-ups, surface mostly orthogonal): * `Unable to find binding for call AVG($N)` — Substrait isthmus' default AggregateFunctionConverter rejects Calcite's AVG/STDDEV_SAMP/VAR_SAMP signatures. Needs an AggregateSig-style additional-mappings hook registered in DataFusionFragmentConvertor. Once unblocked, the new STDDEV/VAR entries here will start contributing real test wins. * Window functions — `dedup` lowers to ROW_NUMBER OVER. RexOver reaches the project rule but isn't recognized by ScalarFunction.fromSqlKind. Blocks CalciteDedupCommandIT and CalcitePPLDedupIT. * Advanced aggregates / PPL functions — first, last, take, arg_max, percentile_approx, distinct_count_approx, PPL `span` need new enum constants + DataFusion adapters or YAML extensions. Signed-off-by: Kai Huang <ahkcs@amazon.com>
These local routing-and-coverage notes are useful but belong in their own review thread (or stay as untracked working notes), not in the unified-context fix. Keeping them on disk via .gitignore-style untrack so the PR stays focused on the single api/ change. Signed-off-by: Kai Huang <ahkcs@amazon.com>
@dai-chen suggested either adding the setting to the default list at UnifiedQueryContext.java:123 or passing it via the .setting() API on the builder, since only the unified PPL path requires it. Going with option 1 (default-list entry) — it composes the same way every other planning-required default does, and avoids forcing every caller (the production REST handler and every IT) to remember a single magic .setting() call. The IT at UnifiedQueryOpenSearchIT.java:51 was the precedent for the .setting() approach but only one IT needs it; the production caller (RestUnifiedQueryAction) wouldn't have a natural place to wire this without either repeating it everywhere or routing through a helper. Removes the conditional in buildSettings().getSettingValue() — the default map now carries the value the same way QUERY_SIZE_LIMIT, PPL_SUBSEARCH_MAXOUT, and PPL_JOIN_SUBSEARCH_MAXOUT do. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 1d4c113 |
…elds/rename/head/sort Bucket-1 capability-registry expansion for the analytics-engine route — narrow scope: only the two scalar functions PPL sort push-down materialises into a projection (ABS, SUBSTRING). Fields / rename / head don't add scalar surface; they're covered here purely by new QA ITs that lock in the routing-and-shape behavior end-to-end through `POST /_analytics/ppl`. After this PR (with the eval-side surface from opensearch-project#21498 already on main): * `CalciteFieldsCommandIT`, `CalciteRenameCommandIT`, `CalciteHeadCommandIT` 100% green on the analytics path under `tests.analytics.force_routing=true`. * `CalciteSortCommandIT` and `CalcitePPLSortIT` pick up the cast / abs / substring push-down failures (CAST is already in `STANDARD_PROJECT_OPS` upstream from opensearch-project#21476; ABS and SUBSTRING are this PR's contribution). ## Changes **1. `DataFusionAnalyticsBackendPlugin` — `STANDARD_PROJECT_OPS` += ABS, SUBSTRING.** PPL sort push-down lifts an expression like `abs(num0)` or `substring(str0, 1, 3)` into a `LogicalProject` child of the sort, which is what the project rule's capability check sees. DataFusion has both natively; isthmus' default extension catalog already binds them. Without this, the analytics planner rejects the projection with `No backend supports scalar function [ABS] among [datafusion]`. **2. QA ITs in `sandbox/qa/analytics-engine-rest`** — one per command, each self-contained and provisioning the existing `calcs` parquet-backed dataset via `DatasetProvisioner`. Tests fire through `POST /_analytics/ppl` so the core build can validate the analytics-engine path without the SQL plugin. Mirror the failing surface in `CalciteFieldsCommandIT` / `CalciteRenameCommandIT` / `CalciteHeadCommandIT` / `CalciteSortCommandIT` one query at a time: * `FieldsCommandIT` (5 tests) — basic projection, single-column, explicit order, suffix-wildcard `*0` (set-equality, since wildcard expansion order isn't part of the contract), and `fields - num*` exclusion. * `RenameCommandIT` (4 tests) — single rename, multi-rename, post-rename reference fails with "not found", backtick-quoted target names. * `HeadCommandIT` (5 tests) — default-10 cap, explicit count, count > total rows, `head N from M` offset, and value-equality on the first 5 rows (parquet preserves insertion order, so this is deterministic). * `SortCommandIT` (5 tests) — plain ASC/DESC by integer (with calcs' 6 null int0 entries placed at the head/tail per Calcite's nulls-first/last defaults), `eval n = abs(num0) | sort n` covering the 9 null + 8 non-null abs values, and `eval s = substring(str2, 1, 3) | sort s` validating the SUBSTRING capability end-to-end against the 17-row calcs dataset. ## Test plan * `./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*FieldsCommandIT" --tests "*RenameCommandIT" --tests "*HeadCommandIT" --tests "*SortCommandIT"` — 19/19 green. * `./gradlew check -p sandbox -Dsandbox.enabled=true` — green (the unrelated `ScalarDateTimeFunctionIT.testConvertTz` flake from a stale local `libopensearch_native.dylib` resolved by rebuilding the Rust crate; not caused by this PR). * SQL-plugin Calcite ITs against this branch + companion opensearch-project/sql#5413, with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`: `CalciteFieldsCommandIT` 39/39, `CalciteRenameCommandIT` 2/2, `CalciteHeadCommandIT` 4/4, plus +5 sort-push-down wins in `CalciteSortCommandIT` and +1 in `CalcitePPLSortIT` from the ABS / SUBSTRING capability additions. ## Out of scope (separate follow-ups) * `Unable to find binding for call AVG($N)` Substrait-isthmus issue — needs an `AggregateSig`-style additional-mappings hook in `DataFusionFragmentConvertor`. * Window functions (`dedup` lowers to `ROW_NUMBER OVER`). * Advanced aggregates (`first`, `last`, `take`, `arg_max`, `percentile_approx`, `distinct_count_approx`) and PPL `span`. * The eval-predicate surface (`AND`/`OR`/`NOT` in CASE projections, `IS_NULL`/`IS_NOT_NULL`, broader string/conditional ops) and STDDEV/VAR aggregates — kept out of this PR to keep the scope focused on the four commands the QA ITs cover. Signed-off-by: Kai Huang <ahkcs@amazon.com>
Previously, {@code verifyStandardDataRows} (and a handful of bespoke
{@code verifyDataRows} calls in this file) compared rows positionally —
they assumed the engine emits columns in the {@code _source} iteration
order the v2 / Lucene path produces. Under the analytics-engine route the
parquet-backed reader returns columns in storage order, so the same 4
canonical state_country rows came back as {@code [70,"USA",4,"Jake",
"California",2023]} instead of {@code [Jake,USA,California,4,2023,70]}
and 12 of 22 tests failed despite the data being identical.
Replace with a column-name-keyed expected-row map. The helper reads the
actual schema from the response, looks up each canonical value by column
name, places it at the corresponding schema position, then defers to
{@code verifyDataRows} as before. The contract is identical to the
existing {@code verifySchema} matcher — both are set-equality on column
names — so the test no longer leaks the engine's emission order into
the assertion.
Each call site passes the canonical column-name list (with rename
substitutions where applicable). Tests that don't rename age keep
calling the no-arg form. Both paths now pass:
* Analytics-engine route (`tests.analytics.force_routing=true`): 20 / 22
(the remaining 2 are `testRenameInAgg` /
`testRenameWithBackticksInAgg`, blocked on the Substrait isthmus
AVG-binding follow-up tracked in
opensearch-project/OpenSearch#21521).
* v2 / Calcite route (default routing): 20 / 22 (same two tests fail with
`NoClassDefFoundError: LevenshteinDistance` only when running against
the OS-core `:run` cluster — that bundle is missing commons-text.
CI's own integ-test cluster bundles commons-text via the SQL plugin's
classloader and isn't affected.)
Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 4ade92f |
59c728b
into
opensearch-project:feature/mustang-ppl-integration
…elds/rename/head/sort (#21521) Bucket-1 capability-registry expansion for the analytics-engine route — narrow scope: only the two scalar functions PPL sort push-down materialises into a projection (ABS, SUBSTRING). Fields / rename / head don't add scalar surface; they're covered here purely by new QA ITs that lock in the routing-and-shape behavior end-to-end through `POST /_analytics/ppl`. After this PR (with the eval-side surface from #21498 already on main): * `CalciteFieldsCommandIT`, `CalciteRenameCommandIT`, `CalciteHeadCommandIT` 100% green on the analytics path under `tests.analytics.force_routing=true`. * `CalciteSortCommandIT` and `CalcitePPLSortIT` pick up the cast / abs / substring push-down failures (CAST is already in `STANDARD_PROJECT_OPS` upstream from #21476; ABS and SUBSTRING are this PR's contribution). ## Changes **1. `DataFusionAnalyticsBackendPlugin` — `STANDARD_PROJECT_OPS` += ABS, SUBSTRING.** PPL sort push-down lifts an expression like `abs(num0)` or `substring(str0, 1, 3)` into a `LogicalProject` child of the sort, which is what the project rule's capability check sees. DataFusion has both natively; isthmus' default extension catalog already binds them. Without this, the analytics planner rejects the projection with `No backend supports scalar function [ABS] among [datafusion]`. **2. QA ITs in `sandbox/qa/analytics-engine-rest`** — one per command, each self-contained and provisioning the existing `calcs` parquet-backed dataset via `DatasetProvisioner`. Tests fire through `POST /_analytics/ppl` so the core build can validate the analytics-engine path without the SQL plugin. Mirror the failing surface in `CalciteFieldsCommandIT` / `CalciteRenameCommandIT` / `CalciteHeadCommandIT` / `CalciteSortCommandIT` one query at a time: * `FieldsCommandIT` (5 tests) — basic projection, single-column, explicit order, suffix-wildcard `*0` (set-equality, since wildcard expansion order isn't part of the contract), and `fields - num*` exclusion. * `RenameCommandIT` (4 tests) — single rename, multi-rename, post-rename reference fails with "not found", backtick-quoted target names. * `HeadCommandIT` (5 tests) — default-10 cap, explicit count, count > total rows, `head N from M` offset, and value-equality on the first 5 rows (parquet preserves insertion order, so this is deterministic). * `SortCommandIT` (5 tests) — plain ASC/DESC by integer (with calcs' 6 null int0 entries placed at the head/tail per Calcite's nulls-first/last defaults), `eval n = abs(num0) | sort n` covering the 9 null + 8 non-null abs values, and `eval s = substring(str2, 1, 3) | sort s` validating the SUBSTRING capability end-to-end against the 17-row calcs dataset. ## Test plan * `./gradlew :sandbox:qa:analytics-engine-rest:integTest -Dsandbox.enabled=true --tests "*FieldsCommandIT" --tests "*RenameCommandIT" --tests "*HeadCommandIT" --tests "*SortCommandIT"` — 19/19 green. * `./gradlew check -p sandbox -Dsandbox.enabled=true` — green (the unrelated `ScalarDateTimeFunctionIT.testConvertTz` flake from a stale local `libopensearch_native.dylib` resolved by rebuilding the Rust crate; not caused by this PR). * SQL-plugin Calcite ITs against this branch + companion opensearch-project/sql#5413, with `-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`: `CalciteFieldsCommandIT` 39/39, `CalciteRenameCommandIT` 2/2, `CalciteHeadCommandIT` 4/4, plus +5 sort-push-down wins in `CalciteSortCommandIT` and +1 in `CalcitePPLSortIT` from the ABS / SUBSTRING capability additions. ## Out of scope (separate follow-ups) * `Unable to find binding for call AVG($N)` Substrait-isthmus issue — needs an `AggregateSig`-style additional-mappings hook in `DataFusionFragmentConvertor`. * Window functions (`dedup` lowers to `ROW_NUMBER OVER`). * Advanced aggregates (`first`, `last`, `take`, `arg_max`, `percentile_approx`, `distinct_count_approx`) and PPL `span`. * The eval-predicate surface (`AND`/`OR`/`NOT` in CASE projections, `IS_NULL`/`IS_NOT_NULL`, broader string/conditional ops) and STDDEV/VAR aggregates — kept out of this PR to keep the scope focused on the four commands the QA ITs cover. Signed-off-by: Kai Huang <ahkcs@amazon.com>
`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>
…ne route The analytics-engine route and the v2 / Lucene path return columns in different orders when there is no explicit `| fields ...` clause: parquet preserves the storage order chosen by the on-disk format, while the Lucene path preserves `_source` iteration order. Both are valid given the contract `verifySchema` declares (set equality on column names), so positional `verifyDataRows` assertions over-constrain the test and fail under `-Dtests.analytics.force_routing=true` even when the data is correct. Apply the same column-name-keyed match pattern Kai introduced for `CalcitePPLRenameIT` in 59c728b (opensearch-project#5413): * Add `rowOf(key1, val1, ...)` to build column-keyed expected rows. * Add `verifyDataRowsByColumn(...)` to look up each cell value by column name and reorder to match the response schema before delegating to the existing positional `verifyDataRows` matcher. * Convert the four order-sensitive tests (`testMultipleReplace`, `testEmptyStringReplacement`, `testMultipleFieldsInClause`, `testMultiplePairsInSingleCommand`) to the new helpers. * Make `testReplaceNonExistentField` order-agnostic on the `input fields are: [...]` field list — assert that the prefix and every expected field name appear in the message, but not in a fixed order. Test results against analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`: 21/21 pass in both the direct `CalciteReplaceCommandIT` suite and the `CalciteNoPushdownIT > CalciteReplaceCommandIT` re-run. v2 path remains green. Companion to the OpenSearch PR onboarding PPL `replace` command + `replace()` / `regexp_replace()` functions on the analytics-engine route via DataFusion `replace` / `regexp_replace` UDFs. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…ne route The analytics-engine route and the v2 / Lucene path return columns in different orders when there is no explicit `| fields ...` clause: parquet preserves the storage order chosen by the on-disk format, while the Lucene path preserves `_source` iteration order. Both are valid given the contract `verifySchema` declares (set equality on column names), so positional `verifyDataRows` assertions over-constrain the test and fail under `-Dtests.analytics.force_routing=true` even when the data is correct. Apply the same column-name-keyed match pattern Kai introduced for `CalcitePPLRenameIT` in 59c728b (opensearch-project#5413): * Add `rowOf(key1, val1, ...)` to build column-keyed expected rows. * Add `verifyDataRowsByColumn(...)` to look up each cell value by column name and reorder to match the response schema before delegating to the existing positional `verifyDataRows` matcher. * Convert the four order-sensitive tests (`testMultipleReplace`, `testEmptyStringReplacement`, `testMultipleFieldsInClause`, `testMultiplePairsInSingleCommand`) to the new helpers. * Make `testReplaceNonExistentField` order-agnostic on the `input fields are: [...]` field list — assert that the prefix and every expected field name appear in the message, but not in a fixed order. Test results against analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`: 21/21 pass in both the direct `CalciteReplaceCommandIT` suite and the `CalciteNoPushdownIT > CalciteReplaceCommandIT` re-run. v2 path remains green. Companion to the OpenSearch PR onboarding PPL `replace` command + `replace()` / `regexp_replace()` functions on the analytics-engine route via DataFusion `replace` / `regexp_replace` UDFs. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…ine REST path (#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 #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>
…ne route The analytics-engine route and the v2 / Lucene path return columns in different orders when there is no explicit `| fields ...` clause: parquet preserves the storage order chosen by the on-disk format, while the Lucene path preserves `_source` iteration order. Both are valid given the contract `verifySchema` declares (set equality on column names), so positional `verifyDataRows` assertions over-constrain the test and fail under `-Dtests.analytics.force_routing=true` even when the data is correct. Apply the same column-name-keyed match pattern Kai introduced for `CalcitePPLRenameIT` in 59c728b (opensearch-project#5413): * Add `rowOf(key1, val1, ...)` to build column-keyed expected rows. * Add `verifyDataRowsByColumn(...)` to look up each cell value by column name and reorder to match the response schema before delegating to the existing positional `verifyDataRows` matcher. * Convert the four order-sensitive tests (`testMultipleReplace`, `testEmptyStringReplacement`, `testMultipleFieldsInClause`, `testMultiplePairsInSingleCommand`) to the new helpers. * Make `testReplaceNonExistentField` order-agnostic on the `input fields are: [...]` field list — assert that the prefix and every expected field name appear in the message, but not in a fixed order. Test results against analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`: 21/21 pass in both the direct `CalciteReplaceCommandIT` suite and the `CalciteNoPushdownIT > CalciteReplaceCommandIT` re-run. v2 path remains green. Companion to the OpenSearch PR onboarding PPL `replace` command + `replace()` / `regexp_replace()` functions on the analytics-engine route via DataFusion `replace` / `regexp_replace` UDFs. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…ne route (#5415) The analytics-engine route and the v2 / Lucene path return columns in different orders when there is no explicit `| fields ...` clause: parquet preserves the storage order chosen by the on-disk format, while the Lucene path preserves `_source` iteration order. Both are valid given the contract `verifySchema` declares (set equality on column names), so positional `verifyDataRows` assertions over-constrain the test and fail under `-Dtests.analytics.force_routing=true` even when the data is correct. Apply the same column-name-keyed match pattern Kai introduced for `CalcitePPLRenameIT` in 59c728b (#5413): * Add `rowOf(key1, val1, ...)` to build column-keyed expected rows. * Add `verifyDataRowsByColumn(...)` to look up each cell value by column name and reorder to match the response schema before delegating to the existing positional `verifyDataRows` matcher. * Convert the four order-sensitive tests (`testMultipleReplace`, `testEmptyStringReplacement`, `testMultipleFieldsInClause`, `testMultiplePairsInSingleCommand`) to the new helpers. * Make `testReplaceNonExistentField` order-agnostic on the `input fields are: [...]` field list — assert that the prefix and every expected field name appear in the message, but not in a fixed order. Test results against analytics-engine route via `-Dtests.analytics.{force_routing,parquet_indices}=true`: 21/21 pass in both the direct `CalciteReplaceCommandIT` suite and the `CalciteNoPushdownIT > CalciteReplaceCommandIT` re-run. v2 path remains green. Companion to the OpenSearch PR onboarding PPL `replace` command + `replace()` / `regexp_replace()` functions on the analytics-engine route via DataFusion `replace` / `regexp_replace` UDFs. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
The PPL `rex` command's AstBuilder reads `Settings.Key.PPL_REX_MAX_MATCH_LIMIT`
unconditionally and unboxes the result to `int`:
int maxMatchLimit =
(settings != null) ? settings.getSettingValue(...) : 10;
The `(settings != null)` guard only protects against the Settings instance
being null — not against `getSettingValue` returning null for a key that the
caller never registered. On the unified query path, `UnifiedQueryContext`
builds its `Settings` map with only a small whitelist of keys (e.g.
`QUERY_SIZE_LIMIT`, `CALCITE_ENGINE_ENABLED` per opensearch-project#5413). For any unregistered
key, `getSettingValue` returns null, and the auto-unbox NPEs the planner
before any operator-level capability check runs. Every `rex` query through
`/_analytics/ppl` (the analytics-engine route's REST front-end) hits this
NPE today.
Default `PPL_REX_MAX_MATCH_LIMIT=10` in `buildSettings()` so unified-path
behavior matches the cluster-side default registered by
`OpenSearchSettings.PPL_REX_MAX_MATCH_LIMIT_SETTING` — making the v2 path and
the analytics-engine path agree on the same fallback value when neither has
an explicit cluster override. Mirrors the precedent Kai introduced for
`CALCITE_ENGINE_ENABLED` in opensearch-project#5413.
Companion to the OpenSearch core PR onboarding PPL `rex mode=sed` to the
analytics-engine route via DataFusion (Part 1 — sed-mode bridge only;
extract-mode Rust UDFs deferred to Part 2).
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…ation Brings the catch-up branch up to current upstream/main (4 commits since this PR was opened) and current feature/mustang-ppl-integration (9 commits since this PR was opened), so the PR is mergeable into feature/mustang-ppl-integration without conflicts. Squashed (rather than two real merge commits) for the same DCO reason the original commit was squashed: upstream commits authored by many contributors with inconsistent or missing Signed-off-by trailers would otherwise be brought into this PR's history. Newer main commits absorbed (4): - opensearch-project#5419 (LENGTH/REGEXP_REPLACE/DATE_TRUNC unified function spec) - opensearch-project#5408 (datetime type normalization) - opensearch-project#5414 (Gradle wrapper bump + @ignore exclusion) - opensearch-project#5399 (FGAC-scoped SQL cursor continuation) Newer feature commits absorbed (9): - opensearch-project#5403 (analytics-engine optional dependency — major rewiring) - opensearch-project#5407 (Carry CalciteEvalCommandIT through helper-managed index path) - opensearch-project#5413 (Default plugins.calcite.enabled=true on unified path) - opensearch-project#5415, opensearch-project#5416, opensearch-project#5417, opensearch-project#5409, opensearch-project#5400, opensearch-project#5406 (smaller carryovers + bumps) Conflict resolutions (10 from main side, 3 from feature side): api/spec/* (LanguageSpec, UnifiedFunctionSpec, UnifiedPplSpec, UnifiedSqlSpec): took main. Main is a strict superset — adds postAnalysisRules and preCompilationRules extension points, the new FunctionSpecBuilder DSL, SCALAR category for length/regexp_replace/ date_trunc, the DatetimeExtension on PPL spec, and the CoreExtension wiring on SQL spec. PR's RELEVANCE category is preserved unchanged. api/UnifiedQueryPlanner.java, api/compiler/UnifiedQueryCompiler.java: took main. Both adopt the new postAnalysisRules / preCompilationRules hooks introduced in opensearch-project#5408 / opensearch-project#5419. core/executor/QueryService.java: composed both sides — kept HEAD's CalciteClassLoaderHelper.withCalciteClassLoader wrapper around main's StageErrorHandler stage tracking. Same pattern as the original PR resolution; both improvements are orthogonal. legacy/plugin/RestSqlAction.java: took HEAD. The 3-way merge produced a duplicated handleException/getRawErrorCode block; HEAD already contained both the delegateToV2Engine refactor and the ErrorReport unwrap from main, so HEAD is the correct superset. integ-test/build.gradle: took feature. Both sides added the same @ignore exclusion block; feature has alphabetical ordering and a more detailed comment explaining the Gradle 9.4.1 TestEventReporterAsListener cast bug. integ-test/.../CalciteEvalCommandIT.java: composed both sides. Took feature's helper-managed test_eval provisioning (createIndexByRestClient + isIndexExist guard, from opensearch-project#5407) so analytics-engine compatibility runs get a parquet-backed index. Added back PR HEAD's test_eval_agent setup (needed by the dotted-path eval tests for opensearch-project#5351) wrapped in its own isIndexExist guard for the same parquet-aware idempotency. plugin/.../TransportPPLQueryAction.java: took feature. PR opensearch-project#5403 made analytics-engine an optional dependency by moving QueryPlanExecutor from a required constructor parameter to an @Inject(optional=true) setter. Feature's design supersedes our prior wiring. plugin/.../SQLPlugin.java: took feature. The same opensearch-project#5403 simplification removed loadExtensions/EngineExtensionsHolder/executionEngineExtensions plumbing (no longer needed once analytics-engine is optionally bound). Feature retains the createSqlAnalyticsRouter method this PR introduced. plugin/.../config/EngineExtensionsHolder.java: deleted. Unreferenced after taking feature's SQLPlugin/TransportPPLQueryAction; not present on feature branch. Build: :api, :core, :opensearch-sql-plugin, :legacy compileJava + :integ-test compileTestJava all pass; unit tests pass; spotlessCheck clean. Signed-off-by: Kai Huang <ahkcs@amazon.com>
The PPL `rex` command's AstBuilder reads `Settings.Key.PPL_REX_MAX_MATCH_LIMIT`
unconditionally and unboxes the result to `int`:
int maxMatchLimit =
(settings != null) ? settings.getSettingValue(...) : 10;
The `(settings != null)` guard only protects against the Settings instance
being null — not against `getSettingValue` returning null for a key that the
caller never registered. On the unified query path, `UnifiedQueryContext`
builds its `Settings` map with only a small whitelist of keys (e.g.
`QUERY_SIZE_LIMIT`, `CALCITE_ENGINE_ENABLED` per opensearch-project#5413). For any unregistered
key, `getSettingValue` returns null, and the auto-unbox NPEs the planner
before any operator-level capability check runs. Every `rex` query through
`/_analytics/ppl` (the analytics-engine route's REST front-end) hits this
NPE today.
Default `PPL_REX_MAX_MATCH_LIMIT=10` in `buildSettings()` so unified-path
behavior matches the cluster-side default registered by
`OpenSearchSettings.PPL_REX_MAX_MATCH_LIMIT_SETTING` — making the v2 path and
the analytics-engine path agree on the same fallback value when neither has
an explicit cluster override. Mirrors the precedent Kai introduced for
`CALCITE_ENGINE_ENABLED` in opensearch-project#5413.
Companion to the OpenSearch core PR onboarding PPL `rex mode=sed` to the
analytics-engine route via DataFusion (Part 1 — sed-mode bridge only;
extract-mode Rust UDFs deferred to Part 2).
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Summary
Companion to opensearch-project/OpenSearch#21521 (sort/aggregation/eval-predicate scalar surface) and opensearch-project/OpenSearch#21498 (eval string concat / CAST). Two changes — both strictly scoped to PPL
fields/rename/head/sort; aggregation work (AVG/STDDEV_*/VAR_*/statssurface) is explicitly out of scope.1.
UnifiedQueryContext— defaultplugins.calcite.enabled=trueThe unified PPL parser reuses the v2
AstBuilder, which gatestable/regex/rex/converton this setting. The unified context built empty maps without it, so analytics-routedtablequeries 500'd withTable command is supported only when plugins.calcite.enabled=trueeven when the cluster setting was true.Per @dai-chen's review: the default goes in the
Map.of(...)initializer at line 123 (alongsideQUERY_SIZE_LIMIT/PPL_SUBSEARCH_MAXOUT/PPL_JOIN_SUBSEARCH_MAXOUT), not as a special case ingetSettingValue.2.
CalcitePPLRenameIT— column-name-keyed row matcherPreviously,
verifyStandardDataRowsand a handful of bespokeverifyDataRowscalls compared rows positionally — they assumed the engine emits columns in the v2 / Lucene_sourceiteration order. Under the analytics-engine route the parquet-backed reader returns columns in storage order, so the same 4 canonical state_country rows came back as[70,"USA",4,"Jake","California",2023]instead of[Jake,USA,California,4,2023,70]and 12 of 22 tests failed despite the data being identical.Replace with
verifyStandardDataRows(result, canonicalColumns...)plus a genericverifyDataRowsByColumn(result, Map<String, Object>...)helper. Both read the actual schema from the response, look up each canonical value by column name, and place it at the corresponding schema position before comparing — the test no longer leaks the engine's emission order into the assertion.Verified working on both paths: 20 / 22 pass on each. The 2 remaining failures (
testRenameInAgg,testRenameWithBackticksInAgg) are theUnable to find binding for call AVGaggregation issue — out of scope for this PR; tracked in #21521's "Out of scope" section.Test plan
./gradlew :api:checkgreenplugins.calcite.analytics.force_routing=true,POST /_plugins/_ppl {\"query\":\"source=test_eval | table name\"}no longer 500s with theplugins.calcite.enabled=trueerror:integ-test:integTestRemoteagainst an analytics cluster carrying #21521 + #21498, both with and without-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true:CalciteFieldsCommandITCalciteRenameCommandITCalciteHeadCommandITCalcitePPLRenameITCalciteEvalCommandITCalcitePPLSortITCalciteSortCommandITweblogsIP-type scan — separate follow-upCompanion routing-and-coverage workflow docs
The
docs/dev/ppl-analytics-engine-{routing,coverage-sop}.mdfiles originally bundled with the first revision of this PR were dropped per @dai-chen's review — they belong on a separate doc PR.Out of scope
Aggregation surface —
AVG,SUM,COUNT,STDDEV_*,VAR_*, thestatsfamily,count(eval(...)), etc. — is not addressed by this PR. TheUnable to find binding for call AVG($N)Substrait isthmus issue is a cross-plugin design discussion (the SQL plugin'sNullableSqlAvgAggFunctionextendsSqlAggFunctiondirectly rather thanSqlAvgAggFunction, so isthmus' built-in translator skips it). Tracked in #21521.