Skip to content

Default plugins.calcite.enabled=true on the unified query path#5413

Merged
ahkcs merged 4 commits intoopensearch-project:feature/mustang-ppl-integrationfrom
ahkcs:feature/mustang-unified-context-calcite-default
May 6, 2026
Merged

Default plugins.calcite.enabled=true on the unified query path#5413
ahkcs merged 4 commits intoopensearch-project:feature/mustang-ppl-integrationfrom
ahkcs:feature/mustang-unified-context-calcite-default

Conversation

@ahkcs
Copy link
Copy Markdown
Collaborator

@ahkcs ahkcs commented May 6, 2026

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_* / stats surface) is explicitly out of scope.

1. UnifiedQueryContext — default plugins.calcite.enabled=true

The unified PPL parser reuses the v2 AstBuilder, which gates table / regex / rex / convert on this setting. The unified context built empty maps without it, so analytics-routed table queries 500'd with Table command is supported only when plugins.calcite.enabled=true even when the cluster setting was true.

Per @dai-chen's review: the default goes in the Map.of(...) initializer at line 123 (alongside QUERY_SIZE_LIMIT/PPL_SUBSEARCH_MAXOUT/PPL_JOIN_SUBSEARCH_MAXOUT), not as a special case in getSettingValue.

2. CalcitePPLRenameIT — column-name-keyed row matcher

