Commit 2380af7
authored
Enable PPL JSON scalar functions on the analytics-engine route (opensearch-project#21513)
* [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 (opensearch-project#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 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_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 opensearch-project#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>1 parent 4d3562b commit 2380af7
20 files changed
Lines changed: 2942 additions & 13 deletions
File tree
- sandbox
- libs/analytics-framework/src/main/java/org/opensearch/analytics/spi
- plugins/analytics-backend-datafusion
- rust
- src/udf
- src
- internalClusterTest/java/org/opensearch/be/datafusion
- main
- java/org/opensearch/be/datafusion
- resources
- test/java/org/opensearch/be/datafusion
Lines changed: 10 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
172 | 181 | | |
173 | 182 | | |
174 | 183 | | |
| |||
Lines changed: 20 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
52 | 72 | | |
53 | 73 | | |
54 | 74 | | |
| |||
Lines changed: 335 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
0 commit comments