[analytics-engine] Wire PPL spath end-to-end through the analytics-engine route#21664
Conversation
PR Reviewer Guide 🔍(Review updated until commit 4dc07cb)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 4dc07cb Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit a5ac2f9
Suggestions up to commit 2c8bf2b
Suggestions up to commit 594f756
Suggestions up to commit ca7a567
Suggestions up to commit f25feda
|
Two complementary changes for closing PPL `spath` parity on the analytics-engine route. Pairs with opensearch-project/OpenSearch#21664 (the analytics-engine `json_extract_all` UDF + ITEM-on-MAP dispatch + MAP capability registrations). Both PRs together take `CalcitePPLSpathCommandIT` on the analytics-engine route from 0 / 16 to 16 / 16 without regressing the v2 / Calcite path (still 16 / 16). ## Pass rate | IT | Route | Before | After | |---|---|---|---| | `CalcitePPLSpathCommandIT` | analytics-engine (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | **16 / 16** | | `CalcitePPLSpathCommandIT` | default v2 / Calcite (no flags) | 16 / 16 | 16 / 16 (no regression) | The analytics-route number depends on this PR + #21664 landing together; neither PR alone moves it off 0 / 16. ## Changes 1. **`core/.../ExprValueUtils.java`** — two boundary fixes for Arrow Map values that arrive from the analytics-engine response stream: - `fromObjectValue` gains a FQN-keyed branch for `org.apache.arrow.vector.util.Text` (decoded via `toString()`). Arrow's MapVector / StructVector emit values as `Text` (a UTF-8 byte-buffer wrapper that does NOT implement `CharSequence`), which none of the typed `instanceof` branches recognized. Without this, any UDF returning `Map<Utf8, Utf8>` through the analytics-engine route surfaces as `ExpressionEvaluationException: unsupported object class org.apache.arrow.vector.util.Text`. FQN match keeps `core/` free of an Arrow dependency. - `tupleValue` widens its parameter type to `Map<?, Object>` and normalizes keys via `String.valueOf` so the downstream `LinkedHashMap<String, ExprValue>` isn't polluted with non-String keys (the JDBC response formatter then sees stable String keys). 2. **`integ-test/.../CalcitePPLSpathCommandIT.java`** — refactor `init()` to use `TestUtils.createIndexByRestClient` with an explicit keyword mapping for each of the four test indices (`test_spath`, `test_spath_auto`, `test_spath_cmd`, `test_spath_null`) before the per-doc PUTs. Without an explicit createIndex, the dynamic-mapping route bypasses the `tests.analytics.parquet_indices=true` parquet injection (because the toggle only fires inside `TestUtils.createIndexByRestClient`) and the analytics-engine fragment driver then fails with `UnsupportedOperationException: acquireReader is not supported in EngineBackedIndexer` for any test that reaches the runtime — `testSimpleSpath` was the only test affected before this PR (the other 15 failed earlier at the planner capability check). Idempotency via `TestUtils.isIndexExist` so the cluster-reuse pattern between `@Test` methods keeps working. No change for the v2 / Calcite path (the helper is a no-op for non-parquet runs). 3. **`docs/dev/spath-command-analytics-route-status.md`** — new companion to `docs/dev/ppl-analytics-engine-routing.md` documenting the full spath workstream: per-test baseline, bucket classification, the two repos' changes side-by-side, the design decisions (Map return type vs. stringified JSON, ITEM-on-MAP dispatch, FieldType.MAP capability registration), and the reproduction instructions. Signed-off-by: Kai Huang <ahkcs@amazon.com>
Two complementary changes for closing PPL `spath` parity on the analytics-engine route. Pairs with opensearch-project/OpenSearch#21664 (the analytics-engine `json_extract_all` UDF + ITEM-on-MAP dispatch + MAP capability registrations). Both PRs together take `CalcitePPLSpathCommandIT` on the analytics-engine route from 0 / 16 to 16 / 16 without regressing the v2 / Calcite path (still 16 / 16). ## Pass rate | IT | Route | Before | After | |---|---|---|---| | `CalcitePPLSpathCommandIT` | analytics-engine (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | **16 / 16** | | `CalcitePPLSpathCommandIT` | default v2 / Calcite (no flags) | 16 / 16 | 16 / 16 (no regression) | The analytics-route number depends on this PR + #21664 landing together; neither PR alone moves it off 0 / 16. ## Changes 1. **`core/.../ExprValueUtils.java`** — `fromObjectValue` gains a FQN-keyed branch for `org.apache.arrow.vector.util.Text` (decoded via `toString()`). Arrow's MapVector / StructVector emit values as `Text` (a UTF-8 byte-buffer wrapper that does NOT implement `CharSequence`), which none of the typed `instanceof` branches recognized. Without this, any UDF returning `Map<Utf8, Utf8>` through the analytics-engine route surfaces as `ExpressionEvaluationException: unsupported object class org.apache.arrow.vector.util.Text`. FQN match keeps `core/` free of an Arrow dependency. 2. **`integ-test/.../CalcitePPLSpathCommandIT.java`** — refactor `init()` to use `TestUtils.createIndexByRestClient` with an explicit keyword mapping for each of the four test indices (`test_spath`, `test_spath_auto`, `test_spath_cmd`, `test_spath_null`) before the per-doc PUTs. Without an explicit createIndex, the dynamic-mapping route bypasses the `tests.analytics.parquet_indices=true` parquet injection (because the toggle only fires inside `TestUtils.createIndexByRestClient`) and the analytics-engine fragment driver then fails with `UnsupportedOperationException: acquireReader is not supported in EngineBackedIndexer` for any test that reaches the runtime — `testSimpleSpath` was the only test affected before this PR (the other 15 failed earlier at the planner capability check). Idempotency via `TestUtils.isIndexExist` so the cluster-reuse pattern between `@Test` methods keeps working. No change for the v2 / Calcite path (the helper is a no-op for non-parquet runs). Signed-off-by: Kai Huang <ahkcs@amazon.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21664 +/- ##
============================================
- Coverage 73.45% 73.42% -0.03%
+ Complexity 74811 74808 -3
============================================
Files 5997 5997
Lines 339688 339688
Branches 48961 48961
============================================
- Hits 249515 249418 -97
- Misses 70305 70385 +80
- Partials 19868 19885 +17 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Two complementary changes for closing PPL `spath` parity on the analytics-engine route. Pairs with opensearch-project/OpenSearch#21664 (the analytics-engine `json_extract_all` UDF + ITEM-on-MAP dispatch + MAP capability registrations). Both PRs together take `CalcitePPLSpathCommandIT` on the analytics-engine route from 0 / 16 to 16 / 16 without regressing the v2 / Calcite path (still 16 / 16). ## Pass rate | IT | Route | Before | After | |---|---|---|---| | `CalcitePPLSpathCommandIT` | analytics-engine (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | **16 / 16** | | `CalcitePPLSpathCommandIT` | default v2 / Calcite (no flags) | 16 / 16 | 16 / 16 (no regression) | The analytics-route number depends on this PR + #21664 landing together; neither PR alone moves it off 0 / 16. ## Changes 1. **`core/.../ExprValueUtils.java`** — `fromObjectValue` gains a FQN-keyed branch for `org.apache.arrow.vector.util.Text` (decoded via `toString()`). Arrow's MapVector / StructVector emit values as `Text` (a UTF-8 byte-buffer wrapper that does NOT implement `CharSequence`), which none of the typed `instanceof` branches recognized. Without this, any UDF returning `Map<Utf8, Utf8>` through the analytics-engine route surfaces as `ExpressionEvaluationException: unsupported object class org.apache.arrow.vector.util.Text`. FQN match keeps `core/` free of an Arrow dependency. 2. **`integ-test/.../CalcitePPLSpathCommandIT.java`** — refactor `init()` to use `TestUtils.createIndexByRestClient` with an explicit keyword mapping for each of the four test indices (`test_spath`, `test_spath_auto`, `test_spath_cmd`, `test_spath_null`) before the per-doc PUTs. Without an explicit createIndex, the dynamic-mapping route bypasses the `tests.analytics.parquet_indices=true` parquet injection (because the toggle only fires inside `TestUtils.createIndexByRestClient`) and the analytics-engine fragment driver then fails with `UnsupportedOperationException: acquireReader is not supported in EngineBackedIndexer` for any test that reaches the runtime — `testSimpleSpath` was the only test affected before this PR (the other 15 failed earlier at the planner capability check). Idempotency via `TestUtils.isIndexExist` so the cluster-reuse pattern between `@Test` methods keeps working. No change for the v2 / Calcite path (the helper is a no-op for non-parquet runs). Signed-off-by: Kai Huang <ahkcs@amazon.com>
`CalcitePPLSpathCommandIT.init()` was creating its four test indices by raw `PUT /<idx>/_doc/N` requests, which auto-creates the index via the default Lucene path. The analytics-engine compatibility run (`-Dtests.analytics.parquet_indices=true`) injects the parquet/composite settings *inside* `TestUtils.createIndexByRestClient`, so the raw-PUT indices were Lucene-only and DataFusion failed with `UnsupportedOperationException: acquireReader is not supported in EngineBackedIndexer` for every test on the analytics-engine route. Fix: create the empty index up-front via `createIndexByRestClient(..., null)` so the toggle has a chance to inject parquet settings, then let the subsequent doc PUTs populate it via dynamic mapping. No mapping is declared — DataFusion is fine with dynamic mapping on a parquet-backed composite index. Same pattern as `CalciteEvalCommandIT` and `CalciteFieldFormatCommandIT`. No change for the v2 / Calcite path (the helper is a no-op when the parquet toggle isn't set). ## Pass rate Pairs with opensearch-project/OpenSearch#21664. Both PRs are required to move the analytics-engine route off 0 / 16. | IT | Route | Before | After | |---|---|---|---| | `CalcitePPLSpathCommandIT` | analytics-engine (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | **16 / 16** | | `CalcitePPLSpathCommandIT` | default v2 / Calcite (no flags) | 16 / 16 | 16 / 16 (no regression) | Signed-off-by: Kai Huang <ahkcs@amazon.com>
dc48574 to
d5ece62
Compare
|
Persistent review updated to latest commit d5ece62 |
d5ece62 to
f25feda
Compare
|
Persistent review updated to latest commit f25feda |
`CalcitePPLSpathCommandIT.init()` was creating its four test indices by raw `PUT /<idx>/_doc/N` requests, which auto-creates the index via the default Lucene path. The analytics-engine compatibility run (`-Dtests.analytics.parquet_indices=true`) injects the parquet/composite settings *inside* `TestUtils.createIndexByRestClient`, so the raw-PUT indices were Lucene-only and DataFusion failed with `UnsupportedOperationException: acquireReader is not supported in EngineBackedIndexer` for every test on the analytics-engine route. Fix: create the empty index up-front via `createIndexByRestClient(..., null)` so the toggle has a chance to inject parquet settings, then let the subsequent doc PUTs populate it via dynamic mapping. No mapping is declared — DataFusion is fine with dynamic mapping on a parquet-backed composite index. Same pattern as `CalciteEvalCommandIT` and `CalciteFieldFormatCommandIT`. No change for the v2 / Calcite path (the helper is a no-op when the parquet toggle isn't set). ## Pass rate Pairs with opensearch-project/OpenSearch#21664. Both PRs are required to move the analytics-engine route off 0 / 16. | IT | Route | Before | After | |---|---|---|---| | `CalcitePPLSpathCommandIT` | analytics-engine (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | **16 / 16** | | `CalcitePPLSpathCommandIT` | default v2 / Calcite (no flags) | 16 / 16 | 16 / 16 (no regression) | Signed-off-by: Kai Huang <ahkcs@amazon.com>
f25feda to
ca7a567
Compare
|
Persistent review updated to latest commit ca7a567 |
ca7a567 to
594f756
Compare
|
Persistent review updated to latest commit 594f756 |
594f756 to
2c8bf2b
Compare
|
Persistent review updated to latest commit 2c8bf2b |
2c8bf2b to
a5ac2f9
Compare
|
Done @mch2 — dropped the |
|
Persistent review updated to latest commit a5ac2f9 |
…gine route
Closes the analytics-engine gap for the PPL `spath` command. The path-mode
variant (`spath path=...`) already worked via the existing `json_extract`
wiring; this PR adds the auto-extract mode (`spath input=doc` →
`JSON_EXTRACT_ALL` returning `MAP<VARCHAR, VARCHAR>`) and its downstream
operators (ITEM lookup, WHERE on extracted values).
| IT | Before | After |
|---|---|---|
| `sql/integ-test/.../CalcitePPLSpathCommandIT` (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | 16 / 16 |
| `sql/integ-test/.../CalcitePPLSpathCommandIT` (default v2/Calcite route) | 16 / 16 | 16 / 16 (no regression) |
| `sandbox/qa/analytics-engine-rest/.../SpathCommandIT` (new) | n/a | 16 / 16 |
Baseline failure modes on the analytics-engine route:
- 15 tests: `OpenSearchProjectRule.annotateExpr` → `No backend supports
scalar function [JSON_EXTRACT_ALL] among [datafusion]`.
- 1 test (`testSimpleSpath`): `EngineBackedIndexer.acquireReader` →
`UnsupportedOperationException` (test-infra issue, fixed on the SQL
plugin side in a paired PR).
1. **`json_extract_all` Rust UDF** (`sandbox/plugins/analytics-backend-datafusion/rust/src/udf/json_extract_all.rs`).
~550 lines + 16 unit tests. Returns Arrow `Map<Utf8, Utf8>`; mirrors
`JsonExtractAllFunctionImpl`'s legacy contract (dot-path flatten, `{}`
array marker, `[a, b, c]` merge format for duplicate keys / arrays,
`"null"` literal for JSON nulls, malformed → empty map, top-level
scalar → NULL).
2. **SPI enum additions** in `analytics-framework`:
- `ScalarFunction.JSON_EXTRACT_ALL` enum constant.
- `FieldType.MAP` enum constant + `case MAP -> FieldType.MAP` in
`fromSqlTypeName`.
3. **Capability registrations** in `DataFusionAnalyticsBackendPlugin`:
- New `MAP_RETURNING_PROJECT_OPS` set (mirrors `ARRAY_RETURNING_PROJECT_OPS`)
registered with `FieldType.MAP`. Required because
`OpenSearchProjectRule.resolveScalarViableBackends` keys on the call's
return type, and JSON_EXTRACT_ALL's `MAP<VARCHAR, VARCHAR>` return
wouldn't match `SUPPORTED_FIELD_TYPES`.
- `STANDARD_FILTER_OPS` registered against `FieldType.MAP` so
`where doc.user.name = 'John'` (which references the underlying MAP
column through ITEM) survives the filter-rule's field-index-keyed
viability check.
- Adapter binding `ScalarFunction.JSON_EXTRACT_ALL → JsonExtractAllAdapter`.
4. **Substrait wiring**:
- `opensearch_scalar_functions.yaml` — entries for `json_extract_all`
and `map_extract`.
- `DataFusionFragmentConvertor.ADDITIONAL_SCALAR_SIGS` — function
mappings for both names.
- `JsonFunctionAdapters.JsonExtractAllAdapter` — name-mapping adapter.
5. **`ITEM(Map, key)` dispatch** in `ArrayElementAdapter`. PPL's
`result.user.name` lowers to `ITEM(JSON_EXTRACT_ALL(doc), 'user.name')`.
Two transforms for the MAP-input branch:
- Route to `map_extract` (DataFusion's native map accessor) instead of
`array_element`. Since `map_extract` returns `List<value>` (maps
permit duplicate keys), wrap the call in `array_element(..., 1)` to
project the singleton list back to a scalar.
- Coerce the lookup key (CHAR(N) literal) to VARCHAR before emission so
it unifies with the substrait `any1` type-variable binding the YAML
declares.
6. **`ArrowValues.MapVector` flattening** in `analytics-engine`. Arrow
`MapVector` is laid out as `List<Struct{key, value}>`, so
`MapVector.getObject(i)` returns a `JsonStringArrayList` of entry
structs rather than a proper map. Reassemble into a
`LinkedHashMap<String, Object>` (Text→String normalization on keys and
values) so the SQL-plugin response marshaller sees the same shape as a
legacy v2 `Map<String, Object>` column.
7. **QA-side `SpathCommandIT`** under
`sandbox/qa/analytics-engine-rest/...`. Mirrors `CalcitePPLSpathCommandIT`
one test method to one, sends queries via `POST /_analytics/ppl`, no
SQL-plugin dependency. Verifies the full spath surface end-to-end
(both modes, ITEM-on-MAP eval / where / stats / sort, edge cases).
Four small datasets under `resources/datasets/spath_{simple,auto,cmd,null}/`.
Every piece in this PR is reusable beyond `spath`:
- The MAP_RETURNING_PROJECT_OPS pattern + MAP filter capability are
generic for any future PPL function emitting a Calcite MAP RelDataType.
- `ArrayElementAdapter`'s ITEM-on-MAP branch + the `map_extract` YAML
entry handle every `result['key']` / `result.field` access on a map
column, not just spath's.
- `ArrowValues.MapVector` flattening unblocks any UDF returning
`Map<Utf8, Utf8>` from the analytics-engine route.
The SQL plugin side has a test-infrastructure change to ensure the v2 /
Calcite IT's test indices get parquet-backed for the analytics-engine
compatibility run: opensearch-project/sql#5441.
The `:run` cluster needs `-Dopensearch.experimental.feature.transport.stream.enabled=true`
when SQL plugin co-installs with analytics-engine (otherwise the SQL plugin's
PPL transport handler double-registers against analytics-engine's and Guice
fails at boot).
```bash
JAVA_HOME=/path/to/temurin-25 ./gradlew :run -Dsandbox.enabled=true \
-Dopensearch.experimental.feature.transport.stream.enabled=true \
-PinstalledPlugins="['opensearch-job-scheduler:3.7.0.0-SNAPSHOT', \
'arrow-flight-rpc', 'analytics-engine', 'parquet-data-format', \
'analytics-backend-datafusion', 'analytics-backend-lucene', \
'composite-engine', 'opensearch-sql-plugin:3.7.0.0-SNAPSHOT']"
./gradlew :sandbox:qa:analytics-engine-rest:integTest \
-Dsandbox.enabled=true --tests "*SpathCommandIT"
./gradlew :integ-test:integTestRemote \
-Dtests.rest.cluster=localhost:9200 \
-Dtests.cluster=localhost:9300 \
-Dtests.clustername=runTask \
-Dtests.analytics.force_routing=true \
-Dtests.analytics.parquet_indices=true \
--tests "org.opensearch.sql.calcite.remote.CalcitePPLSpathCommandIT"
```
Signed-off-by: Kai Huang <ahkcs@amazon.com>
a5ac2f9 to
4dc07cb
Compare
|
Persistent review updated to latest commit 4dc07cb |
…gine route (opensearch-project#21664) Closes the analytics-engine gap for the PPL `spath` command. The path-mode variant (`spath path=...`) already worked via the existing `json_extract` wiring; this PR adds the auto-extract mode (`spath input=doc` → `JSON_EXTRACT_ALL` returning `MAP<VARCHAR, VARCHAR>`) and its downstream operators (ITEM lookup, WHERE on extracted values). | IT | Before | After | |---|---|---| | `sql/integ-test/.../CalcitePPLSpathCommandIT` (`-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=true`) | 0 / 16 | 16 / 16 | | `sql/integ-test/.../CalcitePPLSpathCommandIT` (default v2/Calcite route) | 16 / 16 | 16 / 16 (no regression) | | `sandbox/qa/analytics-engine-rest/.../SpathCommandIT` (new) | n/a | 16 / 16 | Baseline failure modes on the analytics-engine route: - 15 tests: `OpenSearchProjectRule.annotateExpr` → `No backend supports scalar function [JSON_EXTRACT_ALL] among [datafusion]`. - 1 test (`testSimpleSpath`): `EngineBackedIndexer.acquireReader` → `UnsupportedOperationException` (test-infra issue, fixed on the SQL plugin side in a paired PR). 1. **`json_extract_all` Rust UDF** (`sandbox/plugins/analytics-backend-datafusion/rust/src/udf/json_extract_all.rs`). ~550 lines + 16 unit tests. Returns Arrow `Map<Utf8, Utf8>`; mirrors `JsonExtractAllFunctionImpl`'s legacy contract (dot-path flatten, `{}` array marker, `[a, b, c]` merge format for duplicate keys / arrays, `"null"` literal for JSON nulls, malformed → empty map, top-level scalar → NULL). 2. **SPI enum additions** in `analytics-framework`: - `ScalarFunction.JSON_EXTRACT_ALL` enum constant. - `FieldType.MAP` enum constant + `case MAP -> FieldType.MAP` in `fromSqlTypeName`. 3. **Capability registrations** in `DataFusionAnalyticsBackendPlugin`: - New `MAP_RETURNING_PROJECT_OPS` set (mirrors `ARRAY_RETURNING_PROJECT_OPS`) registered with `FieldType.MAP`. Required because `OpenSearchProjectRule.resolveScalarViableBackends` keys on the call's return type, and JSON_EXTRACT_ALL's `MAP<VARCHAR, VARCHAR>` return wouldn't match `SUPPORTED_FIELD_TYPES`. - `STANDARD_FILTER_OPS` registered against `FieldType.MAP` so `where doc.user.name = 'John'` (which references the underlying MAP column through ITEM) survives the filter-rule's field-index-keyed viability check. - Adapter binding `ScalarFunction.JSON_EXTRACT_ALL → JsonExtractAllAdapter`. 4. **Substrait wiring**: - `opensearch_scalar_functions.yaml` — entries for `json_extract_all` and `map_extract`. - `DataFusionFragmentConvertor.ADDITIONAL_SCALAR_SIGS` — function mappings for both names. - `JsonFunctionAdapters.JsonExtractAllAdapter` — name-mapping adapter. 5. **`ITEM(Map, key)` dispatch** in `ArrayElementAdapter`. PPL's `result.user.name` lowers to `ITEM(JSON_EXTRACT_ALL(doc), 'user.name')`. Two transforms for the MAP-input branch: - Route to `map_extract` (DataFusion's native map accessor) instead of `array_element`. Since `map_extract` returns `List<value>` (maps permit duplicate keys), wrap the call in `array_element(..., 1)` to project the singleton list back to a scalar. - Coerce the lookup key (CHAR(N) literal) to VARCHAR before emission so it unifies with the substrait `any1` type-variable binding the YAML declares. 6. **`ArrowValues.MapVector` flattening** in `analytics-engine`. Arrow `MapVector` is laid out as `List<Struct{key, value}>`, so `MapVector.getObject(i)` returns a `JsonStringArrayList` of entry structs rather than a proper map. Reassemble into a `LinkedHashMap<String, Object>` (Text→String normalization on keys and values) so the SQL-plugin response marshaller sees the same shape as a legacy v2 `Map<String, Object>` column. 7. **QA-side `SpathCommandIT`** under `sandbox/qa/analytics-engine-rest/...`. Mirrors `CalcitePPLSpathCommandIT` one test method to one, sends queries via `POST /_analytics/ppl`, no SQL-plugin dependency. Verifies the full spath surface end-to-end (both modes, ITEM-on-MAP eval / where / stats / sort, edge cases). Four small datasets under `resources/datasets/spath_{simple,auto,cmd,null}/`. Every piece in this PR is reusable beyond `spath`: - The MAP_RETURNING_PROJECT_OPS pattern + MAP filter capability are generic for any future PPL function emitting a Calcite MAP RelDataType. - `ArrayElementAdapter`'s ITEM-on-MAP branch + the `map_extract` YAML entry handle every `result['key']` / `result.field` access on a map column, not just spath's. - `ArrowValues.MapVector` flattening unblocks any UDF returning `Map<Utf8, Utf8>` from the analytics-engine route. The SQL plugin side has a test-infrastructure change to ensure the v2 / Calcite IT's test indices get parquet-backed for the analytics-engine compatibility run: opensearch-project/sql#5441. The `:run` cluster needs `-Dopensearch.experimental.feature.transport.stream.enabled=true` when SQL plugin co-installs with analytics-engine (otherwise the SQL plugin's PPL transport handler double-registers against analytics-engine's and Guice fails at boot). ```bash JAVA_HOME=/path/to/temurin-25 ./gradlew :run -Dsandbox.enabled=true \ -Dopensearch.experimental.feature.transport.stream.enabled=true \ -PinstalledPlugins="['opensearch-job-scheduler:3.7.0.0-SNAPSHOT', \ 'arrow-flight-rpc', 'analytics-engine', 'parquet-data-format', \ 'analytics-backend-datafusion', 'analytics-backend-lucene', \ 'composite-engine', 'opensearch-sql-plugin:3.7.0.0-SNAPSHOT']" ./gradlew :sandbox:qa:analytics-engine-rest:integTest \ -Dsandbox.enabled=true --tests "*SpathCommandIT" ./gradlew :integ-test:integTestRemote \ -Dtests.rest.cluster=localhost:9200 \ -Dtests.cluster=localhost:9300 \ -Dtests.clustername=runTask \ -Dtests.analytics.force_routing=true \ -Dtests.analytics.parquet_indices=true \ --tests "org.opensearch.sql.calcite.remote.CalcitePPLSpathCommandIT" ``` Signed-off-by: Kai Huang <ahkcs@amazon.com>
Summary
Closes the analytics-engine gap for the PPL
spathcommand. The path-mode variant (spath path=...) already worked via the existingjson_extractwiring; this PR adds the auto-extract mode (spath input=doc→JSON_EXTRACT_ALLreturningMAP<VARCHAR, VARCHAR>) and its downstream operators (ITEM lookup, WHERE on extracted values), plus a self-contained QA IT.Pass rate
sql/integ-test/.../CalcitePPLSpathCommandIT(analytics-engine route)sql/integ-test/.../CalcitePPLSpathCommandIT(default v2 / Calcite route)sandbox/qa/analytics-engine-rest/.../SpathCommandIT(new)Analytics-engine route uses
-Dtests.analytics.force_routing=true -Dtests.analytics.parquet_indices=trueon:integ-test:integTestRemote.Baseline failure modes (before this PR):
OpenSearchProjectRule.annotateExpr→IllegalStateException: No backend supports scalar function [JSON_EXTRACT_ALL] among [datafusion].testSimpleSpath):EngineBackedIndexer.acquireReader→UnsupportedOperationException— a test-infra issue, fixed on the SQL plugin side in opensearch-project/sql#5441.Changes
1.
json_extract_allRust UDFsandbox/plugins/analytics-backend-datafusion/rust/src/udf/json_extract_all.rs— ~550 lines plus 16 unit tests. Returns ArrowMap<Utf8, Utf8>; mirrorsJsonExtractAllFunctionImpl's legacy contract:user.name).{}segment (tags{}).List.toString()rendering ([a, b, c])."null".2. SPI enum additions in
analytics-frameworkScalarFunction.JSON_EXTRACT_ALLenum constant.FieldType.MAPenum constant +case MAP -> FieldType.MAPinfromSqlTypeName.3. Capability registrations in
DataFusionAnalyticsBackendPluginMAP_RETURNING_PROJECT_OPSset (mirrorsARRAY_RETURNING_PROJECT_OPS) registered withFieldType.MAP. Required becauseOpenSearchProjectRule.resolveScalarViableBackendskeys on the call's return type, and JSON_EXTRACT_ALL'sMAP<VARCHAR, VARCHAR>return wouldn't matchSUPPORTED_FIELD_TYPES.STANDARD_FILTER_OPSregistered againstFieldType.MAPsowhere doc.user.name = 'John'(which references the underlying MAP column through ITEM) survives the filter-rule's field-index-keyed viability check.ScalarFunction.JSON_EXTRACT_ALL → JsonExtractAllAdapter.4. Substrait wiring
opensearch_scalar_functions.yaml— entries forjson_extract_allandmap_extract.DataFusionFragmentConvertor.ADDITIONAL_SCALAR_SIGS— function mappings for both names.JsonFunctionAdapters.JsonExtractAllAdapter— name-mapping adapter.5.
ITEM(Map, key)dispatch inArrayElementAdapterPPL's
result.user.namelowers toITEM(JSON_EXTRACT_ALL(doc), 'user.name'). Two transforms apply for the MAP-input branch:map_extract(DataFusion's native map accessor) instead ofarray_element.map_extractreturnsList<value>because maps semantically permit duplicate keys, so we wrap the call inarray_element(..., 1)to project the singleton list back to a scalar.any1type-variable binding the YAML declares.6.
ArrowValues.MapVectorflattening inanalytics-engineArrow
MapVectoris laid out asList<Struct{key, value}>, soMapVector.getObject(i)returns aJsonStringArrayListof entry structs rather than a proper map. This change reassembles entries into aLinkedHashMap<String, Object>withText → Stringnormalization on both keys and values, so the SQL-plugin response marshaller sees the same shape as a legacy v2Map<String, Object>column.7. QA-side
SpathCommandITUnder
sandbox/qa/analytics-engine-rest/. MirrorsCalcitePPLSpathCommandITone test method to one, sends queries viaPOST /_analytics/ppl, no SQL-plugin dependency. Verifies the full spath surface end-to-end:Four small datasets under
resources/datasets/spath_{simple,auto,cmd,null}/.Knock-on coverage
Every piece in this PR is reusable beyond
spath:MAP_RETURNING_PROJECT_OPS+ MAP filter capability are generic for any future PPL function emitting a Calcite MAP RelDataType.ArrayElementAdapter's ITEM-on-MAP branch + themap_extractYAML entry handle everyresult['key']/result.fieldaccess on a map column, not just spath's.ArrowValues.MapVectorflattening unblocks any UDF returningMap<Utf8, Utf8>from the analytics-engine route.Paired SQL plugin PR
opensearch-project/sql#5441 — test-infra fix that routes the v2 / Calcite IT's test indices through
TestUtils.createIndexByRestClient, so thetests.analytics.parquet_indices=truetoggle has a chance to inject parquet/composite settings before any doc PUTs land.How to verify
QA-side IT (no SQL plugin needed):
./gradlew :sandbox:qa:analytics-engine-rest:integTest \ -Dsandbox.enabled=true --tests "*SpathCommandIT"End-to-end through the SQL plugin (requires #5441 applied):
Also addressed
Rebased onto current
main. Per @mch2's review, no gradle build files are modified by this PR — the streaming-transport feature gate needed for the analytics-engine + SQL-plugin co-install must be passed at run time as-Dopensearch.experimental.feature.transport.stream.enabled=true.