This is an automated email from the ASF dual-hosted git repository.
avantgardnerio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git
The following commit(s) were added to refs/heads/main by this push:
new 5333cd6b4 feat(core): PartitionedBoundedWindowAggExec — parallel BWAG
wrapper (#2253)
5333cd6b4 is described below
commit 5333cd6b45ae61e725ab60ebcc1a6312cd4da56a
Author: Brent Gardner <[email protected]>
AuthorDate: Sat Aug 8 06:52:48 2026 -0600
feat(core): PartitionedBoundedWindowAggExec — parallel BWAG wrapper (#2253)
Wraps DataFusion's `BoundedWindowAggExec` and overrides its
`SinglePartition` distribution requirement to `Unspecified`. Hides BWAG
from tree walkers by returning only the wrapper's input from
`children()`, so `EnforceDistribution` can't reinsert an SPM(K→1)
beneath it. Safe iff the input is already range-repartitioned + halo
covers frame boundaries (see module doc).
This is a Ballista-side placeholder for the upstream draft at
apache/datafusion#23026 ("Parallel bounded RANGE-frame window functions
without PARTITION BY"). Once that lands and Ballista bumps its DF pin
past it, this wrapper collapses and callers target DF's BWAG directly.
- `ballista_core::execution_plans::partitioned_bounded_window_agg`: the
new operator. `InputOrderMode` and `can_repartition` are hardcoded
(`Sorted` / `false`) — the only planned caller is a
no-PARTITION-BY + single-Column-ORDER-BY range-window rule.
- `BallistaPhysicalPlanNode::PartitionedBoundedWindowAgg`: proto
message carrying only `window_expr` — the rest is implicit from the
caller's shape gates. Round-trip goes through DF's
`serialize_physical_window_expr` / `parse_physical_window_expr`.
- Unit test `per_partition_execute_running_sum_no_cross_partition_leak`
proves BWAG actually aggregates (partition 0's running sums are
[1, 3, 6], not [1, 2, 3]) and that the K→K partitioning doesn't leak
across partition boundaries (partition 1's first sum is 100, not 106).
No in-tree caller yet — this is prep for the parallel-window rule
extracted from #2223. Landing it separately keeps that PR's diff
focused on the scheduler rule.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
ballista/core/proto/ballista.proto | 11 +
ballista/core/src/execution_plans/mod.rs | 2 +
.../partitioned_bounded_window_agg.rs | 328 +++++++++++++++++++++
ballista/core/src/serde/generated/ballista.rs | 17 +-
ballista/core/src/serde/mod.rs | 56 +++-
5 files changed, 410 insertions(+), 4 deletions(-)
diff --git a/ballista/core/proto/ballista.proto
b/ballista/core/proto/ballista.proto
index 99fceb91e..285accd52 100644
--- a/ballista/core/proto/ballista.proto
+++ b/ballista/core/proto/ballista.proto
@@ -56,6 +56,7 @@ message BallistaPhysicalPlanNode {
UnorderedRangeRepartitionExecNode unordered_range_repartition = 8;
OrderedRangeRepartitionExecNode ordered_range_repartition = 9;
PerPartitionFilterExecNode per_partition_filter = 10;
+ PartitionedBoundedWindowAggExecNode partitioned_bounded_window_agg = 11;
}
}
@@ -133,6 +134,16 @@ message PerPartitionFilterExecNode {
repeated datafusion.PhysicalExprNode predicates = 1;
}
+// Wrapper for `BoundedWindowAggExec` that overrides
+// `required_input_distribution` to `Unspecified` — see the module doc on
+// `execution_plans::partitioned_bounded_window_agg` for what makes that safe.
+// The child plan is plumbed by the framework as `inputs[0]` during decode.
+// `input_order_mode` and `can_repartition` are hardcoded on the decode side
+// per the rule's shape gates; only `window_expr` needs to cross the wire.
+message PartitionedBoundedWindowAggExecNode {
+ repeated datafusion.PhysicalWindowExprNode window_expr = 1;
+}
+
message ChaosExecNode {
double failure_probability = 1;
string fault_type = 2;
diff --git a/ballista/core/src/execution_plans/mod.rs
b/ballista/core/src/execution_plans/mod.rs
index 5d9240f3a..230b68913 100644
--- a/ballista/core/src/execution_plans/mod.rs
+++ b/ballista/core/src/execution_plans/mod.rs
@@ -23,6 +23,7 @@ mod chaos_exec;
mod distributed_explain_analyze;
mod distributed_query;
mod ordered_range_repartition;
+mod partitioned_bounded_window_agg;
mod per_partition_filter;
pub mod plan_algebra;
mod range_repartition_common;
@@ -42,6 +43,7 @@ use datafusion::common::exec_err;
pub use distributed_explain_analyze::DistributedExplainAnalyzeExec;
pub use distributed_query::{DistributedQueryExec, execute_physical_plan};
pub use ordered_range_repartition::OrderedRangeRepartitionExec;
+pub use partitioned_bounded_window_agg::PartitionedBoundedWindowAggExec;
pub use per_partition_filter::{PerPartitionFilterExec,
range_partition_predicates};
pub use plan_algebra::{preserves_distribution, preserves_partitioning};
pub use runtime_stats::{
diff --git
a/ballista/core/src/execution_plans/partitioned_bounded_window_agg.rs
b/ballista/core/src/execution_plans/partitioned_bounded_window_agg.rs
new file mode 100644
index 000000000..d8f044442
--- /dev/null
+++ b/ballista/core/src/execution_plans/partitioned_bounded_window_agg.rs
@@ -0,0 +1,328 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Wrap a `BoundedWindowAggExec` and override its
+//! `required_input_distribution` to `Unspecified`.
+//!
+//! # Status: temporary
+//!
+//! This is a Ballista-side wrapper because DataFusion's `BoundedWindowAggExec`
+//! doesn't yet expose a way to override its `SinglePartition` requirement.
+//! The upstream draft is <https://github.com/apache/datafusion/pull/23026>
+//! ("Parallel bounded RANGE-frame window functions without PARTITION BY").
+//! When that lands and Ballista bumps its DF pin past it, this wrapper
+//! should collapse and the rule should target DF's BWAG directly.
+//!
+//! DataFusion's `BoundedWindowAggExec` declares
+//! `Distribution::SinglePartition` when no PARTITION BY is present — a
+//! correctness guard because a window frame's semantics span rows across
+//! the whole input. With `ParallelWindowRule`'s range-repartition upstream,
+//! each input partition IS globally range-disjoint (halo covers boundary
+//! neighbours), so BWAG CAN run per-partition and produce K correct outputs.
+//!
+//! This wrapper flips only the distribution declaration. Everything else —
+//! schema, ordering, per-partition `execute()` — delegates to a canonical
+//! inner BWAG constructed at rule time. Because `children()` returns only
+//! the wrapper's own input (not the inner BWAG), tree walkers like
+//! `EnforceDistribution` never see BWAG and can't re-insert an
+//! `SPM(K→1)` beneath it.
+//!
+//! # Constraints assumed by the caller
+//!
+//! The wrapper is safe iff the input is already range-repartitioned so that
+//! each partition is a globally disjoint slice of the ORDER BY key + halo
+//! for frame boundaries. Callers are responsible for this — the wrapper
+//! itself doesn't (and can't) verify it. Wiring this wrapper below arbitrary
+//! (non-range-partitioned) inputs will silently produce wrong window values.
+//!
+//! # Assumption on DataFusion internals
+//!
+//! `BoundedWindowAggExec::execute(i, ctx)` in DataFusion 54 processes
+//! partition `i` of its input independently: no cross-partition state, no
+//! spawned tasks touching sibling partitions. The wrapper depends on that
+//! shape. The `per_partition_execute_running_sum_no_cross_partition_leak`
+//! test exercises this — if BWAG ever gains cross-partition state, that
+//! test's partition-1 sums would shift and it would fail.
+
+use std::fmt;
+use std::sync::Arc;
+
+use datafusion::arrow::datatypes::SchemaRef;
+use datafusion::common::{Result, Statistics, internal_err};
+use datafusion::execution::TaskContext;
+use datafusion::physical_expr::{Distribution, OrderingRequirements};
+use datafusion::physical_plan::execution_plan::{CardinalityEffect,
InputOrderMode};
+use datafusion::physical_plan::metrics::MetricsSet;
+use datafusion::physical_plan::windows::BoundedWindowAggExec;
+use datafusion::physical_plan::windows::WindowExpr;
+use datafusion::physical_plan::{
+ DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties,
+ SendableRecordBatchStream,
+};
+
+// The rule's `as_candidate` gates guarantee no PARTITION BY + single Column
+// ORDER BY over a sorted source, so `BWAG::try_new` is always invoked with
+// `InputOrderMode::Sorted` and `can_repartition=false` (partition_keys() is
+// empty either way when there's no PARTITION BY). Hardcode both to keep the
+// wire and the type small.
+const BWAG_INPUT_ORDER_MODE: InputOrderMode = InputOrderMode::Sorted;
+const BWAG_CAN_REPARTITION: bool = false;
+
+/// Wrap a `BoundedWindowAggExec` overriding its `required_input_distribution`
+/// to `Unspecified`. See module docs for what makes this safe.
+#[derive(Debug, Clone)]
+pub struct PartitionedBoundedWindowAggExec {
+ /// Canonical inner BWAG built at construction. Not a plan-tree child —
+ /// `children()` returns only [`Self::input`], so tree walkers can't
+ /// reach it.
+ inner_bwag: Arc<BoundedWindowAggExec>,
+ /// Multi-partition input; same `Arc` `inner_bwag.input()` holds.
+ input: Arc<dyn ExecutionPlan>,
+}
+
+impl PartitionedBoundedWindowAggExec {
+ /// Construct the wrapper. Builds a canonical inner
+ /// `BoundedWindowAggExec` from `window_expr` + `input` with the
+ /// hardcoded mode/repartition constants; failures propagate verbatim so
+ /// callers see the same error surface as constructing BWAG directly.
+ pub fn try_new(
+ window_expr: Vec<Arc<dyn WindowExpr>>,
+ input: Arc<dyn ExecutionPlan>,
+ ) -> Result<Self> {
+ let inner_bwag = Arc::new(BoundedWindowAggExec::try_new(
+ window_expr,
+ input.clone(),
+ BWAG_INPUT_ORDER_MODE,
+ BWAG_CAN_REPARTITION,
+ )?);
+ Ok(Self { inner_bwag, input })
+ }
+
+ /// The wrapped `BoundedWindowAggExec` — for accessors that don't exist
+ /// on `Self` and for wire-encoding.
+ pub fn inner_bwag(&self) -> &Arc<BoundedWindowAggExec> {
+ &self.inner_bwag
+ }
+
+ /// The window expressions carried by the wrapped BWAG.
+ pub fn window_expr(&self) -> &[Arc<dyn WindowExpr>] {
+ self.inner_bwag.window_expr()
+ }
+
+ /// Output schema (delegates to BWAG).
+ pub fn schema(&self) -> SchemaRef {
+ self.inner_bwag.schema()
+ }
+}
+
+impl DisplayAs for PartitionedBoundedWindowAggExec {
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) ->
fmt::Result {
+ write!(f, "PartitionedBoundedWindowAggExec: ")?;
+ self.inner_bwag.fmt_as(t, f)
+ }
+}
+
+impl ExecutionPlan for PartitionedBoundedWindowAggExec {
+ fn name(&self) -> &'static str {
+ "PartitionedBoundedWindowAggExec"
+ }
+
+ fn properties(&self) -> &Arc<PlanProperties> {
+ // BWAG's properties already advertise `input.output_partitioning()`
+ // as this operator's output partitioning — reuse them verbatim.
+ self.inner_bwag.properties()
+ }
+
+ fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+ vec![&self.input]
+ }
+
+ fn required_input_distribution(&self) -> Vec<Distribution> {
+ // The whole point of this wrapper.
+ vec![Distribution::UnspecifiedDistribution]
+ }
+
+ fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
+ self.inner_bwag.required_input_ordering()
+ }
+
+ fn maintains_input_order(&self) -> Vec<bool> {
+ vec![true]
+ }
+
+ fn with_new_children(
+ self: Arc<Self>,
+ children: Vec<Arc<dyn ExecutionPlan>>,
+ ) -> Result<Arc<dyn ExecutionPlan>> {
+ let [new_input] = children.as_slice() else {
+ return internal_err!(
+ "PartitionedBoundedWindowAggExec expects exactly 1 child, got
{}",
+ children.len()
+ );
+ };
+ Ok(Arc::new(Self::try_new(
+ self.window_expr().to_vec(),
+ new_input.clone(),
+ )?))
+ }
+
+ fn execute(
+ &self,
+ partition: usize,
+ context: Arc<TaskContext>,
+ ) -> Result<SendableRecordBatchStream> {
+ self.inner_bwag.execute(partition, context)
+ }
+
+ fn metrics(&self) -> Option<MetricsSet> {
+ self.inner_bwag.metrics()
+ }
+
+ fn partition_statistics(&self, partition: Option<usize>) ->
Result<Arc<Statistics>> {
+ self.inner_bwag.partition_statistics(partition)
+ }
+
+ fn cardinality_effect(&self) -> CardinalityEffect {
+ self.inner_bwag.cardinality_effect()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use datafusion::arrow::array::Float64Array;
+ use datafusion::arrow::compute::SortOptions;
+ use datafusion::arrow::datatypes::{DataType, Field, Schema};
+ use datafusion::arrow::record_batch::RecordBatch;
+ use datafusion::datasource::memory::MemorySourceConfig;
+ use datafusion::datasource::source::DataSourceExec;
+ use datafusion::functions_aggregate::sum::sum_udaf;
+ use datafusion::logical_expr::{
+ WindowFrame, WindowFrameBound, WindowFrameUnits,
WindowFunctionDefinition,
+ };
+ use datafusion::physical_expr::LexOrdering;
+ use datafusion::physical_expr::PhysicalSortExpr;
+ use datafusion::physical_expr::expressions::col;
+ use datafusion::physical_plan::windows::create_window_expr;
+ use datafusion::prelude::SessionContext;
+ use datafusion::scalar::ScalarValue;
+ use futures::TryStreamExt;
+
+ /// Two range-disjoint partitions, both sorted on `v`. Within each
+ /// partition rows are 1.0 apart so a 2.0-preceding frame spans multiple
+ /// rows (running sum). The between-partition gap (97.0) is wider than
+ /// the frame, so no frame ever crosses the boundary — per-partition BWAG
+ /// stays correct despite the K→K shape.
+ #[tokio::test]
+ async fn per_partition_execute_running_sum_no_cross_partition_leak()
+ -> datafusion::common::Result<()> {
+ let schema =
+ Arc::new(Schema::new(vec![Field::new("v", DataType::Float64,
false)]));
+ let partitions = vec![
+ vec![RecordBatch::try_new(
+ schema.clone(),
+ vec![Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0]))],
+ )?],
+ vec![RecordBatch::try_new(
+ schema.clone(),
+ vec![Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0]))],
+ )?],
+ ];
+ let v_expr = col("v", schema.as_ref())?;
+ let order_by = vec![PhysicalSortExpr {
+ expr: v_expr.clone(),
+ options: SortOptions::default(),
+ }];
+ let sort_information = vec![
+ LexOrdering::new(order_by.clone())
+ .expect("single sort expr is a valid LexOrdering"),
+ ];
+ let source = MemorySourceConfig::try_new(&partitions, schema.clone(),
None)?
+ .try_with_sort_information(sort_information)?;
+ let input: Arc<dyn ExecutionPlan> =
+ Arc::new(DataSourceExec::new(Arc::new(source)));
+
+ // sum(v) OVER (ORDER BY v RANGE BETWEEN 2.0 PRECEDING AND CURRENT
ROW).
+ // Rows within a partition are 1.0 apart so a 2.0 frame spans them;
+ // partitions are 97.0 apart so no frame crosses the boundary.
+ let frame = WindowFrame::new_bounds(
+ WindowFrameUnits::Range,
+ WindowFrameBound::Preceding(ScalarValue::Float64(Some(2.0))),
+ WindowFrameBound::CurrentRow,
+ );
+ let window_expr = create_window_expr(
+ &WindowFunctionDefinition::AggregateUDF(sum_udaf()),
+ "sum(v)".to_string(),
+ &[v_expr],
+ &[],
+ &order_by,
+ Arc::new(frame),
+ schema.clone(),
+ false,
+ false,
+ None,
+ )?;
+ let pbwag =
PartitionedBoundedWindowAggExec::try_new(vec![window_expr], input)?;
+
+ // Shape: wrapper must not collapse K→1 and must hide BWAG from the
tree.
+ assert_eq!(
+ pbwag.properties().output_partitioning().partition_count(),
+ 2,
+ "PBWAG must not collapse partitions"
+ );
+ assert!(matches!(
+ pbwag.required_input_distribution().as_slice(),
+ [Distribution::UnspecifiedDistribution]
+ ));
+ assert_eq!(
+ pbwag.children().len(),
+ 1,
+ "children() must return only the input, hiding the inner BWAG"
+ );
+
+ // Execute both partitions. Expected sums are running-sums within each
+ // partition; if any row from partition 0 leaked into partition 1's
+ // frame (or vice versa), partition 1's first sum would be > 100.0.
+ // Output schema is [v, sum(v)].
+ let ctx = SessionContext::new().task_ctx();
+ let pbwag: Arc<dyn ExecutionPlan> = Arc::new(pbwag);
+ for (partition, expected) in [(0, [1.0, 3.0, 6.0]), (1, [100.0, 201.0,
303.0])] {
+ let batches: Vec<RecordBatch> =
+ pbwag.execute(partition, ctx.clone())?.try_collect().await?;
+ let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
+ assert_eq!(total_rows, 3, "partition {partition}: row count
preserved");
+ let sums: Vec<f64> = batches
+ .iter()
+ .flat_map(|b| {
+ b.column_by_name("sum(v)")
+ .expect("sum(v) column")
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .expect("Float64Array")
+ .values()
+ .to_vec()
+ })
+ .collect();
+ assert_eq!(
+ sums,
+ expected.to_vec(),
+ "partition {partition}: frame-of-one → sum equals input"
+ );
+ }
+ Ok(())
+ }
+}
diff --git a/ballista/core/src/serde/generated/ballista.rs
b/ballista/core/src/serde/generated/ballista.rs
index 5bf6964eb..c5d3899dd 100644
--- a/ballista/core/src/serde/generated/ballista.rs
+++ b/ballista/core/src/serde/generated/ballista.rs
@@ -31,7 +31,7 @@ pub struct LogicalPlanCacheNode {
pub struct BallistaPhysicalPlanNode {
#[prost(
oneof = "ballista_physical_plan_node::PhysicalPlanType",
- tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10"
+ tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11"
)]
pub physical_plan_type: ::core::option::Option<
ballista_physical_plan_node::PhysicalPlanType,
@@ -61,6 +61,8 @@ pub mod ballista_physical_plan_node {
OrderedRangeRepartition(super::OrderedRangeRepartitionExecNode),
#[prost(message, tag = "10")]
PerPartitionFilter(super::PerPartitionFilterExecNode),
+ #[prost(message, tag = "11")]
+
PartitionedBoundedWindowAgg(super::PartitionedBoundedWindowAggExecNode),
}
}
/// Value-range router over N locally-sorted overlapping input partitions.
@@ -146,6 +148,19 @@ pub struct PerPartitionFilterExecNode {
::datafusion_proto::protobuf::PhysicalExprNode,
>,
}
+/// Wrapper for `BoundedWindowAggExec` that overrides
+/// `required_input_distribution` to `Unspecified` — see the module doc on
+/// `execution_plans::partitioned_bounded_window_agg` for what makes that safe.
+/// The child plan is plumbed by the framework as `inputs\[0\]` during decode.
+/// `input_order_mode` and `can_repartition` are hardcoded on the decode side
+/// per the rule's shape gates; only `window_expr` needs to cross the wire.
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PartitionedBoundedWindowAggExecNode {
+ #[prost(message, repeated, tag = "1")]
+ pub window_expr: ::prost::alloc::vec::Vec<
+ ::datafusion_proto::protobuf::PhysicalWindowExprNode,
+ >,
+}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChaosExecNode {
#[prost(double, tag = "1")]
diff --git a/ballista/core/src/serde/mod.rs b/ballista/core/src/serde/mod.rs
index 3ceaa2dfd..eb2f0181c 100644
--- a/ballista/core/src/serde/mod.rs
+++ b/ballista/core/src/serde/mod.rs
@@ -57,9 +57,9 @@ use std::{convert::TryInto, io::Cursor};
use crate::execution_plans::sort_shuffle::SortShuffleConfig;
use crate::execution_plans::{
BufferExec, BufferMode, ChaosExec, CoalescePlan,
OrderedRangeRepartitionExec,
- PartitionGroup, PerPartitionFilterExec, RuntimeStatsExec,
ShuffleReaderExec,
- ShuffleWriterExec, SortShuffleWriterExec, UnorderedRangeRepartitionExec,
- UnresolvedShuffleExec,
+ PartitionGroup, PartitionedBoundedWindowAggExec, PerPartitionFilterExec,
+ RuntimeStatsExec, ShuffleReaderExec, ShuffleWriterExec,
SortShuffleWriterExec,
+ UnorderedRangeRepartitionExec, UnresolvedShuffleExec,
};
use crate::serde::protobuf::{
ballista_logical_plan_node::LogicalPlanType,
@@ -662,6 +662,31 @@ impl PhysicalExtensionCodec for
BallistaPhysicalExtensionCodec {
predicates,
)?))
}
+ PhysicalPlanType::PartitionedBoundedWindowAgg(node) => {
+ let [input] = inputs else {
+ return Err(DataFusionError::Internal(format!(
+ "PartitionedBoundedWindowAggExec expects exactly 1
input, got {}",
+ inputs.len()
+ )));
+ };
+ let input_schema = input.schema();
+ let window_expr = node
+ .window_expr
+ .iter()
+ .map(|we| {
+
datafusion_proto::physical_plan::from_proto::parse_physical_window_expr(
+ we,
+ &decode_ctx,
+ input_schema.as_ref(),
+ &converter,
+ )
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(Arc::new(PartitionedBoundedWindowAggExec::try_new(
+ window_expr,
+ input.clone(),
+ )?))
+ }
}
}
@@ -922,6 +947,31 @@ impl PhysicalExtensionCodec for
BallistaPhysicalExtensionCodec {
))
})?;
Ok(())
+ } else if let Some(exec) =
node.downcast_ref::<PartitionedBoundedWindowAggExec>()
+ {
+ let converter = DefaultPhysicalProtoConverter {};
+ let window_expr = exec
+ .window_expr()
+ .iter()
+ .map(|we| {
+
datafusion_proto::physical_plan::to_proto::serialize_physical_window_expr(
+ we,
+ self.default_codec.as_ref(),
+ &converter,
+ )
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ let proto = protobuf::BallistaPhysicalPlanNode {
+ physical_plan_type:
Some(PhysicalPlanType::PartitionedBoundedWindowAgg(
+ protobuf::PartitionedBoundedWindowAggExecNode {
window_expr },
+ )),
+ };
+ proto.encode(buf).map_err(|e| {
+ DataFusionError::Internal(format!(
+ "failed to encode PartitionedBoundedWindowAggExec: {e:?}"
+ ))
+ })?;
+ Ok(())
} else {
Err(DataFusionError::Internal(format!(
"Unsupported plan node, name: [{}] ",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]