This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git
The following commit(s) were added to refs/heads/main by this push:
new 39e2f23a3f feat: Range Partitioning FFI (#23520)
39e2f23a3f is described below
commit 39e2f23a3fbeebf4eebc3160d91a29c6a7286be7
Author: Saad Tajwar <[email protected]>
AuthorDate: Wed Jul 22 07:01:29 2026 -0400
feat: Range Partitioning FFI (#23520)
## Which issue does this PR close?
- Closes #22394
## Rationale for this change
Exposing range partition metadata via the FFI for external consumers.
## What changes are included in this PR?
- Added FFI mirror struct for `RangePartitioning` and added new enum
variant for range in `FFI_Partitioning`
- For native -> FFI, added match arm for the new variant, same with FFI
-> native but changed the approach of `From` -> `TryFrom` to utilize the
validation for `RangePartitioning` and modified `plan_properties` to
match
- Added tests
## Are these changes tested?
Yes
## Are there any user-facing changes?
Yes, exposing Range partitioning over FFI. This exposes a new `Range`
variant in the `FFI_Partitioning` enum, which may cause consumers of
this enum to add another arm to match statements to handle the new enum.
New `FFI_RangePartitioning` struct for the `Range` variant.
---------
Co-authored-by: Tim Saucer <[email protected]>
---
datafusion/ffi/src/physical_expr/partitioning.rs | 157 ++++++++++++++++++++---
datafusion/ffi/src/plan_properties.rs | 46 ++++++-
2 files changed, 183 insertions(+), 20 deletions(-)
diff --git a/datafusion/ffi/src/physical_expr/partitioning.rs
b/datafusion/ffi/src/physical_expr/partitioning.rs
index eec437639e..2a9a8528c6 100644
--- a/datafusion/ffi/src/physical_expr/partitioning.rs
+++ b/datafusion/ffi/src/physical_expr/partitioning.rs
@@ -17,20 +17,35 @@
use std::sync::Arc;
-use datafusion_physical_expr::Partitioning;
+use datafusion_common::{DataFusionError, ScalarValue, SplitPoint};
+use datafusion_physical_expr::{
+ LexOrdering, Partitioning, PhysicalSortExpr, RangePartitioning,
+};
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
use stabby::vec::Vec as SVec;
+use crate::arrow_wrappers::WrappedArray;
use crate::physical_expr::FFI_PhysicalExpr;
+use crate::physical_expr::sort::FFI_PhysicalSortExpr;
+
+/// A stable struct for sharing [`RangePartitioning`] across FFI boundaries.
+/// See [`RangePartitioning`] for the descriptions of each field.
+#[repr(C)]
+#[derive(Debug)]
+pub struct FFI_RangePartitioning {
+ split_points: SVec<SVec<WrappedArray>>,
+ ordering: SVec<FFI_PhysicalSortExpr>,
+}
/// A stable struct for sharing [`Partitioning`] across FFI boundaries.
-/// See ['Partitioning'] for the meaning of each variant.
+/// See [`Partitioning`] for the meaning of each variant.
#[repr(C)]
#[derive(Debug)]
pub enum FFI_Partitioning {
RoundRobinBatch(usize),
Hash(SVec<FFI_PhysicalExpr>, usize),
UnknownPartitioning(usize),
+ Range(FFI_RangePartitioning),
}
impl From<&Partitioning> for FFI_Partitioning {
@@ -45,49 +60,130 @@ impl From<&Partitioning> for FFI_Partitioning {
.collect();
Self::Hash(exprs, *size)
}
- // FFI does not yet expose range partition metadata.
- // See https://github.com/apache/datafusion/issues/22394
Partitioning::Range(range) => {
- Self::UnknownPartitioning(range.partition_count())
+ // Producer-side conversion should be infallible at ABI
boundary
+ let split_points = range
+ .split_points()
+ .iter()
+ .map(|split_point| {
+ split_point
+ .values()
+ .iter()
+ .map(|value| {
+ WrappedArray::try_from(value).expect(
+ "ScalarValue in RangePartitioning should
convert to WrappedArray",
+ )
+ })
+ .collect()
+ })
+ .collect();
+ let ordering = range
+ .ordering()
+ .iter()
+ .map(FFI_PhysicalSortExpr::from)
+ .collect();
+ Self::Range(FFI_RangePartitioning {
+ split_points,
+ ordering,
+ })
}
Partitioning::UnknownPartitioning(size) =>
Self::UnknownPartitioning(*size),
}
}
}
-impl From<&FFI_Partitioning> for Partitioning {
- fn from(value: &FFI_Partitioning) -> Self {
- match value {
+impl TryFrom<FFI_Partitioning> for Partitioning {
+ type Error = DataFusionError;
+
+ fn try_from(value: FFI_Partitioning) -> Result<Self, Self::Error> {
+ Ok(match value {
FFI_Partitioning::RoundRobinBatch(size) => {
- Partitioning::RoundRobinBatch(*size)
+ Partitioning::RoundRobinBatch(size)
}
FFI_Partitioning::Hash(exprs, size) => {
let exprs = exprs.iter().map(<Arc<dyn
PhysicalExpr>>::from).collect();
- Self::Hash(exprs, *size)
+ Self::Hash(exprs, size)
+ }
+ FFI_Partitioning::Range(range) => {
+ let split_points = range
+ .split_points
+ .into_iter()
+ .map(|split_point| {
+ split_point
+ .into_iter()
+ .map(ScalarValue::try_from)
+ .collect::<Result<Vec<_>, _>>()
+ .map(SplitPoint::new)
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let ordering =
+
LexOrdering::new(range.ordering.iter().map(PhysicalSortExpr::from))
+ .ok_or_else(|| {
+ DataFusionError::Internal(
+ "FFI Range partitioning ordering must be non-empty"
+ .to_string(),
+ )
+ })?;
+
+ Self::Range(RangePartitioning::try_new(ordering,
split_points)?)
}
FFI_Partitioning::UnknownPartitioning(size) => {
- Self::UnknownPartitioning(*size)
+ Self::UnknownPartitioning(size)
}
- }
+ })
}
}
#[cfg(test)]
mod tests {
- use datafusion_physical_expr::Partitioning;
- use datafusion_physical_expr::expressions::lit;
+ use std::sync::Arc;
+
+ use arrow_schema::SortOptions;
+ use datafusion_common::{Result, ScalarValue, SplitPoint};
+ use datafusion_physical_expr::expressions::{Column, lit};
+ use datafusion_physical_expr::{
+ LexOrdering, Partitioning, PhysicalSortExpr, RangePartitioning,
+ };
+ use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
+ use stabby::vec::Vec as SVec;
- use crate::physical_expr::partitioning::FFI_Partitioning;
+ use crate::physical_expr::partitioning::{FFI_Partitioning,
FFI_RangePartitioning};
+
+ fn range_partitioning() -> Result<Partitioning> {
+ let a = Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>;
+ let b = Arc::new(Column::new("b", 1)) as Arc<dyn PhysicalExpr>;
+ let ordering = LexOrdering::new([
+ PhysicalSortExpr::new(a, SortOptions::default()),
+ PhysicalSortExpr::new(b, SortOptions::new(true, false)),
+ ])
+ .expect("non-empty ordering");
+ let split_points = vec![
+ SplitPoint::new(vec![
+ ScalarValue::Int64(Some(10)),
+ ScalarValue::Utf8(Some("a".to_string())),
+ ]),
+ SplitPoint::new(vec![
+ ScalarValue::Int64(Some(20)),
+ ScalarValue::Utf8(Some("b".to_string())),
+ ]),
+ ];
+ Ok(Partitioning::Range(RangePartitioning::try_new(
+ ordering,
+ split_points,
+ )?))
+ }
#[test]
- fn round_trip_ffi_partitioning() {
+ fn round_trip_ffi_partitioning() -> Result<()> {
for partitioning in [
Partitioning::RoundRobinBatch(10),
Partitioning::Hash(vec![lit(1)], 10),
Partitioning::UnknownPartitioning(10),
+ range_partitioning()?,
] {
let ffi_partitioning: FFI_Partitioning = (&partitioning).into();
- let returned: Partitioning = (&ffi_partitioning).into();
+ let returned: Partitioning = ffi_partitioning.try_into()?;
if let Partitioning::UnknownPartitioning(return_size) = returned {
let Partitioning::UnknownPartitioning(original_size) =
partitioning
@@ -99,5 +195,32 @@ mod tests {
assert_eq!(partitioning, returned);
}
}
+
+ Ok(())
+ }
+
+ #[test]
+ fn round_trip_ffi_range_partitioning_compound_key() -> Result<()> {
+ let partitioning = range_partitioning()?;
+
+ let ffi_partitioning: FFI_Partitioning = (&partitioning).into();
+ let returned: Partitioning = ffi_partitioning.try_into()?;
+ assert_eq!(partitioning, returned);
+
+ Ok(())
+ }
+
+ #[test]
+ fn ffi_range_partitioning_rejects_empty_ordering() {
+ let ffi_partitioning = FFI_Partitioning::Range(FFI_RangePartitioning {
+ split_points: SVec::new(),
+ ordering: SVec::new(),
+ });
+
+ let err = Partitioning::try_from(ffi_partitioning).unwrap_err();
+ assert!(
+ err.to_string().contains("ordering must be non-empty"),
+ "{err}"
+ );
}
}
diff --git a/datafusion/ffi/src/plan_properties.rs
b/datafusion/ffi/src/plan_properties.rs
index b286ee2d7d..09ef26af32 100644
--- a/datafusion/ffi/src/plan_properties.rs
+++ b/datafusion/ffi/src/plan_properties.rs
@@ -20,7 +20,7 @@ use std::sync::Arc;
use arrow::datatypes::SchemaRef;
use datafusion_common::error::{DataFusionError, Result};
-use datafusion_physical_expr::EquivalenceProperties;
+use datafusion_physical_expr::{EquivalenceProperties, Partitioning};
use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
use datafusion_physical_plan::PlanProperties;
use datafusion_physical_plan::execution_plan::{Boundedness, EmissionType};
@@ -172,6 +172,7 @@ impl TryFrom<FFI_PlanProperties> for PlanProperties {
.unwrap_or_default();
let partitioning = unsafe {
(ffi_props.output_partitioning)(&ffi_props) };
+ let partitioning = Partitioning::try_from(partitioning)?;
let eq_properties = if sort_exprs.is_empty() {
EquivalenceProperties::new(Arc::new(schema))
@@ -187,7 +188,7 @@ impl TryFrom<FFI_PlanProperties> for PlanProperties {
Ok(PlanProperties::new(
eq_properties,
- (&partitioning).into(),
+ partitioning,
emission_type,
boundedness,
))
@@ -260,13 +261,15 @@ impl From<FFI_EmissionType> for EmissionType {
#[cfg(test)]
mod tests {
+ use arrow::datatypes::{DataType, Field, Schema};
use datafusion::physical_expr::PhysicalSortExpr;
use datafusion::physical_plan::Partitioning;
+ use datafusion_common::{ScalarValue, SplitPoint};
+ use datafusion_physical_expr::{LexOrdering, RangePartitioning};
use super::*;
fn create_test_props() -> Result<PlanProperties> {
- use arrow::datatypes::{DataType, Field, Schema};
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Float32,
false)]));
@@ -282,6 +285,25 @@ mod tests {
))
}
+ fn create_range_test_props() -> Result<PlanProperties> {
+ let schema = Arc::new(Schema::new(vec![Field::new("a",
DataType::Int64, false)]));
+ let col = datafusion::physical_plan::expressions::col("a", &schema)?;
+ let ordering = LexOrdering::new([PhysicalSortExpr::new_default(col)])
+ .expect("non-empty ordering");
+ let split_points = vec![
+ SplitPoint::new(vec![ScalarValue::Int64(Some(10))]),
+ SplitPoint::new(vec![ScalarValue::Int64(Some(20))]),
+ ];
+ let range = RangePartitioning::try_new(ordering, split_points)?;
+
+ Ok(PlanProperties::new(
+ EquivalenceProperties::new(schema),
+ Partitioning::Range(range),
+ EmissionType::Incremental,
+ Boundedness::Bounded,
+ ))
+ }
+
#[test]
fn test_round_trip_ffi_plan_properties() -> Result<()> {
let original_props = create_test_props()?;
@@ -314,4 +336,22 @@ mod tests {
Ok(())
}
+
+ #[test]
+ fn test_round_trip_ffi_plan_properties_range_partitioning() -> Result<()> {
+ let original_props = create_range_test_props()?;
+
+ let mut local_props_ptr = FFI_PlanProperties::from(&original_props);
+ local_props_ptr.library_marker_id = crate::mock_foreign_marker_id;
+
+ let foreign_props: PlanProperties = local_props_ptr.try_into()?;
+
+ assert_eq!(
+ format!("{:?}", foreign_props.output_partitioning()),
+ format!("{:?}", original_props.output_partitioning())
+ );
+ assert_eq!(format!("{foreign_props:?}"),
format!("{original_props:?}"));
+
+ Ok(())
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]