[analytics-engine] wire PPL TAKE / FIRST / LAST / LIST / VALUES end-to-end - #21731
Conversation
PR Reviewer Guide 🔍(Review updated until commit b674382)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to b674382 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 0d26df5
Suggestions up to commit 2ddafc9
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21731 +/- ##
============================================
- Coverage 73.46% 73.45% -0.02%
- Complexity 74825 74837 +12
============================================
Files 5997 6005 +8
Lines 339688 339767 +79
Branches 48961 48969 +8
============================================
+ Hits 249558 249564 +6
- Misses 70272 70321 +49
- Partials 19858 19882 +24 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
… aggregates Adds five enum entries for PPL TAKE / FIRST / LAST / LIST / VALUES and an input-parameterised IntermediateField type resolver. Existing fixed-shape aggregates (HLL Binary sketch, COUNT Int64 counter) keep their constant intermediate types via IntermediateTypeResolver.fixed(); state-expanding aggregates whose FINAL state shape derives from arg 0 use IntermediateTypeResolver.passThroughArg0() so the planner can resolve the exchange column type from the actual call's arg type rather than a constant. Internalises the Arrow → Calcite type mapping (previously in the planner module's ArrowCalciteTypes) so the SPI is self-contained. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
…nd-to-end Wires PPL TAKE / FIRST / LAST / LIST / VALUES through the DataFusion backend: * Rust UDAF for take(field, n) at rust/src/udaf/take.rs. Limit is read from a Substrait Literal when present, or from row 0 of arg 1 when Calcite has materialised n as a constant Project column. Default n = 10. State is a bounded List<elem> emitted from state(); coordinator-side merge_batch concatenates and re-truncates. UDAF registered on every SessionContext via crate::udaf::register_all. * Substrait extension entries in opensearch_aggregate_functions.yaml for take, first_value, last_value, and array_agg. take's 2-arg shape uses distinct any1/any2 generics so isthmus's wildcard binding accepts mismatched arg types (state list<elem> + integer n). * DataFusionFragmentConvertor: LOCAL_TAKE_OP / LOCAL_FIRST_OP / LOCAL_LAST_OP / LOCAL_ARRAY_AGG_OP stub SqlAggFunctions plus a pre-emit RelShuttle that rewrites PPL aggregation calls (TAKE / FIRST / LAST / LIST / VALUES) onto the stubs so isthmus's AggregateFunctionConverter resolves them through ADDITIONAL_AGGREGATE_SIGS to the YAML extension names. LIST/VALUES rebuild the call's return type as ARRAY<actual-arg0> to override PPL's lossy STRING_ARRAY which would otherwise force ARRAY<VARCHAR>. * SubstraitPlanRewriter.visit(Aggregate): post-emit pass that inlines Project-column literals into Aggregate.Measure args. Required because Calcite's RelBuilder.aggregate auto-projects literal aggregate-args as $f1 columns, leaving the substrait emit with FieldRefs instead of Literals. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
…g aggregates Three planner-level changes that compose to make TAKE / FIRST / LAST / LIST / VALUES correct across shards: 1. Move AggregateCallAnnotation out of AggregateCall.rexList onto an OpenSearchAggregate side-map keyed by call index. Calcite's AggCallBinding.preOperands treats every rexList entry as a leading operand for inferReturnType, which corrupts return-type inference for PPL operators that read getOperandType(0) (ARG0_ARRAY on TAKE / LIST / VALUES would double-wrap). Storing annotations out-of-band on OpenSearchAggregate sidesteps the issue without touching PPL. Drops the AGG_CALL_ANNOTATION marker class and the annotation-stripping logic in stripAnnotations / copyResolved. 2. DistributedAggregateRewriter: drive intermediate-field types through the SPI's IntermediateTypeResolver (replacing the static ArrowCalciteTypes helper, which is removed). For STATE_EXPANDING + APPROXIMATE engine-native-merge aggregates, pin the FINAL aggCall's explicit return type to the StageInputScan column type so substrait declares what Rust's derive_schema_from_partial_plan produces — PPL's stock ReturnTypes are wrong here (ARG0_ARRAY double-wraps, STRING_ARRAY ignores the actual element type). 3. Cross-shard literal-arg forwarding for TAKE's N. OpenSearchAggregateSplitRule captures any RexLiteral aggregate-args from the original SINGLE aggregate's underlying Project and stashes them on the FINAL OpenSearchAggregate as a side-map. DistributedAggregateRewriter (Phase 2b) wraps the FINAL's StageInputScan in an OpenSearchProject that re-creates each captured literal as a constant column, and rebuilds the FINAL aggCall's argList to [stateColIdx, ...litColIdxs]. The convertor's existing SubstraitPlanRewriter inliner then emits the literals as Substrait Literal expressions. The producer-side PARTIAL stage still consumes N inside its own accumulator; without this, FINAL would re-aggregate without N and fall back to the default limit. IT cluster runs with -da:org.apache.calcite... so Calcite's typeMatchesInferred assertion is silenced — post opensearch-project#21690 the wire schema is derived in Rust, not Java, and PPL operators with non-idempotent return-type inference would otherwise trip this assertion on the FINAL side. Production runs without -ea. CoordinatorReduceIT: ten new tests (single-shard + cross-shard for each of the five aggregates). LIST/VALUES across-shards remain @AwaitsFix until the PPL frontend stops declaring STRING_ARRAY for them. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
Two stacked issues blocked cross-shard LIST/VALUES: 1. PPL declares LIST/VALUES return type via PPLReturnTypes.STRING_ARRAY which forces ARRAY<VARCHAR> regardless of input element type. Fix: repair the PARTIAL aggCall return type to ARRAY<actual-arg0> in OpenSearchAggregateSplitRule (PARTIAL only — FINAL keeps the original list to satisfy Volcano's parent row-type check on transformTo). The corrected type propagates through PARTIAL output → StageInputScan → FINAL substrait base_schema. 2. DataFusion's substrait consumer ignores AggregationPhase, so a FINAL array_agg(state) gets lowered as a single-pass aggregate that re-wraps each shard's list rather than concatenating. Fix: custom Rust UDAFs `list_merge` and `list_merge_distinct` whose `update_batch` un-nests each List<elem> row into elements (with optional dedup). Same pattern as TAKE's accumulator. The convertor's pre-emit RelShuttle detects the FINAL form (arg0 already a list type) and routes LIST → list_merge, VALUES → list_merge_distinct. PARTIAL still uses DataFusion's native array_agg (with INVOCATION_DISTINCT for VALUES). Cross-shard testListAcrossShards / testValuesAcrossShards pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
|
Persistent review updated to latest commit 0d26df5 |
|
@expani Thanks for review, addressed your comments. |
…ites Addresses two review comments on the state-expanding aggregate (TAKE / FIRST / LAST / LIST / VALUES) wiring. 1. Move literal-arg inlining off the post-emit Substrait IR pass and onto the isthmus binding layer. Calcite's RelBuilder.aggregate auto-projects literal aggregate-args (e.g. TAKE's N) as separate columns and AggregateCall.argList only carries column indices, so without inlining our Rust UDAFs only see a column reference at construction time. With DataFusion's two-stage execution, the Final accumulator dispatches through merge_batch — which receives only state columns, not the constant — so the limit can never be resolved on the coordinator. The fix overrides AggregateFunctionConverter.convert() in createVisitor() (next to the existing getSigs() override): after isthmus binds the AggregateFunctionInvocation, replace any FieldReference arg pointing to a literal Project column with the converted RexLiteral via the supplied Function<RexNode, Expression> — same Substrait literal an inlined arg would produce, routed through isthmus's standard literal converter. SubstraitPlanRewriter.visit(Aggregate) and its simpleStructOffset helper are deleted; the post-emit pass now only handles Filter rewrites and the PrecisionTimestampLiteral precision fix. 2. Extract the PPL aggregate-name → LOCAL_*_OP rewrite from DataFusionFragmentConvertor into a new package-private PplAggregateCallRewriter, matching the UntypedNullPreprocessor / DatetimeOutputCastRewriter sibling pattern. The if/else-if chain on operator name becomes a switch on toUpperCase(Locale.ROOT) and the six-way `==` chain that gates already-rewritten calls becomes a Set<SqlAggFunction> contains check. The two call sites in DataFusionFragmentConvertor now read like the other preprocessor invocations. CoordinatorReduceIT (18 tests), integTestMemtable, and integTestStreaming all pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
|
Persistent review updated to latest commit b674382 |
…o-end (opensearch-project#21731) * analytics-framework: extend AggregateFunction SPI for state-expanding aggregates Adds five enum entries for PPL TAKE / FIRST / LAST / LIST / VALUES and an input-parameterised IntermediateField type resolver. Existing fixed-shape aggregates (HLL Binary sketch, COUNT Int64 counter) keep their constant intermediate types via IntermediateTypeResolver.fixed(); state-expanding aggregates whose FINAL state shape derives from arg 0 use IntermediateTypeResolver.passThroughArg0() so the planner can resolve the exchange column type from the actual call's arg type rather than a constant. Internalises the Arrow → Calcite type mapping (previously in the planner module's ArrowCalciteTypes) so the SPI is self-contained. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-backend-datafusion: wire 5 PPL state-expanding aggregates end-to-end Wires PPL TAKE / FIRST / LAST / LIST / VALUES through the DataFusion backend: * Rust UDAF for take(field, n) at rust/src/udaf/take.rs. Limit is read from a Substrait Literal when present, or from row 0 of arg 1 when Calcite has materialised n as a constant Project column. Default n = 10. State is a bounded List<elem> emitted from state(); coordinator-side merge_batch concatenates and re-truncates. UDAF registered on every SessionContext via crate::udaf::register_all. * Substrait extension entries in opensearch_aggregate_functions.yaml for take, first_value, last_value, and array_agg. take's 2-arg shape uses distinct any1/any2 generics so isthmus's wildcard binding accepts mismatched arg types (state list<elem> + integer n). * DataFusionFragmentConvertor: LOCAL_TAKE_OP / LOCAL_FIRST_OP / LOCAL_LAST_OP / LOCAL_ARRAY_AGG_OP stub SqlAggFunctions plus a pre-emit RelShuttle that rewrites PPL aggregation calls (TAKE / FIRST / LAST / LIST / VALUES) onto the stubs so isthmus's AggregateFunctionConverter resolves them through ADDITIONAL_AGGREGATE_SIGS to the YAML extension names. LIST/VALUES rebuild the call's return type as ARRAY<actual-arg0> to override PPL's lossy STRING_ARRAY which would otherwise force ARRAY<VARCHAR>. * SubstraitPlanRewriter.visit(Aggregate): post-emit pass that inlines Project-column literals into Aggregate.Measure args. Required because Calcite's RelBuilder.aggregate auto-projects literal aggregate-args as $f1 columns, leaving the substrait emit with FieldRefs instead of Literals. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-engine: distributed planner rewrites for PPL state-expanding aggregates Three planner-level changes that compose to make TAKE / FIRST / LAST / LIST / VALUES correct across shards: 1. Move AggregateCallAnnotation out of AggregateCall.rexList onto an OpenSearchAggregate side-map keyed by call index. Calcite's AggCallBinding.preOperands treats every rexList entry as a leading operand for inferReturnType, which corrupts return-type inference for PPL operators that read getOperandType(0) (ARG0_ARRAY on TAKE / LIST / VALUES would double-wrap). Storing annotations out-of-band on OpenSearchAggregate sidesteps the issue without touching PPL. Drops the AGG_CALL_ANNOTATION marker class and the annotation-stripping logic in stripAnnotations / copyResolved. 2. DistributedAggregateRewriter: drive intermediate-field types through the SPI's IntermediateTypeResolver (replacing the static ArrowCalciteTypes helper, which is removed). For STATE_EXPANDING + APPROXIMATE engine-native-merge aggregates, pin the FINAL aggCall's explicit return type to the StageInputScan column type so substrait declares what Rust's derive_schema_from_partial_plan produces — PPL's stock ReturnTypes are wrong here (ARG0_ARRAY double-wraps, STRING_ARRAY ignores the actual element type). 3. Cross-shard literal-arg forwarding for TAKE's N. OpenSearchAggregateSplitRule captures any RexLiteral aggregate-args from the original SINGLE aggregate's underlying Project and stashes them on the FINAL OpenSearchAggregate as a side-map. DistributedAggregateRewriter (Phase 2b) wraps the FINAL's StageInputScan in an OpenSearchProject that re-creates each captured literal as a constant column, and rebuilds the FINAL aggCall's argList to [stateColIdx, ...litColIdxs]. The convertor's existing SubstraitPlanRewriter inliner then emits the literals as Substrait Literal expressions. The producer-side PARTIAL stage still consumes N inside its own accumulator; without this, FINAL would re-aggregate without N and fall back to the default limit. IT cluster runs with -da:org.apache.calcite... so Calcite's typeMatchesInferred assertion is silenced — post opensearch-project#21690 the wire schema is derived in Rust, not Java, and PPL operators with non-idempotent return-type inference would otherwise trip this assertion on the FINAL side. Production runs without -ea. CoordinatorReduceIT: ten new tests (single-shard + cross-shard for each of the five aggregates). LIST/VALUES across-shards remain @AwaitsFix until the PPL frontend stops declaring STRING_ARRAY for them. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-engine: cross-shard LIST and VALUES via list_merge UDAF Two stacked issues blocked cross-shard LIST/VALUES: 1. PPL declares LIST/VALUES return type via PPLReturnTypes.STRING_ARRAY which forces ARRAY<VARCHAR> regardless of input element type. Fix: repair the PARTIAL aggCall return type to ARRAY<actual-arg0> in OpenSearchAggregateSplitRule (PARTIAL only — FINAL keeps the original list to satisfy Volcano's parent row-type check on transformTo). The corrected type propagates through PARTIAL output → StageInputScan → FINAL substrait base_schema. 2. DataFusion's substrait consumer ignores AggregationPhase, so a FINAL array_agg(state) gets lowered as a single-pass aggregate that re-wraps each shard's list rather than concatenating. Fix: custom Rust UDAFs `list_merge` and `list_merge_distinct` whose `update_batch` un-nests each List<elem> row into elements (with optional dedup). Same pattern as TAKE's accumulator. The convertor's pre-emit RelShuttle detects the FINAL form (arg0 already a list type) and routes LIST → list_merge, VALUES → list_merge_distinct. PARTIAL still uses DataFusion's native array_agg (with INVOCATION_DISTINCT for VALUES). Cross-shard testListAcrossShards / testValuesAcrossShards pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-backend-datafusion: relocate state-expanding aggregate rewrites Addresses two review comments on the state-expanding aggregate (TAKE / FIRST / LAST / LIST / VALUES) wiring. 1. Move literal-arg inlining off the post-emit Substrait IR pass and onto the isthmus binding layer. Calcite's RelBuilder.aggregate auto-projects literal aggregate-args (e.g. TAKE's N) as separate columns and AggregateCall.argList only carries column indices, so without inlining our Rust UDAFs only see a column reference at construction time. With DataFusion's two-stage execution, the Final accumulator dispatches through merge_batch — which receives only state columns, not the constant — so the limit can never be resolved on the coordinator. The fix overrides AggregateFunctionConverter.convert() in createVisitor() (next to the existing getSigs() override): after isthmus binds the AggregateFunctionInvocation, replace any FieldReference arg pointing to a literal Project column with the converted RexLiteral via the supplied Function<RexNode, Expression> — same Substrait literal an inlined arg would produce, routed through isthmus's standard literal converter. SubstraitPlanRewriter.visit(Aggregate) and its simpleStructOffset helper are deleted; the post-emit pass now only handles Filter rewrites and the PrecisionTimestampLiteral precision fix. 2. Extract the PPL aggregate-name → LOCAL_*_OP rewrite from DataFusionFragmentConvertor into a new package-private PplAggregateCallRewriter, matching the UntypedNullPreprocessor / DatetimeOutputCastRewriter sibling pattern. The if/else-if chain on operator name becomes a switch on toUpperCase(Locale.ROOT) and the six-way `==` chain that gates already-rewritten calls becomes a Set<SqlAggFunction> contains check. The two call sites in DataFusionFragmentConvertor now read like the other preprocessor invocations. CoordinatorReduceIT (18 tests), integTestMemtable, and integTestStreaming all pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> --------- Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
These four tests were muted by #21774 right after #21731 introduced them. The underlying aggCall slot adjustment / IntermediateField re-classification issue was fixed by #21875 (Refactor distributed-aggregate rewriter). Verified locally: 12/12 runs pass with -Dtests.iters=3. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
…o-end (opensearch-project#21731) * analytics-framework: extend AggregateFunction SPI for state-expanding aggregates Adds five enum entries for PPL TAKE / FIRST / LAST / LIST / VALUES and an input-parameterised IntermediateField type resolver. Existing fixed-shape aggregates (HLL Binary sketch, COUNT Int64 counter) keep their constant intermediate types via IntermediateTypeResolver.fixed(); state-expanding aggregates whose FINAL state shape derives from arg 0 use IntermediateTypeResolver.passThroughArg0() so the planner can resolve the exchange column type from the actual call's arg type rather than a constant. Internalises the Arrow → Calcite type mapping (previously in the planner module's ArrowCalciteTypes) so the SPI is self-contained. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-backend-datafusion: wire 5 PPL state-expanding aggregates end-to-end Wires PPL TAKE / FIRST / LAST / LIST / VALUES through the DataFusion backend: * Rust UDAF for take(field, n) at rust/src/udaf/take.rs. Limit is read from a Substrait Literal when present, or from row 0 of arg 1 when Calcite has materialised n as a constant Project column. Default n = 10. State is a bounded List<elem> emitted from state(); coordinator-side merge_batch concatenates and re-truncates. UDAF registered on every SessionContext via crate::udaf::register_all. * Substrait extension entries in opensearch_aggregate_functions.yaml for take, first_value, last_value, and array_agg. take's 2-arg shape uses distinct any1/any2 generics so isthmus's wildcard binding accepts mismatched arg types (state list<elem> + integer n). * DataFusionFragmentConvertor: LOCAL_TAKE_OP / LOCAL_FIRST_OP / LOCAL_LAST_OP / LOCAL_ARRAY_AGG_OP stub SqlAggFunctions plus a pre-emit RelShuttle that rewrites PPL aggregation calls (TAKE / FIRST / LAST / LIST / VALUES) onto the stubs so isthmus's AggregateFunctionConverter resolves them through ADDITIONAL_AGGREGATE_SIGS to the YAML extension names. LIST/VALUES rebuild the call's return type as ARRAY<actual-arg0> to override PPL's lossy STRING_ARRAY which would otherwise force ARRAY<VARCHAR>. * SubstraitPlanRewriter.visit(Aggregate): post-emit pass that inlines Project-column literals into Aggregate.Measure args. Required because Calcite's RelBuilder.aggregate auto-projects literal aggregate-args as $f1 columns, leaving the substrait emit with FieldRefs instead of Literals. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-engine: distributed planner rewrites for PPL state-expanding aggregates Three planner-level changes that compose to make TAKE / FIRST / LAST / LIST / VALUES correct across shards: 1. Move AggregateCallAnnotation out of AggregateCall.rexList onto an OpenSearchAggregate side-map keyed by call index. Calcite's AggCallBinding.preOperands treats every rexList entry as a leading operand for inferReturnType, which corrupts return-type inference for PPL operators that read getOperandType(0) (ARG0_ARRAY on TAKE / LIST / VALUES would double-wrap). Storing annotations out-of-band on OpenSearchAggregate sidesteps the issue without touching PPL. Drops the AGG_CALL_ANNOTATION marker class and the annotation-stripping logic in stripAnnotations / copyResolved. 2. DistributedAggregateRewriter: drive intermediate-field types through the SPI's IntermediateTypeResolver (replacing the static ArrowCalciteTypes helper, which is removed). For STATE_EXPANDING + APPROXIMATE engine-native-merge aggregates, pin the FINAL aggCall's explicit return type to the StageInputScan column type so substrait declares what Rust's derive_schema_from_partial_plan produces — PPL's stock ReturnTypes are wrong here (ARG0_ARRAY double-wraps, STRING_ARRAY ignores the actual element type). 3. Cross-shard literal-arg forwarding for TAKE's N. OpenSearchAggregateSplitRule captures any RexLiteral aggregate-args from the original SINGLE aggregate's underlying Project and stashes them on the FINAL OpenSearchAggregate as a side-map. DistributedAggregateRewriter (Phase 2b) wraps the FINAL's StageInputScan in an OpenSearchProject that re-creates each captured literal as a constant column, and rebuilds the FINAL aggCall's argList to [stateColIdx, ...litColIdxs]. The convertor's existing SubstraitPlanRewriter inliner then emits the literals as Substrait Literal expressions. The producer-side PARTIAL stage still consumes N inside its own accumulator; without this, FINAL would re-aggregate without N and fall back to the default limit. IT cluster runs with -da:org.apache.calcite... so Calcite's typeMatchesInferred assertion is silenced — post opensearch-project#21690 the wire schema is derived in Rust, not Java, and PPL operators with non-idempotent return-type inference would otherwise trip this assertion on the FINAL side. Production runs without -ea. CoordinatorReduceIT: ten new tests (single-shard + cross-shard for each of the five aggregates). LIST/VALUES across-shards remain @AwaitsFix until the PPL frontend stops declaring STRING_ARRAY for them. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-engine: cross-shard LIST and VALUES via list_merge UDAF Two stacked issues blocked cross-shard LIST/VALUES: 1. PPL declares LIST/VALUES return type via PPLReturnTypes.STRING_ARRAY which forces ARRAY<VARCHAR> regardless of input element type. Fix: repair the PARTIAL aggCall return type to ARRAY<actual-arg0> in OpenSearchAggregateSplitRule (PARTIAL only — FINAL keeps the original list to satisfy Volcano's parent row-type check on transformTo). The corrected type propagates through PARTIAL output → StageInputScan → FINAL substrait base_schema. 2. DataFusion's substrait consumer ignores AggregationPhase, so a FINAL array_agg(state) gets lowered as a single-pass aggregate that re-wraps each shard's list rather than concatenating. Fix: custom Rust UDAFs `list_merge` and `list_merge_distinct` whose `update_batch` un-nests each List<elem> row into elements (with optional dedup). Same pattern as TAKE's accumulator. The convertor's pre-emit RelShuttle detects the FINAL form (arg0 already a list type) and routes LIST → list_merge, VALUES → list_merge_distinct. PARTIAL still uses DataFusion's native array_agg (with INVOCATION_DISTINCT for VALUES). Cross-shard testListAcrossShards / testValuesAcrossShards pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> * analytics-backend-datafusion: relocate state-expanding aggregate rewrites Addresses two review comments on the state-expanding aggregate (TAKE / FIRST / LAST / LIST / VALUES) wiring. 1. Move literal-arg inlining off the post-emit Substrait IR pass and onto the isthmus binding layer. Calcite's RelBuilder.aggregate auto-projects literal aggregate-args (e.g. TAKE's N) as separate columns and AggregateCall.argList only carries column indices, so without inlining our Rust UDAFs only see a column reference at construction time. With DataFusion's two-stage execution, the Final accumulator dispatches through merge_batch — which receives only state columns, not the constant — so the limit can never be resolved on the coordinator. The fix overrides AggregateFunctionConverter.convert() in createVisitor() (next to the existing getSigs() override): after isthmus binds the AggregateFunctionInvocation, replace any FieldReference arg pointing to a literal Project column with the converted RexLiteral via the supplied Function<RexNode, Expression> — same Substrait literal an inlined arg would produce, routed through isthmus's standard literal converter. SubstraitPlanRewriter.visit(Aggregate) and its simpleStructOffset helper are deleted; the post-emit pass now only handles Filter rewrites and the PrecisionTimestampLiteral precision fix. 2. Extract the PPL aggregate-name → LOCAL_*_OP rewrite from DataFusionFragmentConvertor into a new package-private PplAggregateCallRewriter, matching the UntypedNullPreprocessor / DatetimeOutputCastRewriter sibling pattern. The if/else-if chain on operator name becomes a switch on toUpperCase(Locale.ROOT) and the six-way `==` chain that gates already-rewritten calls becomes a Set<SqlAggFunction> contains check. The two call sites in DataFusionFragmentConvertor now read like the other preprocessor invocations. CoordinatorReduceIT (18 tests), integTestMemtable, and integTestStreaming all pass. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com> --------- Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
…#21901) These four tests were muted by opensearch-project#21774 right after opensearch-project#21731 introduced them. The underlying aggCall slot adjustment / IntermediateField re-classification issue was fixed by opensearch-project#21875 (Refactor distributed-aggregate rewriter). Verified locally: 12/12 runs pass with -Dtests.iters=3. Signed-off-by: Sandesh Kumar <sandeshkr419@gmail.com>
Summary
Wires five PPL state-expanding aggregates through the analytics engine with correct multi-shard behavior:
take(field, n)— bounded array of first N valuesfirst(field)/last(field)— first/last value per grouplist(field)/values(field)— array of values (values = distinct)All work both single-shard and across shards.
Commits
SPI (
analytics-framework): five newAggregateFunctionenum entries plusIntermediateTypeResolverso the planner can derive the FINAL exchange-column type from the actual call's arg type rather than a constant.Backend (
analytics-backend-datafusion):take(field, n)with boundedList<elem>state and coordinator-side merge inupdate_batch(DataFusion's substrait consumer ignoresAggregationPhase, so FINAL is also a single-pass aggregate that needs to un-nest the per-shard state).take,first_value,last_value,array_agg).take's 2-arg shape usesany1/any2so isthmus's wildcard binding accepts mismatched arg types.LOCAL_*_OPstubs + a pre-emit RelShuttle that rebinds PPL aggregations onto the stubs so isthmus resolves them through our extension catalog.SubstraitPlanRewriter.visit(Aggregate)inlines literal Project columns as SubstraitExpression.Literalon aggregate-call args (Calcite materializes constant aggregate-args as$f1columns).Planner (
analytics-engine):AggregateCallAnnotationout ofAggregateCall.rexListonto anOpenSearchAggregateside-map. Calcite'sAggCallBinding.preOperandswould otherwise corrupt return-type inference for PPL'sARG0_ARRAY.DistributedAggregateRewriterdrives intermediate-field types throughIntermediateTypeResolverand pins the FINAL aggCall return type to the StageInputScan column for state-expanding aggregates.take's N:OpenSearchAggregateSplitRulecaptures literal aggregate-args from the original Project; the rewriter re-creates them as a constant Project below FINAL so the Rust accumulator preserves N.Cross-shard LIST / VALUES (
analytics-engine):STRING_ARRAYreturn type forcesARRAY<VARCHAR>regardless of input element.OpenSearchAggregateSplitRulerepairs the PARTIAL aggCall return type toARRAY<actual-arg0>(PARTIAL only — FINAL keeps the original to satisfy Volcano's parent row-type check).AggregationPhase, so a FINALarray_agg(state)re-wraps each shard's list rather than concatenating. Custom Rust UDAFslist_merge/list_merge_distinctun-nest the per-shard state. The convertor routes LIST/VALUES at FINAL (arg0 is already a list type) to the merge UDAFs; PARTIAL keepsarray_agg.Test plan
takeandlist_mergeaccumulators (PARTIAL, FINALun-nest, distinct dedup, null handling).
CoordinatorReduceIT— single-shard + cross-shard for each of the fiveaggregates. 18/18 pass, 0 deferred.
:sandbox:qa:analytics-engine-rest:integTestgreen.annotation side-map move.
Notes
The IT cluster runs with
-da:org.apache.calcite...to silence Calcite'sAggregate.typeMatchesInferreddebug assertion. The wire schema isderived in Rust from the producer's substrait plan (post #21690), not from
Calcite types, so this assertion is architecturally redundant for our setup
— enforced at runtime by Rust's
derive_schema_from_partial_plan+ Arrow'sensure_field_compatibility. Production runs without-ea.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.