Add 3 different types of PPL scalar functions to analytics-engine - prove wiring based on DataFusion capabilities - #21476
Conversation
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit e65e5c2.
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. |
4841317 to
ef9a714
Compare
…nt types of scalar functions and supporting them with backend-datafusion. 1. year(ts) - rewrite to date_part using an adapter. 2. convert_tz - rewrites to rust side UDF 3. unix_timestamp - rewrite to DataFusion native to_unixtime(ts) Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
f296c2f to
e65e5c2
Compare
|
❌ Gradle check result for e65e5c2: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21476 +/- ##
============================================
+ Coverage 73.38% 73.46% +0.08%
- Complexity 74380 74440 +60
============================================
Files 5970 5970
Lines 338267 338267
Branches 48753 48753
============================================
+ Hits 248228 248517 +289
+ Misses 70237 69922 -315
- Partials 19802 19828 +26 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
db24ab0 to
7b1cbd1
Compare
Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
6a800a7 to
d0e9ea6
Compare
|
GC stalled , but passed on prior run - no changes outside of sandbox and sandbox checked passes...merging this. |
…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>
Group G math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with opensearch-project#21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from opensearch-project#21476. SCALAR_MAX / SCALAR_MIN declare SqlTypeName.ANY as their Calcite return type (polymorphic operand shape). AbstractNameMappingAdapter previously preserved original.getType() unconditionally — which made substrait serialisation fail with "Unable to convert the type ANY" for these two. Extended the base in analytics-framework/spi: when the original call's type is ANY, fall back to rexBuilder.makeCall(targetOperator, operands) with no explicit type so Calcite's target-operator inference derives a concrete fp64/int64/etc. return type from operands. DOUBLE / INTEGER / etc. return types continue to propagate through the preservation path for opensearch-project#21476's callers. RewriteOperatorAdapter.java and RewriteOperatorAdapterTests.java deleted; javadoc references in SignumFunction, MathScalarFunctionsIT, and ScalarFunctionTests updated accordingly. Unit coverage for the new ANY fallback lives alongside the base class in AbstractNameMappingAdapterTests. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group G math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with opensearch-project#21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from opensearch-project#21476. SCALAR_MAX / SCALAR_MIN declare SqlTypeName.ANY as their Calcite return type (polymorphic operand shape). AbstractNameMappingAdapter previously preserved original.getType() unconditionally — which made substrait serialisation fail with "Unable to convert the type ANY" for these two. Extended the base in analytics-framework/spi: when the original call's type is ANY, fall back to rexBuilder.makeCall(targetOperator, operands) with no explicit type so Calcite's target-operator inference derives a concrete fp64/int64/etc. return type from operands. DOUBLE / INTEGER / etc. return types continue to propagate through the preservation path for opensearch-project#21476's callers. RewriteOperatorAdapter.java and RewriteOperatorAdapterTests.java deleted; javadoc references in SignumFunction, MathScalarFunctionsIT, and ScalarFunctionTests updated accordingly. Unit coverage for the new ANY fallback lives alongside the base class in AbstractNameMappingAdapterTests. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with opensearch-project#21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from opensearch-project#21476. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the second PPL json_* UDF on top of opensearch-project#21476 (json_array_length). Matches the legacy SQL-plugin contract: object → JSON-array-encoded keys in insertion order; non-object / malformed / scalar → SQL NULL. - Rust UDF at rust/src/udf/json_keys.rs with scalar + columnar paths - Shared rust/src/udf/json_common.rs helpers (parse, arity, Utf8 downcast, PPL-path → JSONPath) seeded for later json_* UDFs - serde_json preserve_order feature to preserve legacy LinkedHashMap ordering - Java wiring: ScalarFunction.JSON_KEYS, JsonKeysAdapter, Substrait sig, YAML signature, plugin project-op + adapter registration - ScalarJsonFunctionIT parity test for the four legacy fixtures Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
Adds the second PPL json_* UDF on top of opensearch-project#21476 (json_array_length). Matches the legacy SQL-plugin contract: object → JSON-array-encoded keys in insertion order; non-object / malformed / scalar → SQL NULL. - Rust UDF at rust/src/udf/json_keys.rs with scalar + columnar paths - Shared rust/src/udf/json_common.rs helpers (parse, arity, Utf8 downcast, PPL-path → JSONPath) seeded for later json_* UDFs - serde_json preserve_order feature to preserve legacy LinkedHashMap ordering - Java wiring: ScalarFunction.JSON_KEYS, JsonKeysAdapter, Substrait sig, YAML signature, plugin project-op + adapter registration - ScalarJsonFunctionIT parity test for the four legacy fixtures Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
## Why an adapter extension is necessary
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
opensearch-project#21527, is extended here to recognize 3 OR 4 operands. Pattern is at position 1
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
## Out of scope (deferred to Part 2)
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
## Test results
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
## Companion PR
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
## Why an adapter extension is necessary
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
opensearch-project#21527, is extended here to recognize 3 OR 4 operands. Pattern is at position 1
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
## Out of scope (deferred to Part 2)
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
## Test results
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
## Companion PR
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
* Adding sweep of MATH scalar functions. math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with #21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from #21476. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix compile Signed-off-by: Marc Handalian <marc.handalian@gmail.com> --------- Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…rove wiring based on DataFusion capabilities (opensearch-project#21476) * This PR adds three functions to illustrate implementation for different types of scalar functions and supporting them with backend-datafusion. 1. year(ts) - rewrite to date_part using an adapter. 2. convert_tz - rewrites to rust side UDF 3. unix_timestamp - rewrite to DataFusion native to_unixtime(ts) Signed-off-by: Marc Handalian <marc.handalian@gmail.com> * Consolidate and clean up scalar_functions yaml. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> * add missing udf registration sites Signed-off-by: Marc Handalian <marc.handalian@gmail.com> --------- Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
…elds/rename/head/sort (opensearch-project#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 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>
…h-project#21520) * Adding sweep of MATH scalar functions. math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with opensearch-project#21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from opensearch-project#21476. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix compile Signed-off-by: Marc Handalian <marc.handalian@gmail.com> --------- Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rove wiring based on DataFusion capabilities (opensearch-project#21476) * This PR adds three functions to illustrate implementation for different types of scalar functions and supporting them with backend-datafusion. 1. year(ts) - rewrite to date_part using an adapter. 2. convert_tz - rewrites to rust side UDF 3. unix_timestamp - rewrite to DataFusion native to_unixtime(ts) Signed-off-by: Marc Handalian <marc.handalian@gmail.com> * Consolidate and clean up scalar_functions yaml. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> * add missing udf registration sites Signed-off-by: Marc Handalian <marc.handalian@gmail.com> --------- Signed-off-by: Marc Handalian <marc.handalian@gmail.com>
…elds/rename/head/sort (opensearch-project#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 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>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Adds the second PPL json_* UDF on top of opensearch-project#21476 (json_array_length). Matches the legacy SQL-plugin contract: object → JSON-array-encoded keys in insertion order; non-object / malformed / scalar → SQL NULL. - Rust UDF at rust/src/udf/json_keys.rs with scalar + columnar paths - Shared rust/src/udf/json_common.rs helpers (parse, arity, Utf8 downcast, PPL-path → JSONPath) seeded for later json_* UDFs - serde_json preserve_order feature to preserve legacy LinkedHashMap ordering - Java wiring: ScalarFunction.JSON_KEYS, JsonKeysAdapter, Substrait sig, YAML signature, plugin project-op + adapter registration - ScalarJsonFunctionIT parity test for the four legacy fixtures Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_array_length to DataFusion backend
First PPL json_* function wired through PPL → Calcite → Substrait →
DataFusion. Scaffolds the pattern every follow-up UDF reuses: Rust kernel
+ YAML signature + ScalarFunction enum entry + JsonFunctionAdapters
rename + FunctionMappings.s(...) binding + STANDARD_PROJECT_OPS entry.
Rust UDF (rust/src/udf/json_array_length.rs) coerces the input to Utf8,
parses with serde_json, and returns Int32 to match PPL's
INTEGER_FORCE_NULLABLE declaration — returning Int64 would leak through
column-valued calls even though literal args const-fold via a narrowing
CAST. Malformed / non-array / NULL input → NULL, matching legacy
JsonArrayLengthFunctionImpl's NullPolicy.ANY + Gson parity.
ScalarFunction.CAST added to STANDARD_PROJECT_OPS so PPL's implicit CAST
around a UDF call (inserted when the UDF's declared return type differs
from the eval column's inferred type) doesn't fail OpenSearchProjectRule
with "No backend supports scalar function [CAST]". DataFusion handles
CAST natively — no UDF needed.
STANDARD_PROJECT_OPS and scalarFunctionAdapters reshaped to one-entry-
per-line (Map.ofEntries / Set.of) so parallel json_* PRs append without
touching neighbour lines.
Tests:
* 10 Rust unit tests (flat/nested arrays, non-array, malformed, NULL,
coerce_types accept/reject, arity guard, scalar-input fast path).
* JsonFunctionAdaptersTests guards adapter shape + return-type
preservation (BIGINT vs LOCAL_OP's INTEGER_NULLABLE).
* ScalarJsonFunctionIT covers happy path, empty array, non-array
object → NULL, malformed → NULL via /_analytics/ppl.
Parity-checked against legacy SQL plugin
CalcitePPLJsonBuiltinFunctionIT.testJsonArrayLength.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] JSON: introduce jsonpath-rust parser + shared helpers
Lands the parser crate + a small shared helpers module ahead of the per-
function json_* UDFs. Keeping this on its own commit lets reviewers sign
off on the crate choice (jsonpath-rust 0.7) and path-conversion behaviour
before 8 UDF bodies land on top.
* rust/Cargo.toml: add jsonpath-rust = "0.7".
* rust/src/udf/json_common.rs:
- convert_ppl_path: PPL path syntax (`a{i}.b{}`) -> JSONPath (`$.a[i].b[*]`).
Mirrors JsonUtils.convertToJsonPath in sql/core. Empty string maps
to "$" to match legacy root semantics.
- parse: serde_json wrapper returning None on malformed input, the
contract every json_* UDF will share.
- check_arity / check_arity_range: plan_err! wrappers for the
top-of-invoke guards.
* rust/src/udf/mod.rs: register the module (helpers are crate-private).
Consumers land in follow-up commits on the same PR (#21513); a module-
level #![allow(dead_code)] keeps this commit's cargo check clean.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_keys to DataFusion backend
Adds the second PPL json_* UDF on top of #21476 (json_array_length).
Matches the legacy SQL-plugin contract: object → JSON-array-encoded keys
in insertion order; non-object / malformed / scalar → SQL NULL.
- Rust UDF at rust/src/udf/json_keys.rs with scalar + columnar paths
- Shared rust/src/udf/json_common.rs helpers (parse, arity, Utf8 downcast,
PPL-path → JSONPath) seeded for later json_* UDFs
- serde_json preserve_order feature to preserve legacy LinkedHashMap ordering
- Java wiring: ScalarFunction.JSON_KEYS, JsonKeysAdapter, Substrait sig,
YAML signature, plugin project-op + adapter registration
- ScalarJsonFunctionIT parity test for the four legacy fixtures
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_extract to DataFusion backend
Rust UDF at rust/src/udf/json_extract.rs wraps jsonpath-rust: single path →
unquoted scalar or JSON-serialized container; multi-path → JSON array with
literal null slots for misses. < 2 args, malformed doc, malformed path, and
explicit-null matches all collapse to SQL NULL, matching legacy
JsonExtractFunctionImpl's calcite jsonQuery/jsonValue pair.
JsonExtractAdapter renames the PPL call to the Rust UDF name via the variadic
path; routing lives in FunctionMappings.s(...) in DataFusionFragmentConvertor
and the STANDARD_PROJECT_OPS allow-list.
Also fixes a pre-existing transport bug in DatafusionResultStream.getFieldValue:
VarCharVector.getObject returns Arrow Text, which StreamOutput.writeGenericValue
cannot serialize, so string-valued UDF results (json_keys, json_extract) were
dropped when shard results traveled back to the coordinator. Converting
VarCharVector cells to String at the source mirrors ArrowValues.toJavaValue
and unblocks every string-returning UDF.
Parity IT (ScalarJsonFunctionIT) replays four verbatim legacy cases covering
single-path scalar/container match, wildcard multi-match, multi-path with
missing path, and explicit-null resolution.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_delete to DataFusion backend
Mutation UDF #1. Introduces the shared mutation walker that json_set,
json_append, and json_extend will reuse on the same PR.
Rust side (rust/src/udf/json_delete.rs + json_common.rs):
* `parse_ppl_segments` tokenises PPL paths (a.b{0}.c{}) into Field /
Index / Wildcard segments without allocating field names.
* `walk_mut` drives a mutation closure against every terminal match in
a serde_json::Value; missing intermediate keys and out-of-range
indices are silent no-ops, matching Jayway's SUPPRESS_EXCEPTIONS
behaviour that legacy `JsonDeleteFunctionImpl` (→ Calcite
`JsonFunctions.jsonRemove`) relies on.
* `json_delete` terminal closure: `shift_remove` on Object (preserves
insertion order via serde_json's `preserve_order` feature),
`Vec::remove` on Array-with-Index, `Vec::clear` on Array-with-Wildcard.
Any-NULL-arg / malformed doc / malformed path → NULL.
The walker is generic enough that json_set / json_append / json_extend
are now pure terminal-closure swaps (set value, push value, extend
array) — no further traversal plumbing needed.
Java side:
* JSON_DELETE added to `ScalarFunction`, `STANDARD_PROJECT_OPS`, and
`scalarFunctionAdapters`.
* `JsonDeleteAdapter` is a plain `AbstractNameMappingAdapter` rename
(matches the other json_* adapters).
* Substrait YAML signature uses `variadic: {min: 1}` — same shape as
json_extract.
Tests:
* 10 Rust unit tests for json_delete (4 legacy IT fixtures replayed:
flat-key, nested, missing-path-unchanged, wildcard-array; plus
any-NULL / malformed / coerce_types / return_type).
* 4 new walker tests in json_common (tokeniser, flat-delete,
missing-noop, wildcard-fan-out, index-out-of-range-noop).
* ScalarJsonFunctionIT gains `testJsonDeleteParityWithLegacy`
replaying all 4 legacy assertions.
Parity-checked against legacy SQL plugin
`CalcitePPLJsonBuiltinFunctionIT.testJsonDelete*`.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_set to DataFusion backend
Mutation UDF #2. Reuses the walker introduced by #json_delete; this
commit is a pure terminal-closure swap on the Rust side (replace, not
remove) plus the usual 7-file Java/YAML wiring.
Rust side (rust/src/udf/json_set.rs):
* Terminal closure overwrites only existing keys on Object
(`map.contains_key` guard), in-range slots on Array-with-Index, and
every element on Array-with-Wildcard. This is the replace-only
semantics from legacy `JsonSetFunctionImpl` (→ Calcite
`JsonFunctions.jsonSet`, which guards `ctx.set` with
`ctx.read(k) != null`).
* Variadic arity: (doc, path1, val1, [path2, val2, ...]). Fewer than
3 args or an odd total (unpaired trailing path) short-circuits to
NULL, mirroring the "malformed input → NULL" convention the other
json_* UDFs follow.
* Values are always stored as `Value::String` because every arg is
coerced to Utf8 by `coerce_types` — matches the legacy fixture's
`"b":"3"` (stringified, not numeric).
* Root-path (`parse_ppl_segments` returns empty) is a no-op to match
Jayway's behaviour: `ctx.set("$", v)` silently fails because the
root is indelible and unreplaceable.
Java side:
* JSON_SET added to `ScalarFunction`, `STANDARD_PROJECT_OPS`, and
`scalarFunctionAdapters`.
* `JsonSetAdapter` is a plain `AbstractNameMappingAdapter` rename.
* Substrait YAML signature uses `variadic: {min: 1}` — same shape as
json_extract / json_delete.
Tests:
* 9 Rust unit tests for json_set (3 legacy IT fixtures replayed:
wildcard-replace, wrong-path-unchanged, partial-wildcard-set; plus
multi-pair / any-NULL / malformed-doc / malformed-path /
coerce_types / return_type).
* ScalarJsonFunctionIT gains `testJsonSetParityWithLegacy` replaying
all 3 legacy assertions.
Parity-checked against legacy SQL plugin
`CalcitePPLJsonBuiltinFunctionIT.testJsonSet*`.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_append to DataFusion backend
Mutation UDF #3. Another walker reuse: terminal closure pushes the
paired value onto array-valued targets (non-array / missing targets
are silent no-ops).
Rust side (rust/src/udf/json_append.rs):
* Terminal closure branches: Object+Field → look up field, if it's an
Array push the stringified value; Array+Index → if the indexed slot
is an Array, push; Array+Wildcard → push onto every array-valued
child. Non-array matches are skipped, matching legacy
`JsonFunctions.jsonInsert` via Jayway's Collection-parent branch
(`Collection.add`) which is how `JsonAppendFunctionImpl`'s
`.meaningless_key` suffix trick ultimately expands.
* Variadic arity (doc, path1, val1, [path2, val2, ...]). Fewer than 3
args or an odd total (unpaired trailing path) → NULL — the
malformed-input-to-NULL convention all other json_* UDFs share.
Matches legacy's `RuntimeException("needs corresponding path and
values")` observably-as-error via NULL surface.
* Pre-stringified values: all args are Utf8-coerced at `coerce_types`
entry, so nested `json_object(...)` / `json_array(...)` arrive here
already stringified. They are pushed as `Value::String`, which
reproduces the legacy IT's quoted-JSON-as-element rows without the
new engine having to implement `json_object`/`json_array` yet
(they ship in a follow-up PR).
Java side:
* JSON_APPEND added to `ScalarFunction`, `STANDARD_PROJECT_OPS`, and
`scalarFunctionAdapters`.
* `JsonAppendAdapter` is a plain `AbstractNameMappingAdapter` rename.
* Substrait YAML signature uses `variadic: {min: 1}` — same shape as
json_extract / json_delete / json_set.
Tests:
* 12 Rust unit tests for json_append (3 legacy IT fixtures replayed
with pre-stringified nested JSON: named-array push, nested-path
push, stringified-object push; plus multi-pair / wildcard-fan-out /
non-array-noop / missing-path-noop / any-NULL / malformed-doc /
malformed-path / coerce_types / return_type).
* ScalarJsonFunctionIT gains `testJsonAppendParityWithLegacy`
replaying all 3 legacy assertions with literal stringified JSON in
place of the nested constructor calls the legacy test uses.
Parity-checked against legacy SQL plugin
`CalcitePPLJsonBuiltinFunctionIT.testJsonAppend`.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
* [Analytics Engine] Port json_extend to DataFusion backend
Mutation UDF #4 — last walker reuse. Same push shape as json_append,
but each paired value is first tried as a JSON-array parse: success →
spread the elements; failure → push the whole string as one element
(parity with legacy `JsonExtendFunctionImpl`'s `gson.fromJson(v,
List.class)` try/fall-back).
Rust side (rust/src/udf/json_extend.rs):
* Helper `spread(raw) -> Vec<Value>`: returns the parsed items when
`raw` is a JSON array, else `[Value::String(raw)]`. Scalars,
objects, and malformed JSON all go through the single-push branch.
* Terminal closure reuses json_append's array-target guards (Object
field → Array, Array+Index → inner Array, Array+Wildcard → every
array child). `Vec::extend(items.iter().cloned())` handles the
spread and the single-push case uniformly.
* Variadic arity matches every other mutation UDF. Invalid arity /
any-NULL / malformed-doc / malformed-path → NULL.
Deliberate divergence from legacy: integer-typed spread elements stay
integers (serde_json preserves source type) rather than being widened
to Double as Gson does. Documented in `json.md:555` but not covered by
any legacy IT; we preserve the more useful default and will file a
tracking issue for the wider Gson-compat decision.
Java side:
* JSON_EXTEND added to `ScalarFunction`, `STANDARD_PROJECT_OPS`, and
`scalarFunctionAdapters`.
* `JsonExtendAdapter` is a plain `AbstractNameMappingAdapter` rename.
* Substrait YAML signature uses `variadic: {min: 1}` — same shape as
the other variadic json_* UDFs.
Tests:
* 13 Rust unit tests for json_extend (3 legacy IT fixtures replayed:
single-push on non-array value, plain-string push, JSON-array
spread; plus empty-array-value / mixed-type-spread / wildcard-fan
/ non-array-noop / missing-path-noop / any-NULL / malformed-doc /
malformed-path / coerce_types / return_type).
* ScalarJsonFunctionIT gains `testJsonExtendParityWithLegacy`
replaying all 3 legacy assertions with literal stringified JSON
standing in for the nested constructor calls the legacy test uses.
Parity-checked against legacy SQL plugin
`CalcitePPLJsonBuiltinFunctionIT.testJsonExtend`.
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
---------
Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…h-project#21520) * Adding sweep of MATH scalar functions. math scalar functions — 32 ITs covering ABS, ACOS, ASIN, ATAN, ATAN2, CBRT, CEIL, COS, COSH, COT, DEGREES, E, EXP, EXPM1, FLOOR, LN, LOG, LOG10, LOG2, PI, POWER, RADIANS, RAND, ROUND, SCALAR_MAX, SCALAR_MIN, SIGN, SIN, SINH, TAN, TRUNCATE — pushed down through analytics-engine → Substrait → DataFusion. Rebased on upstream/main with opensearch-project#21476 landed (AbstractNameMappingAdapter + YEAR / CONVERT_TZ / UNIX_TIMESTAMP). Conflicts resolved by union in: - ScalarFunction.java (math enum entries) - DataFusionAnalyticsBackendPlugin.java (STANDARD_PROJECT_OPS + scalarFunctionAdapters map) - DataFusionFragmentConvertor.java (ADDITIONAL_SCALAR_SIGS) - opensearch_scalar_functions.yaml (cbrt / cot / pi / random / round / signum / trunc signatures) Migrated SCALAR_MAX / SCALAR_MIN / SIGN off the local RewriteOperatorAdapter onto the shared AbstractNameMappingAdapter from opensearch-project#21476. Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix compile Signed-off-by: Marc Handalian <marc.handalian@gmail.com> --------- Signed-off-by: Marc Handalian <marc.handalian@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
…dge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from opensearch-project#21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (opensearch-project#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from opensearch-project#21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume opensearch-project#5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
* [Analytics Backend / DataFusion] Wire PPL rex sed-mode (Part 1) — bridge-only
Onboards the PPL `rex` command's `mode=sed` surface — the part that lowers to
standard Calcite library operators and bridges through Substrait to DataFusion's
native UDFs. Three sed sub-variants covered:
* `rex field=f mode=sed "s/old/new/"` (no flags) → SqlLibraryOperators.REGEXP_REPLACE_3
(already mapped via the PPL `replace` onboarding from #21527 — no-op here).
* `rex field=f mode=sed "s/old/new/g"` / `/i` / `/gi` → SqlLibraryOperators.REGEXP_REPLACE_PG_4
(4-arg with flags string). New bridge in this PR. DataFusion's regexp_replace
natively accepts 4-arg `(str, pat, repl, flags)` per its substrait UDF binding.
* `rex field=f mode=sed "y/from/to/"` (transliteration) → SqlLibraryOperators.TRANSLATE3.
New bridge in this PR. Resolves to DataFusion's `translate` UDF
(datafusion-functions/src/unicode/translate.rs).
The 4-arg `REGEXP_REPLACE_PG_4` carries the same Java-regex syntax baggage as the
3-arg form: `\Q…\E` quoted-literal blocks (Rust regex rejects them) and bare `$N`
backreferences in the replacement (Rust's identifier-greedy parser
mis-resolves them). RegexpReplaceAdapter, introduced for the 3-arg form in
and replacement at position 2 in both signatures — the rewrite logic doesn't
change. Operands beyond position 2 (the flags string in the 4-arg form) pass
through verbatim. Two new RegexpReplaceAdapterTests cover the 4-arg path.
`TRANSLATE3` doesn't need an adapter — its arguments are character classes, not
regex syntax.
* Rex extract mode (`rex field=f "(?<g>...)"`) — uses the SQL plugin's custom
Java UDFs `REX_EXTRACT`, `REX_EXTRACT_MULTI`, `REX_OFFSET`, which have no
native DataFusion equivalent. Slated for a follow-up PR that adds Rust-side
UDF implementations, similar to the convert_tz precedent (#21476).
* Sed with occurrence flag (`s/.../.../<N>`) — emits 5-arg
`REGEXP_REPLACE_5`, which DataFusion's native `regexp_replace` does not
support (max 4 args). Also Part 2.
* `RegexpReplaceAdapterTests` — 21/21 (19 from #21527 + 2 new for the 4-arg path).
* `RexCommandIT` (new self-contained QA IT, calcs dataset) — 9/9. Covers all sed
sub-variants: literal (no flags), `/g` global, `/i` case-insensitive, `/gi`
combined, backreferences via `$N`, transliteration `y/from/to/` and
no-match passthrough.
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green.
The unified-path NPE caused by a missing PPL_REX_MAX_MATCH_LIMIT default is fixed
in opensearch-project/sql#5418 — required for any rex query (sed or extract) to
reach the planner via /_analytics/ppl. This PR's Test results assume #5418 is
applied. Pre-fix: every query NPEs in `AstBuilder.visitRexCommand`. Post-fix:
9/9 RexCommandIT pass.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
* [Analytics Backend / DataFusion] Wire PPL rex extract-mode (Part 2) — Rust UDFs + array result type
Completes the PPL `rex` onboarding started in Part 1 (#21550). The sed-mode forms
were already covered by bridges to existing Calcite/DataFusion operators. The
extract-mode form has no native DataFusion equivalent and needs three custom
Rust UDFs, three Java SqlOperator adapters, and a small handful of analytics-
framework / engine plumbing changes to model array result types end-to-end.
* `rex_extract(input, pattern_lit, group_lit) -> varchar` — single named or
numbered group capture. Compiles the regex once at plan time, runs per row.
* `rex_extract_multi(input, pattern_lit, group_lit, max_match) -> list<varchar>`
— multi-match. `max_match=0` means unbounded; otherwise caps the result at
the requested element count. Returns NULL (not an empty list) when there
are no matches, matching the SQL plugin's Java implementation.
* `rex_offset(input, pattern_lit) -> varchar` — emits the named-group offsets
formatted as `"name1=s1-e1&name2=s2-e2"`, alphabetically sorted; end is
inclusive, matching the SQL plugin's `RexOffsetFunction.end - 1` convention.
Each UDF has 5 unit tests covering the contract above.
* `RexExtractAdapter`, `RexExtractMultiAdapter`, `RexOffsetAdapter` — keyed on
the SQL plugin's PPL builtin operator names (`REX_EXTRACT`,
`REX_EXTRACT_MULTI`, `REX_OFFSET`) via the analytics-framework
ScalarFunction enum. Each adapter rewrites the incoming RexCall to a local
target SqlOperator (`LOCAL_REX_EXTRACT_OP`, etc.) that
`DataFusionFragmentConvertor`'s `ADDITIONAL_SCALAR_SIGS` maps to the
corresponding `rex_extract` / `rex_extract_multi` / `rex_offset` Substrait
extension declared in `opensearch_scalar_functions.yaml`.
* Pattern operands (and the group operand for the extract variants) are
validated as RexLiterals at plan time. Column-valued patterns would force
per-row regex compilation on the Rust side and are rejected with an
IllegalArgumentException — same contract as the precedent set by
RegexpReplaceAdapter in Part 1. `RexExtractAdapterTests` covers this.
* `FieldType.ARRAY` enum value + `fromSqlTypeName(ARRAY) -> FieldType.ARRAY`
in analytics-framework. Without this, `OpenSearchProjectRule.resolveScalar
ViableBackends` returns `null` for any scalar with an array return type
and the planner emits "No backend supports scalar function [REX_EXTRACT_
MULTI] among [datafusion]". `REX_EXTRACT_MULTI`'s ProjectCapability.Scalar
declaration is now `Set.of(FieldType.ARRAY)` rather than the broad scalar
set used by every other op (UPPER, ABS, ...) — those genuinely don't return
arrays.
* `ListVector` handling in three call sites that previously triggered Arrow's
`JsonStringArrayList.<clinit>`, which references `JavaTimeModule` from
`jackson-datatype-jsr310` (not on the `arrow-flight-rpc` parent plugin's
classloader). Bypassing `getObject()` and reading offset buffer + inner
data vector directly:
- `DatafusionResultStream.getFieldValue` (shard-side row materialization)
- `ArrowValues.toJavaValue` (coordinator post-execution row reading)
- `RowResponseCodec` (`inferArrowField` + `setVectorValue`) — the
Object[]-row → Arrow VectorSchemaRoot wire codec needed an explicit
list<utf8> Field with proper child Field, plus a ListVector setter
using `startNewValue`/`endValue` + the inner VarCharVector's `setSafe`.
* `RexCommandIT` extended from 9 sed tests to 16 — adds 7 extract-mode cases:
single named group, multiple named groups in one row, missing-group
NULL handling, multi-match capturing all, `max_match` cap, offset_field
output, and no-match passthrough as NULL.
* Rust UDFs — 15/15 unit tests (5 per UDF).
* `RexExtractAdapterTests` — 4/4.
* `RexCommandIT` — 16/16 (9 sed from Part 1 + 7 new extract).
* `./gradlew check -p sandbox -Dsandbox.enabled=true` — green (678 tasks,
all sandbox module unit tests + spotless + license + forbidden API).
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
* [Analytics Backend / DataFusion] Onboard array_length scalar function (Part 3)
Wires Calcite's `SqlLibraryOperators.ARRAY_LENGTH` to DataFusion's native
`array_length`, completing the end-to-end story for PPL `rex` extract-mode
multi-match: queries can now size the list returned by `rex_extract_multi`
(`eval count = array_length(g)`).
* `ScalarFunction.ARRAY_LENGTH` enum value (resolves via the `valueOf()`
fallback on the Calcite operator name).
* Registered in `STANDARD_PROJECT_OPS`. Returns `bigint`, so the existing
`SUPPORTED_FIELD_TYPES` (numeric ∪ keyword ∪ date ∪ {BOOLEAN, TEXT})
covers the capability lookup — no special-case needed.
* `FunctionMappings.s(SqlLibraryOperators.ARRAY_LENGTH, "array_length")` in
`DataFusionFragmentConvertor.ADDITIONAL_SCALAR_SIGS`. Library operators
don't auto-resolve through the substrait default catalog — the same
explicit pinning pattern used for `ILIKE`, `DATE_PART`, and the
`REGEXP_REPLACE_*` family.
* `array_length` extension declaration in `opensearch_scalar_functions.yaml`
with `list<varchar<L1>>` → `i64` and `list<string>` → `i64` impls. Without
a custom YAML extension that matches the actual list type, isthmus emits
"Unable to convert call ARRAY_LENGTH(list<varchar<...>>)" for the
`rex_extract_multi` output.
Lifts CalciteRexCommandIT (SQL plugin's standard rex IT class) through the
analytics-engine route from 14/18 → 17/18. The remaining failure
(testRexMaxMatchConfigurableLimit) is a unified-query architectural gap —
`UnifiedQueryContext` ignores cluster-setting overrides and uses the static
default — unrelated to rex or array_length.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
* Collapse array_length impls to single list<any1>
CI surfaced this on the post-rebase rex run:
Duplicate key FunctionAnchor{urn=extension:org.opensearch:scalar_functions,
key=array_length:list} (attempted merging values
array_length:list and array_length:list)
The Part 3 commit declared two impls — `list<varchar<L1>>` and `list<string>`
— with the intent of covering both element-type families produced by
`rex_extract_multi`'s pair of impls. But substrait's compound function key
drops the inner parametric element type at the key level, so both impls
collapse to the same key `array_length:list`. The YAML loader rejects the
collision when the analytics-backend-datafusion plugin's
`SimpleExtension.ExtensionCollection` merges the file in.
Replace the two impls with a single `list<any1>` polymorphic impl. The
`any1` type variable matches any element type at planning, so a call site
that produces `list<varchar<L1>>` (rex_extract_multi varchar overload) and
a call site that produces `list<string>` (rex_extract_multi string
overload) both bind to the one impl. Net effect on planning is equivalent
and the duplicate-key collision goes away.
The duplicate didn't surface on the original rex CI run because the prior
PPL_REX_MAX_MATCH_LIMIT NPE failed every query at plan time before the
function-extension merge was reached. Once the mavenLocal pin fix landed
the prior commit and queries actually reached the planner, this older
latent collision was unmasked.
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
---------
Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Description
Ships the analytics-engine's PPL-function wiring foundation on top of PR #21436. Demonstrates one function per decision-tree category so follow-up PRs have a reference for every pattern.
This PR demonstrates 3 different ways to add functions, based on where each function's semantics live and whether the Calcite call shape needs rewriting:
YAML signature only —
date_part. DataFusion has it natively; substrait core doesn't declare a signature. One YAML entry.Adapter rewrite (cat 3) —
YEAR(ts)→date_part('year', ts)viaYearAdapter.UNIX_TIMESTAMP(ts)→to_unixtime(ts)viaUnixTimestampAdapter. Both use the reusableAbstractNameMappingAdapter— a concrete subclass per function. Follow-up date-part functions (MonthAdapter, DayAdapter, ...) use the same shape.Rust UDF (cat 4) —
convert_tz. DataFusion has no native timezone-shift; the UDF implements it via chrono-tz. AConvertTzAdapterbridges PPL's bespoke operator to a locally-declaredSqlFunctionthat isthmus can resolve to the UDF.Supporting changes
AbstractNameMappingAdapter— reusable base for rename + prepend/append literal-arg injection. Preserves the original call's return type so the enclosingProject's cachedrowTypestays consistent with the adapted expression.ScalarFunctionenum additions:YEAR,CONVERT_TZ,UNIX_TIMESTAMP.fromSqlFunctionnow returns null on unknown rather than throwing (matches its javadoc contract).DataFusionAnalyticsBackendPlugindeclaresprojectCapabilities()for the new functions and wires the three adapters throughscalarFunctionAdapters().DataFusionFragmentConvertorwires a customScalarFunctionConverterthrough the 6-argConverterProviderconstructor soADDITIONAL_SCALAR_SIGSactually takes effect.OpenSearchProject.stripAnnotationsfrom Enable PPL fillnull on the analytics-engine route via DataFusion COALESCE #21472: recursive RexShuttle unwrap so nestedAnnotatedProjectExpressionwrappers are stripped at every depth before fragment conversion. Required for any call with literal operands.Related Issues
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.