Previously, verifyStandardDataRows and a handful of bespoke verifyDataRows calls compared rows positionally — they assumed the engine emits columns in the v2 / Lucene _source iteration 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 generic verifyDataRowsByColumn(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 the Unable to find binding for call AVG aggregation issue — out of scope for this PR; tracked in #21521's "Out of scope" section.

Test plan

  • ./gradlew :api:check green
  • Manual: with plugins.calcite.analytics.force_routing=true, POST /_plugins/_ppl {\"query\":\"source=test_eval | table name\"} no longer 500s with the plugins.calcite.enabled=true error
  • :integ-test:integTestRemote against an analytics cluster carrying #21521 + #21498, both with and without -Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true:
IT Analytics path v2 / Calcite path Notes
CalciteFieldsCommandIT 39 / 39 ✅ 39 / 39 ✅ scope of this PR
CalciteRenameCommandIT 2 / 2 ✅ 2 / 2 ✅ scope of this PR
CalciteHeadCommandIT 4 / 4 ✅ (2 skipped) 4 / 4 ✅ scope of this PR
CalcitePPLRenameIT 20 / 22 20 / 22 column-order helper added by this PR; remaining 2 are AVG-binding (out of scope)
CalciteEvalCommandIT 4 / 4 ✅ 4 / 4 ✅ scope of #21498
CalcitePPLSortIT 14 / 18 18 / 18 ✅ 4 analytics-side fails on tie-break ordering / date-format diff — separate follow-up
CalciteSortCommandIT 26 / 30 30 / 30 ✅ 4 analytics-side fails on weblogs IP-type scan — separate follow-up

Companion routing-and-coverage workflow docs

The docs/dev/ppl-analytics-engine-{routing,coverage-sop}.md files 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_*, the stats family, count(eval(...)), etc. — is not addressed by this PR. The Unable to find binding for call AVG($N) Substrait isthmus issue is a cross-plugin design discussion (the SQL plugin's NullableSqlAvgAggFunction extends SqlAggFunction directly rather than SqlAvgAggFunction, so isthmus' built-in translator skips it). Tracked in #21521.

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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 4ef37bd.

PathLineSeverityDescription
api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java230mediumHard-coded override forces CALCITE_ENGINE_ENABLED to true when the key is absent from settings, silently enabling the Calcite routing path regardless of administrator configuration intent. A missing key typically means the feature is disabled or unset; returning TRUE instead bypasses that default-off contract and could route queries through a non-expected engine path without any visible configuration change.
docs/dev/ppl-analytics-engine-coverage-sop.md16mediumDocumentation directs developers to add a personal GitHub fork (ahkcs/sql) as a named remote and to base production-relevant work on ahkcs/feature/ppl-coverage-bundle. Establishing a personal fork as a sanctioned part of the official development workflow creates an unverified trust anchor — code or tooling pulled from that branch bypasses normal upstream review, making it a potential supply chain injection point.
docs/dev/ppl-analytics-engine-coverage-sop.md38lowSOP instructs developers to manually add 'runtimeOnly org.apache.commons:commons-text:1.11.0' directly to a production sandbox build file outside the normal PR/review process. While framed as a workaround pending an upstream fix, the instruction socially engineers an unreviewed dependency addition into a production artifact.
docs/dev/ppl-analytics-engine-routing.md420lowA specific commit on the personal fork (ahkcs/sql@29339c9c) is cited as the 'canonical reference for the SQL-side test infra'. Designating an unverifiable personal-fork commit as the authoritative source for infrastructure patterns encourages developers to replicate code from outside the reviewed upstream history.

The table above displays the top 10 most important findings.

Total: 4 | Critical: 0 | High: 0 | Medium: 2 | Low: 2


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

PR Reviewer Guide 🔍

(Review updated until commit 4ade92f)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Default CALCITE_ENGINE_ENABLED=true in UnifiedQueryContext

Relevant files:

  • api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java

Sub-PR theme: Column-order-agnostic rename integration test helpers

Relevant files:

  • integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLRenameIT.java

⚡ Recommended focus areas for review

Hardcoded Default

CALCITE_ENGINE_ENABLED is hardcoded to true in the UnifiedQueryContext.Builder settings map. If a caller explicitly passes false for this key (e.g., to disable Calcite for a specific unified-path request), the hardcoded default will be overridden by that caller's value — but there is no mechanism to propagate the actual cluster setting into this context. This means the unified path always behaves as if Calcite is enabled, regardless of any runtime configuration that might want to disable it. This should be validated as an intentional design decision and documented accordingly.

private final Map<Settings.Key, Object> settings =
    new HashMap<Settings.Key, Object>(
        Map.of(
            QUERY_SIZE_LIMIT, SysLimit.DEFAULT.querySizeLimit(),
            PPL_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.subsearchLimit(),
            PPL_JOIN_SUBSEARCH_MAXOUT, SysLimit.DEFAULT.joinSubsearchLimit(),
            CALCITE_ENGINE_ENABLED, true));
Unchecked Cast

In verifyStandardDataRows, new LinkedHashMap[canonicalValues.length] creates a raw array of LinkedHashMap and is assigned to Map<String, Object>[] expectedRows. This produces an unchecked cast warning and is not type-safe. Consider using a List<Map<String, Object>> instead to avoid raw array creation.

Map<String, Object>[] expectedRows = new LinkedHashMap[canonicalValues.length];
for (int i = 0; i < canonicalValues.length; i++) {
  Map<String, Object> row = new LinkedHashMap<>();
  for (int c = 0; c < canonicalColumns.length; c++) {
    row.put(canonicalColumns[c], canonicalValues[i][c]);
  }
  expectedRows[i] = row;
}
Missing Row Count Check

verifyDataRowsByColumn does not assert that the total number of rows in the response matches the number of expected rows. If the response returns extra rows, the test will still pass as long as the expected rows are present. Consider adding a check that result.getJSONArray("datarows").length() == expectedRows.length.

  JSONObject result, Map<String, Object>... expectedRows) {
JSONArray schema = result.getJSONArray("schema");
int n = schema.length();
String[] columnOrder = new String[n];
for (int i = 0; i < n; i++) {
  columnOrder[i] = schema.getJSONObject(i).getString("name");
}
@SuppressWarnings({"unchecked", "rawtypes"})
Matcher<JSONArray>[] rowMatchers = new Matcher[expectedRows.length];
for (int r = 0; r < expectedRows.length; r++) {
  Object[] reordered = new Object[n];
  for (int c = 0; c < n; c++) {
    if (!expectedRows[r].containsKey(columnOrder[c])) {
      throw new IllegalArgumentException(
          "Expected row at index "
              + r
              + " is missing canonical value for response column ["
              + columnOrder[c]
              + "]; provided keys: "
              + expectedRows[r].keySet());
    }
    reordered[c] = expectedRows[r].get(columnOrder[c]);
  }
  rowMatchers[r] = rows(reordered);
}
verifyDataRows(result, rowMatchers);

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

PR Code Suggestions ✨

Latest suggestions up to 4ade92f
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Validate key types to prevent silent runtime errors

The cast (String) pairs[i] will throw a ClassCastException at runtime if a
non-String key is accidentally passed. Since the method signature accepts Object...,
this is a silent misuse risk. Consider adding an explicit check or changing the
signature to alternate String, Object pairs.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLRenameIT.java [379-388]

 private static Map<String, Object> rowOf(Object... pairs) {
   if (pairs.length % 2 != 0) {
     throw new IllegalArgumentException("rowOf expects an even number of args (key, value, ...)");
   }
   Map<String, Object> row = new LinkedHashMap<>();
   for (int i = 0; i < pairs.length; i += 2) {
+    if (!(pairs[i] instanceof String)) {
+      throw new IllegalArgumentException("rowOf expects String keys, but got: " + pairs[i]);
+    }
     row.put((String) pairs[i], pairs[i + 1]);
   }
   return row;
 }
Suggestion importance[1-10]: 3

__

Why: The suggestion adds a runtime type check for keys in rowOf, which is a defensive improvement. However, since this is a test helper method called only with string literals in the same file, the practical risk of a non-String key being passed is very low.

Low
Suppress unchecked cast warning explicitly

The array is declared as Map<String, Object>[] but instantiated as a raw
LinkedHashMap[], which causes an unchecked cast warning and could mask type errors.
Use a consistent generic type or suppress with a comment explaining the limitation.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLRenameIT.java [423]

+@SuppressWarnings("unchecked")
 Map<String, Object>[] expectedRows = new LinkedHashMap[canonicalValues.length];
Suggestion importance[1-10]: 2

__

Why: The suggestion is valid but minor — the existing code already has @SuppressWarnings annotations nearby (lines 451-452), and this is a test utility method. The improvement is cosmetic and has minimal impact on correctness or functionality.

Low

Previous suggestions

Suggestions up to commit 4ef37bd
CategorySuggestion                                                                                                                                    Impact
General
Use equals() for key comparison safety

The current code uses reference equality (==) to compare Key enum values, which
works for enums but is fragile if Key is ever not an enum or if the comparison needs
to handle subclasses. More importantly, the settings.containsKey(key) check is done
but then settings.get(key) is called unconditionally for all other keys — if
settings can return null for a key that is present but mapped to null, the behavior
is correct, but the intent is clearer with .equals() for non-enum types. Verify that
Key is indeed an enum; if it is, the == is fine, but add a comment clarifying this
assumption.

api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java [233-236]

-if (key == Key.CALCITE_ENGINE_ENABLED && !settings.containsKey(key)) {
+if (Key.CALCITE_ENGINE_ENABLED.equals(key) && !settings.containsKey(key)) {
   return (T) Boolean.TRUE;
 }
 return (T) settings.get(key);
Suggestion importance[1-10]: 2

__

Why: The suggestion to use .equals() over == for enum comparison is generally unnecessary since Java enums are singletons and == is the idiomatic and correct way to compare them. The suggestion itself acknowledges this (if it is, the == is fine), making the change purely cosmetic with no real correctness benefit.

Low

// {@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)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

ahkcs added a commit to ahkcs/OpenSearch that referenced this pull request May 6, 2026
…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>
ahkcs added 2 commits May 6, 2026 14:30
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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 1d4c113

@ahkcs ahkcs added the enhancement New feature or request label May 6, 2026
@ahkcs ahkcs requested a review from dai-chen May 6, 2026 21:37
ahkcs added a commit to ahkcs/OpenSearch that referenced this pull request May 6, 2026
…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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Persistent review updated to latest commit 4ade92f

@ahkcs ahkcs merged commit 59c728b into opensearch-project:feature/mustang-ppl-integration May 6, 2026
31 of 38 checks passed
mch2 pushed a commit to opensearch-project/OpenSearch that referenced this pull request May 6, 2026
…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>
ahkcs added a commit to ahkcs/OpenSearch that referenced this pull request May 6, 2026
`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>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request May 7, 2026
…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>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request May 7, 2026
…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>
mch2 pushed a commit to opensearch-project/OpenSearch that referenced this pull request May 7, 2026
…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>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request May 7, 2026
…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>
ahkcs pushed a commit that referenced this pull request May 7, 2026
…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>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request May 7, 2026
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>
ahkcs added a commit to ahkcs/sql that referenced this pull request May 8, 2026
…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>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request May 8, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants