use crate::{
EquivalenceProperties, PhysicalExpr, equivalence::ProjectionMapping,
expressions::UnKnownColumn, physical_exprs_contains, physical_exprs_equal,
};
pub use datafusion_common::SplitPoint;
use datafusion_common::{Result, validate_range_split_points};
use datafusion_physical_expr_common::physical_expr::format_physical_expr_list;
use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
#[cfg(feature = "proto")]
use datafusion_physical_expr_common::sort_expr::{
sort_exprs_try_from_proto, sort_exprs_try_to_proto,
};
use std::fmt;
use std::fmt::Display;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum Partitioning {
RoundRobinBatch(usize),
Hash(Vec<Arc<dyn PhysicalExpr>>, usize),
Range(RangePartitioning),
UnknownPartitioning(usize),
}
impl Display for Partitioning {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Partitioning::RoundRobinBatch(size) => write!(f, "RoundRobinBatch({size})"),
Partitioning::Hash(phy_exprs, size) => {
let phy_exprs_str = phy_exprs
.iter()
.map(|e| format!("{e}"))
.collect::<Vec<String>>()
.join(", ");
write!(f, "Hash([{phy_exprs_str}], {size})")
}
Partitioning::Range(range) => write!(f, "{range}"),
Partitioning::UnknownPartitioning(size) => {
write!(f, "UnknownPartitioning({size})")
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RangePartitioning {
ordering: LexOrdering,
split_points: Vec<SplitPoint>,
}
impl RangePartitioning {
pub fn new(ordering: LexOrdering, split_points: Vec<SplitPoint>) -> Self {
Self {
ordering,
split_points,
}
}
pub fn try_new(ordering: LexOrdering, split_points: Vec<SplitPoint>) -> Result<Self> {
validate_range_split_points(
&split_points,
&ordering
.iter()
.map(|sort_expr| sort_expr.options)
.collect::<Vec<_>>(),
)?;
Ok(Self::new(ordering, split_points))
}
pub fn ordering(&self) -> &LexOrdering {
&self.ordering
}
pub fn split_points(&self) -> &[SplitPoint] {
&self.split_points
}
pub fn partition_count(&self) -> usize {
self.split_points.len() + 1
}
fn project(
&self,
mapping: &ProjectionMapping,
input_eq_properties: &EquivalenceProperties,
) -> Option<Self> {
let exprs = self
.ordering
.iter()
.map(|sort_expr| Arc::clone(&sort_expr.expr))
.collect::<Vec<_>>();
let projected_exprs = input_eq_properties
.project_expressions(&exprs, mapping)
.collect::<Option<Vec<_>>>()?;
let sort_exprs = self
.ordering
.iter()
.zip(projected_exprs)
.map(|(sort_expr, expr)| PhysicalSortExpr::new(expr, sort_expr.options))
.collect::<Vec<_>>();
let ordering = LexOrdering::new(sort_exprs)?;
if ordering.len() != self.ordering.len() {
return None;
}
Some(Self {
ordering,
split_points: self.split_points.clone(),
})
}
}
impl Display for RangePartitioning {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let split_points = format_range_split_points(&self.split_points);
write!(
f,
"Range([{}], [{}], {})",
self.ordering,
split_points,
self.partition_count()
)
}
}
fn format_range_split_points(split_points: &[SplitPoint]) -> String {
split_points
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
fn equivalent_exprs(
left: &[Arc<dyn PhysicalExpr>],
right: &[Arc<dyn PhysicalExpr>],
eq_properties: &EquivalenceProperties,
) -> bool {
if physical_exprs_equal(left, right) {
return true;
}
let eq_groups = eq_properties.eq_group();
if eq_groups.is_empty() {
return false;
}
let normalized_left = normalize_exprs(left, eq_properties);
let normalized_right = normalize_exprs(right, eq_properties);
physical_exprs_equal(&normalized_left, &normalized_right)
}
fn normalize_exprs(
exprs: &[Arc<dyn PhysicalExpr>],
eq_properties: &EquivalenceProperties,
) -> Vec<Arc<dyn PhysicalExpr>> {
let eq_groups = eq_properties.eq_group();
exprs
.iter()
.map(|expr| eq_groups.normalize_expr(Arc::clone(expr)))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartitioningSatisfaction {
NotSatisfied,
Exact,
Subset,
}
impl PartitioningSatisfaction {
pub fn is_satisfied(&self) -> bool {
matches!(self, Self::Exact | Self::Subset)
}
pub fn is_subset(&self) -> bool {
*self == Self::Subset
}
}
impl Partitioning {
pub fn partition_count(&self) -> usize {
use Partitioning::*;
match self {
RoundRobinBatch(n) | Hash(_, n) | UnknownPartitioning(n) => *n,
Range(range) => range.partition_count(),
}
}
fn is_subset_partitioning(
subset_exprs: &[Arc<dyn PhysicalExpr>],
superset_exprs: &[Arc<dyn PhysicalExpr>],
) -> bool {
if subset_exprs.is_empty() || subset_exprs.len() >= superset_exprs.len() {
return false;
}
subset_exprs
.iter()
.all(|subset_expr| physical_exprs_contains(superset_exprs, subset_expr))
}
#[deprecated(since = "52.0.0", note = "Use satisfaction instead")]
pub fn satisfy(
&self,
required: &Distribution,
eq_properties: &EquivalenceProperties,
) -> bool {
self.satisfaction(required, eq_properties, false)
== PartitioningSatisfaction::Exact
}
#[expect(
deprecated,
reason = "HashPartitioned is accepted during the KeyPartitioned migration"
)]
pub fn satisfaction(
&self,
required: &Distribution,
eq_properties: &EquivalenceProperties,
allow_subset: bool,
) -> PartitioningSatisfaction {
match required {
Distribution::UnspecifiedDistribution => PartitioningSatisfaction::Exact,
Distribution::SinglePartition if self.partition_count() == 1 => {
PartitioningSatisfaction::Exact
}
Distribution::HashPartitioned(_) | Distribution::KeyPartitioned(_)
if self.partition_count() == 1 =>
{
PartitioningSatisfaction::Exact
}
Distribution::HashPartitioned(required_exprs)
| Distribution::KeyPartitioned(required_exprs) => match self {
Partitioning::Hash(partition_exprs, _) => Self::key_satisfaction(
partition_exprs,
required_exprs,
eq_properties,
allow_subset,
),
Partitioning::Range(range) => {
let partition_exprs = range
.ordering()
.iter()
.map(|sort_expr| Arc::clone(&sort_expr.expr))
.collect::<Vec<_>>();
Self::key_satisfaction(
&partition_exprs,
required_exprs,
eq_properties,
allow_subset,
)
}
Partitioning::RoundRobinBatch(_)
| Partitioning::UnknownPartitioning(_) => {
PartitioningSatisfaction::NotSatisfied
}
},
Distribution::SinglePartition => PartitioningSatisfaction::NotSatisfied,
}
}
fn key_satisfaction(
partition_exprs: &[Arc<dyn PhysicalExpr>],
required_exprs: &[Arc<dyn PhysicalExpr>],
eq_properties: &EquivalenceProperties,
allow_subset: bool,
) -> PartitioningSatisfaction {
if partition_exprs.is_empty() || required_exprs.is_empty() {
return PartitioningSatisfaction::NotSatisfied;
}
if equivalent_exprs(required_exprs, partition_exprs, eq_properties) {
return PartitioningSatisfaction::Exact;
}
let eq_groups = eq_properties.eq_group();
if !eq_groups.is_empty() {
if allow_subset {
let normalized_partition_exprs =
normalize_exprs(partition_exprs, eq_properties);
let normalized_required_exprs =
normalize_exprs(required_exprs, eq_properties);
if Self::is_subset_partitioning(
&normalized_partition_exprs,
&normalized_required_exprs,
) {
return PartitioningSatisfaction::Subset;
}
}
} else if allow_subset
&& Self::is_subset_partitioning(partition_exprs, required_exprs)
{
return PartitioningSatisfaction::Subset;
}
PartitioningSatisfaction::NotSatisfied
}
pub fn project(
&self,
mapping: &ProjectionMapping,
input_eq_properties: &EquivalenceProperties,
) -> Self {
match self {
Partitioning::Hash(exprs, part) => {
let normalized_exprs = input_eq_properties
.project_expressions(exprs, mapping)
.zip(exprs)
.map(|(proj_expr, expr)| {
proj_expr.unwrap_or_else(|| {
Arc::new(UnKnownColumn::new(&expr.to_string()))
})
})
.collect();
Partitioning::Hash(normalized_exprs, *part)
}
Partitioning::Range(range) => {
if let Some(projected) = range.project(mapping, input_eq_properties) {
Partitioning::Range(projected)
} else {
Partitioning::UnknownPartitioning(range.partition_count())
}
}
Partitioning::RoundRobinBatch(_) | Partitioning::UnknownPartitioning(_) => {
self.clone()
}
}
}
}
#[cfg(feature = "proto")]
impl Partitioning {
pub fn try_to_proto(
&self,
ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
) -> Result<datafusion_proto_models::protobuf::Partitioning> {
use datafusion_proto_models::protobuf;
let partition_method = match self {
Partitioning::RoundRobinBatch(n) => {
protobuf::partitioning::PartitionMethod::RoundRobin(wire_partition_count(
*n,
)?)
}
Partitioning::Hash(exprs, n) => {
protobuf::partitioning::PartitionMethod::Hash(
protobuf::PhysicalHashRepartition {
hash_expr: ctx.encode_children_expressions(exprs)?,
partition_count: wire_partition_count(*n)?,
},
)
}
Partitioning::Range(range) => {
let sort_expr = sort_exprs_try_to_proto(range.ordering().iter(), ctx)?;
let split_point = range
.split_points()
.iter()
.map(|split_point| {
let value = split_point
.values()
.iter()
.map(|value| value.try_into().map_err(Into::into))
.collect::<Result<Vec<_>>>()?;
Ok(protobuf::PhysicalRangeSplitPoint { value })
})
.collect::<Result<Vec<_>>>()?;
protobuf::partitioning::PartitionMethod::Range(
protobuf::PhysicalRangePartitioning {
sort_expr,
split_point,
},
)
}
Partitioning::UnknownPartitioning(n) => {
protobuf::partitioning::PartitionMethod::Unknown(wire_partition_count(
*n,
)?)
}
};
Ok(protobuf::Partitioning {
partition_method: Some(partition_method),
})
}
pub fn try_from_proto(
node: &datafusion_proto_models::protobuf::Partitioning,
ctx: &datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx<'_>,
) -> Result<Option<Self>> {
use datafusion_common::{ScalarValue, internal_datafusion_err, internal_err};
use datafusion_proto_models::protobuf;
let Some(partition_method) = node.partition_method.as_ref() else {
return Ok(None);
};
let partitioning = match partition_method {
protobuf::partitioning::PartitionMethod::RoundRobin(n) => {
Partitioning::RoundRobinBatch(partition_count(*n)?)
}
protobuf::partitioning::PartitionMethod::Hash(hash) => {
let exprs = hash
.hash_expr
.iter()
.map(|expr| ctx.decode(expr))
.collect::<Result<Vec<_>>>()?;
Partitioning::Hash(exprs, partition_count(hash.partition_count)?)
}
protobuf::partitioning::PartitionMethod::Unknown(n) => {
Partitioning::UnknownPartitioning(partition_count(*n)?)
}
protobuf::partitioning::PartitionMethod::Range(range) => {
let sort_exprs = sort_exprs_try_from_proto(&range.sort_expr, ctx)?;
let sort_expr_count = sort_exprs.len();
let ordering = LexOrdering::new(sort_exprs).ok_or_else(|| {
internal_datafusion_err!(
"Range partitioning requires non-empty ordering"
)
})?;
if ordering.len() != sort_expr_count {
return internal_err!(
"Range partitioning ordering must not contain duplicate expressions"
);
}
let split_points = range
.split_point
.iter()
.map(|split_point| {
let values = split_point
.value
.iter()
.map(|value| ScalarValue::try_from(value).map_err(Into::into))
.collect::<Result<Vec<_>>>()?;
Ok(SplitPoint::new(values))
})
.collect::<Result<Vec<_>>>()?;
Partitioning::Range(RangePartitioning::try_new(ordering, split_points)?)
}
};
Ok(Some(partitioning))
}
}
#[cfg(feature = "proto")]
fn partition_count(count: u64) -> Result<usize> {
usize::try_from(count).map_err(|_| {
datafusion_common::internal_datafusion_err!(
"Partition count {count} exceeds usize::MAX"
)
})
}
#[cfg(feature = "proto")]
fn wire_partition_count(count: usize) -> Result<u64> {
u64::try_from(count).map_err(|_| {
datafusion_common::internal_datafusion_err!(
"Partition count {count} exceeds u64::MAX"
)
})
}
impl PartialEq for Partitioning {
fn eq(&self, other: &Partitioning) -> bool {
match (self, other) {
(
Partitioning::RoundRobinBatch(count1),
Partitioning::RoundRobinBatch(count2),
) if count1 == count2 => true,
(Partitioning::Hash(exprs1, count1), Partitioning::Hash(exprs2, count2))
if physical_exprs_equal(exprs1, exprs2) && (count1 == count2) =>
{
true
}
(Partitioning::Range(left), Partitioning::Range(right)) => left == right,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub enum Distribution {
UnspecifiedDistribution,
SinglePartition,
#[deprecated(since = "55.0.0", note = "Use Distribution::KeyPartitioned")]
HashPartitioned(Vec<Arc<dyn PhysicalExpr>>),
KeyPartitioned(Vec<Arc<dyn PhysicalExpr>>),
}
#[expect(
deprecated,
reason = "HashPartitioned is accepted during the KeyPartitioned migration"
)]
impl Distribution {
pub fn create_partitioning(self, partition_count: usize) -> Partitioning {
match self {
Distribution::UnspecifiedDistribution => {
Partitioning::UnknownPartitioning(partition_count)
}
Distribution::SinglePartition => Partitioning::UnknownPartitioning(1),
Distribution::HashPartitioned(expr) | Distribution::KeyPartitioned(expr) => {
Partitioning::Hash(expr, partition_count)
}
}
}
}
#[expect(
deprecated,
reason = "HashPartitioned display is preserved during the KeyPartitioned migration"
)]
impl Display for Distribution {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Distribution::UnspecifiedDistribution => write!(f, "Unspecified"),
Distribution::SinglePartition => write!(f, "SinglePartition"),
Distribution::HashPartitioned(exprs) => {
write!(f, "HashPartitioned[{}])", format_physical_expr_list(exprs))
}
Distribution::KeyPartitioned(exprs) => {
write!(f, "KeyPartitioned[{}])", format_physical_expr_list(exprs))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expressions::Column;
use crate::projection::ProjectionTargets;
use arrow::compute::SortOptions;
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion_common::{Result, ScalarValue};
struct PartitioningTestFixture {
schema: SchemaRef,
cols: Vec<Arc<dyn PhysicalExpr>>,
eq_properties: EquivalenceProperties,
}
impl PartitioningTestFixture {
fn new(fields: Vec<(&str, DataType)>) -> Result<Self> {
let schema = Arc::new(Schema::new(
fields
.iter()
.map(|(name, data_type)| Field::new(*name, data_type.clone(), false))
.collect::<Vec<_>>(),
));
let cols = fields
.iter()
.map(|(name, _)| {
Ok(Arc::new(Column::new_with_schema(name, &schema)?)
as Arc<dyn PhysicalExpr>)
})
.collect::<Result<_>>()?;
let eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
Ok(Self {
schema,
cols,
eq_properties,
})
}
fn int64(names: &[&str]) -> Result<Self> {
Self::new(names.iter().map(|name| (*name, DataType::Int64)).collect())
}
fn col(&self, index: usize) -> Arc<dyn PhysicalExpr> {
Arc::clone(&self.cols[index])
}
fn cols(
&self,
indices: impl IntoIterator<Item = usize>,
) -> Vec<Arc<dyn PhysicalExpr>> {
indices.into_iter().map(|index| self.col(index)).collect()
}
fn hash_partitioning(
&self,
indices: impl IntoIterator<Item = usize>,
partition_count: usize,
) -> Partitioning {
Partitioning::Hash(self.cols(indices), partition_count)
}
fn key_distribution(
&self,
indices: impl IntoIterator<Item = usize>,
) -> Distribution {
Distribution::KeyPartitioned(self.cols(indices))
}
fn range_sort_expr(
&self,
index: usize,
options: SortOptions,
) -> PhysicalSortExpr {
PhysicalSortExpr::new(self.col(index), options)
}
fn range_ordering(
&self,
indices: impl IntoIterator<Item = usize>,
) -> LexOrdering {
LexOrdering::new(
indices
.into_iter()
.map(|index| PhysicalSortExpr::new_default(self.col(index))),
)
.expect("ordering must not be empty")
}
fn range(
&self,
indices: impl IntoIterator<Item = usize>,
split_points: Vec<SplitPoint>,
) -> RangePartitioning {
RangePartitioning::try_new(self.range_ordering(indices), split_points)
.expect("test range partitioning should be valid")
}
fn range_partitioning(
&self,
indices: impl IntoIterator<Item = usize>,
split_points: Vec<SplitPoint>,
) -> Partitioning {
Partitioning::Range(self.range(indices, split_points))
}
fn range_partitioning_with_ordering(
&self,
ordering: LexOrdering,
split_points: Vec<SplitPoint>,
) -> Partitioning {
Partitioning::Range(
RangePartitioning::try_new(ordering, split_points)
.expect("test range partitioning should be valid"),
)
}
}
fn assert_satisfaction(
desc: &str,
partitioning: &Partitioning,
required: &Distribution,
eq_properties: &EquivalenceProperties,
expected_with_subset: PartitioningSatisfaction,
expected_without_subset: PartitioningSatisfaction,
) {
assert_eq!(
partitioning.satisfaction(required, eq_properties, true),
expected_with_subset,
"Failed for {desc} with subset enabled"
);
assert_eq!(
partitioning.satisfaction(required, eq_properties, false),
expected_without_subset,
"Failed for {desc} with subset disabled"
);
}
#[test]
#[expect(
deprecated,
reason = "test intentionally covers deprecated HashPartitioned compatibility"
)]
fn partitioning_satisfy_distribution() -> Result<()> {
let fixture = PartitioningTestFixture::new(vec![
("column_1", DataType::Int64),
("column_2", DataType::Utf8),
])?;
let distribution_types = vec![
Distribution::UnspecifiedDistribution,
Distribution::SinglePartition,
Distribution::HashPartitioned(fixture.cols([0, 1])),
fixture.key_distribution([0, 1]),
];
let single_partition = Partitioning::UnknownPartitioning(1);
let unspecified_partition = Partitioning::UnknownPartitioning(10);
let round_robin_partition = Partitioning::RoundRobinBatch(10);
let hash_partition1 = fixture.hash_partitioning([0, 1], 10);
let hash_partition2 = fixture.hash_partitioning([1, 0], 10);
for distribution in distribution_types {
let result = (
single_partition
.satisfaction(&distribution, &fixture.eq_properties, true)
.is_satisfied(),
unspecified_partition
.satisfaction(&distribution, &fixture.eq_properties, true)
.is_satisfied(),
round_robin_partition
.satisfaction(&distribution, &fixture.eq_properties, true)
.is_satisfied(),
hash_partition1
.satisfaction(&distribution, &fixture.eq_properties, true)
.is_satisfied(),
hash_partition2
.satisfaction(&distribution, &fixture.eq_properties, true)
.is_satisfied(),
);
match distribution {
Distribution::UnspecifiedDistribution => {
assert_eq!(result, (true, true, true, true, true))
}
Distribution::SinglePartition => {
assert_eq!(result, (true, false, false, false, false))
}
Distribution::HashPartitioned(_) | Distribution::KeyPartitioned(_) => {
assert_eq!(result, (true, false, false, true, false))
}
}
}
Ok(())
}
#[test]
#[expect(
deprecated,
reason = "test intentionally covers deprecated HashPartitioned compatibility"
)]
fn deprecated_hash_partitioned_matches_key_partitioned() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
let partitioning = fixture.hash_partitioning([0, 1], 4);
let hash_distribution = Distribution::HashPartitioned(fixture.cols([0, 1]));
let key_distribution = fixture.key_distribution([0, 1]);
assert_eq!(
partitioning.satisfaction(&hash_distribution, &fixture.eq_properties, false),
partitioning.satisfaction(&key_distribution, &fixture.eq_properties, false)
);
assert_eq!(
hash_distribution.create_partitioning(4),
key_distribution.create_partitioning(4)
);
Ok(())
}
#[test]
fn hash_partitioning_key_distribution_satisfaction() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b", "c"])?;
let unknown: Arc<dyn PhysicalExpr> = Arc::new(UnKnownColumn::new("dropped"));
let test_cases = vec![
(
"exact: KeyPartitioned([a, b]) satisfied by Hash([a, b])",
fixture.hash_partitioning([0, 1], 4),
fixture.key_distribution([0, 1]),
PartitioningSatisfaction::Exact,
PartitioningSatisfaction::Exact,
),
(
"subset: KeyPartitioned([a, b]) satisfied by Hash([a])",
fixture.hash_partitioning([0], 4),
fixture.key_distribution([0, 1]),
PartitioningSatisfaction::Subset,
PartitioningSatisfaction::NotSatisfied,
),
(
"subset: KeyPartitioned([a, b, c]) satisfied by Hash([b])",
fixture.hash_partitioning([1], 4),
fixture.key_distribution([0, 1, 2]),
PartitioningSatisfaction::Subset,
PartitioningSatisfaction::NotSatisfied,
),
(
"subset reordered: KeyPartitioned([a, b, c]) satisfied by Hash([b, a])",
fixture.hash_partitioning([1, 0], 4),
fixture.key_distribution([0, 1, 2]),
PartitioningSatisfaction::Subset,
PartitioningSatisfaction::NotSatisfied,
),
(
"superset: KeyPartitioned([a]) not satisfied by Hash([a, b])",
fixture.hash_partitioning([0, 1], 4),
fixture.key_distribution([0]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"superset: KeyPartitioned([a, b]) not satisfied by Hash([a, b, c])",
fixture.hash_partitioning([0, 1, 2], 4),
fixture.key_distribution([0, 1]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"partial overlap: KeyPartitioned([a, b]) not satisfied by Hash([a, c])",
fixture.hash_partitioning([0, 2], 4),
fixture.key_distribution([0, 1]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"no overlap: KeyPartitioned([b, c]) not satisfied by Hash([a])",
fixture.hash_partitioning([0], 4),
fixture.key_distribution([1, 2]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"unknown partition expr",
Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
fixture.key_distribution([0, 1]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"unknown required expr",
fixture.hash_partitioning([0, 1], 4),
Distribution::KeyPartitioned(vec![Arc::clone(&unknown)]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"same unknown expr",
Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
Distribution::KeyPartitioned(vec![Arc::clone(&unknown)]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"unknown partition expr is not a valid subset",
Partitioning::Hash(vec![Arc::clone(&unknown)], 4),
Distribution::KeyPartitioned(vec![Arc::clone(&unknown), fixture.col(0)]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"empty hash partitioning",
Partitioning::Hash(vec![], 4),
fixture.key_distribution([0]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
(
"empty key distribution",
fixture.hash_partitioning([0], 4),
Distribution::KeyPartitioned(vec![]),
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
),
];
for (desc, partition, required, expected_with_subset, expected_without_subset) in
test_cases
{
assert_satisfaction(
desc,
&partition,
&required,
&fixture.eq_properties,
expected_with_subset,
expected_without_subset,
);
}
Ok(())
}
fn int_split_point(values: impl IntoIterator<Item = i64>) -> SplitPoint {
SplitPoint::new(
values
.into_iter()
.map(|value| ScalarValue::Int64(Some(value)))
.collect(),
)
}
fn assert_range_try_new_error(
ordering: LexOrdering,
split_points: Vec<SplitPoint>,
expected: &str,
) {
let error = RangePartitioning::try_new(ordering, split_points)
.unwrap_err()
.to_string();
assert!(error.contains(expected), "{error}");
}
#[test]
fn test_range_partitioning_metadata() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
let range_partitioning =
fixture.range([0], vec![int_split_point([10]), int_split_point([20])]);
assert_eq!(range_partitioning.ordering()[0].to_string(), "a@0 ASC");
assert_eq!(
range_partitioning.split_points(),
&[int_split_point([10]), int_split_point([20])]
);
let partitioning = Partitioning::Range(range_partitioning);
assert_eq!(partitioning.partition_count(), 3);
assert_eq!(
partitioning.to_string(),
"Range([a@0 ASC], [(10), (20)], 3)"
);
Ok(())
}
#[test]
fn test_range_partitioning_try_new_validates_split_points() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
let asc_a = fixture.range_ordering([0]);
let ordering_ab = fixture.range_ordering([0, 1]);
assert_range_try_new_error(
ordering_ab.clone(),
vec![int_split_point([10])],
"split point 0 has width 1, but ordering has width 2",
);
RangePartitioning::try_new(
[fixture.range_sort_expr(0, SortOptions::new(true, false))].into(),
vec![int_split_point([20]), int_split_point([10])],
)?;
assert_range_try_new_error(
asc_a,
vec![int_split_point([20]), int_split_point([10])],
"split points must be strictly ordered",
);
assert_range_try_new_error(
[fixture.range_sort_expr(0, SortOptions::new(false, false))].into(),
vec![
SplitPoint::new(vec![ScalarValue::Int64(None)]),
int_split_point([10]),
],
"split points must be strictly ordered",
);
RangePartitioning::try_new(
ordering_ab.clone(),
vec![int_split_point([10, 20]), int_split_point([10, 30])],
)?;
assert_range_try_new_error(
ordering_ab,
vec![int_split_point([10, 30]), int_split_point([10, 20])],
"split points must be strictly ordered",
);
Ok(())
}
#[test]
fn test_range_partitioning_project_preserves_or_degrades() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
let range_partitioning = fixture.range_partitioning_with_ordering(
[fixture.range_sort_expr(1, SortOptions::new(true, false))].into(),
vec![int_split_point([10])],
);
let keep_b_mapping = ProjectionMapping::from_indices(&[1], &fixture.schema)?;
let projected =
range_partitioning.project(&keep_b_mapping, &fixture.eq_properties);
assert_eq!(
projected.to_string(),
"Range([b@0 DESC NULLS LAST], [(10)], 2)"
);
let drop_b_mapping = ProjectionMapping::from_indices(&[0], &fixture.schema)?;
let projected =
range_partitioning.project(&drop_b_mapping, &fixture.eq_properties);
let Partitioning::UnknownPartitioning(partition_count) = projected else {
panic!("expected UnknownPartitioning, got {projected:?}");
};
assert_eq!(partition_count, 2);
Ok(())
}
#[test]
fn test_range_partitioning_project_degrades_if_ordering_collapses() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b"])?;
let target: Arc<dyn PhysicalExpr> = Arc::new(Column::new("x", 0));
let range_partitioning =
fixture.range_partitioning([0, 1], vec![int_split_point([10, 100])]);
let mapping = ProjectionMapping::from_iter([
(
fixture.col(0),
ProjectionTargets::from(vec![(Arc::clone(&target), 0)]),
),
(
fixture.col(1),
ProjectionTargets::from(vec![(Arc::clone(&target), 0)]),
),
]);
let projected = range_partitioning.project(&mapping, &fixture.eq_properties);
let Partitioning::UnknownPartitioning(partition_count) = projected else {
panic!("expected UnknownPartitioning, got {projected:?}");
};
assert_eq!(partition_count, 2);
Ok(())
}
#[test]
fn range_partitioning_key_distribution_satisfaction() -> Result<()> {
let fixture = PartitioningTestFixture::int64(&["a", "b", "c"])?;
let range_a = fixture.range_partitioning([0], vec![int_split_point([10])]);
let range_ab =
fixture.range_partitioning([0, 1], vec![int_split_point([10, 100])]);
assert_satisfaction(
"exact single key",
&range_a,
&fixture.key_distribution([0]),
&fixture.eq_properties,
PartitioningSatisfaction::Exact,
PartitioningSatisfaction::Exact,
);
assert_satisfaction(
"exact compound key",
&range_ab,
&fixture.key_distribution([0, 1]),
&fixture.eq_properties,
PartitioningSatisfaction::Exact,
PartitioningSatisfaction::Exact,
);
assert_satisfaction(
"subset key",
&range_a,
&fixture.key_distribution([0, 1]),
&fixture.eq_properties,
PartitioningSatisfaction::Subset,
PartitioningSatisfaction::NotSatisfied,
);
assert_satisfaction(
"incompatible key",
&range_a,
&fixture.key_distribution([1]),
&fixture.eq_properties,
PartitioningSatisfaction::NotSatisfied,
PartitioningSatisfaction::NotSatisfied,
);
let mut eq_properties = fixture.eq_properties.clone();
eq_properties.add_equal_conditions(fixture.col(0), fixture.col(2))?;
assert_satisfaction(
"equivalent subset key",
&range_a,
&fixture.key_distribution([1, 2]),
&eq_properties,
PartitioningSatisfaction::Subset,
PartitioningSatisfaction::NotSatisfied,
);
let mut eq_properties = fixture.eq_properties.clone();
eq_properties.add_equal_conditions(fixture.col(0), fixture.col(1))?;
assert_satisfaction(
"equivalent exact key",
&range_a,
&fixture.key_distribution([1]),
&eq_properties,
PartitioningSatisfaction::Exact,
PartitioningSatisfaction::Exact,
);
Ok(())
}
}
#[cfg(all(test, feature = "proto"))]
mod ordering_proto_tests {
use std::sync::Arc;
use arrow::compute::SortOptions;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx;
use datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx;
use datafusion_physical_expr_common::sort_expr::{
LexRequirement, PhysicalSortExpr, PhysicalSortRequirement,
sort_exprs_try_from_proto, sort_exprs_try_to_proto,
};
use crate::expressions::Column;
use crate::proto_test_util::{StubDecoder, StubEncoder};
fn schema() -> Schema {
Schema::new(vec![Field::new("a", DataType::Int32, false)])
}
fn sort_expr(descending: bool, nulls_first: bool) -> PhysicalSortExpr {
PhysicalSortExpr::new(
Arc::new(Column::new("a", 0)),
SortOptions {
descending,
nulls_first,
},
)
}
#[test]
fn sort_exprs_round_trip_preserves_options_and_order() {
let encoder = StubEncoder::ok();
let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
let exprs = vec![sort_expr(true, false), sort_expr(false, true)];
let nodes = sort_exprs_try_to_proto(&exprs, &encode_ctx).unwrap();
assert_eq!(
nodes
.iter()
.map(|node| (node.asc, node.nulls_first))
.collect::<Vec<_>>(),
vec![(false, false), (true, true)]
);
let schema = schema();
let decoder = StubDecoder::ok();
let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
let decoded = sort_exprs_try_from_proto(&nodes, &decode_ctx).unwrap();
assert_eq!(
decoded.iter().map(|expr| expr.options).collect::<Vec<_>>(),
exprs.iter().map(|expr| expr.options).collect::<Vec<_>>()
);
}
#[test]
fn sort_exprs_accepts_owned_requirements() {
let encoder = StubEncoder::ok();
let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
let requirement = LexRequirement::from([PhysicalSortRequirement::new(
Arc::new(Column::new("a", 0)),
Some(SortOptions {
descending: true,
nulls_first: true,
}),
)]);
let nodes = sort_exprs_try_to_proto(
requirement
.iter()
.map(|req| PhysicalSortExpr::from(req.clone())),
&encode_ctx,
)
.unwrap();
assert_eq!(nodes.len(), 1);
assert!(!nodes[0].asc);
assert!(nodes[0].nulls_first);
}
#[test]
fn sort_exprs_propagate_encode_errors() {
let encoder = StubEncoder::failing_on(2);
let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
let exprs = vec![sort_expr(false, false), sort_expr(true, true)];
let err = sort_exprs_try_to_proto(&exprs, &encode_ctx).unwrap_err();
assert!(err.to_string().contains("stub encode failure on call 2"));
}
#[test]
fn sort_exprs_reject_missing_inner_expr() {
let encoder = StubEncoder::ok();
let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
let mut nodes =
sort_exprs_try_to_proto(&[sort_expr(false, false)], &encode_ctx).unwrap();
nodes[0].expr = None;
let schema = schema();
let decoder = StubDecoder::ok();
let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
let err = sort_exprs_try_from_proto(&nodes, &decode_ctx).unwrap_err();
assert!(
err.to_string()
.contains("PhysicalSortExpr is missing required field 'expr'")
);
}
}
#[cfg(all(test, feature = "proto"))]
mod partition_count_proto_tests {
use std::sync::Arc;
use arrow::datatypes::{DataType, Field, Schema};
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
use datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx;
use datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx;
use datafusion_proto_models::protobuf;
use super::{Partitioning, partition_count, wire_partition_count};
use crate::expressions::Column;
use crate::proto_test_util::{StubDecoder, StubEncoder, column_node};
fn partitioning_node(
method: protobuf::partitioning::PartitionMethod,
) -> protobuf::Partitioning {
protobuf::Partitioning {
partition_method: Some(method),
}
}
fn counted_methods(count: u64) -> Vec<protobuf::partitioning::PartitionMethod> {
use protobuf::partitioning::PartitionMethod;
vec![
PartitionMethod::RoundRobin(count),
PartitionMethod::Unknown(count),
PartitionMethod::Hash(protobuf::PhysicalHashRepartition {
hash_expr: vec![column_node("a")],
partition_count: count,
}),
]
}
#[test]
fn partition_count_round_trips_at_the_usize_ceiling() {
let wire = wire_partition_count(usize::MAX).unwrap();
assert_eq!(wire, u64::try_from(usize::MAX).unwrap());
assert_eq!(partition_count(wire).unwrap(), usize::MAX);
}
#[test]
fn out_of_range_partition_count_is_reported_not_wrapped() {
let narrowed = partition_count(u64::MAX);
#[cfg(target_pointer_width = "64")]
assert_eq!(narrowed.unwrap(), usize::MAX);
#[cfg(not(target_pointer_width = "64"))]
assert!(
narrowed
.unwrap_err()
.to_string()
.contains("Partition count 18446744073709551615 exceeds usize::MAX")
);
}
#[test]
fn try_from_proto_narrows_every_counted_variant() {
let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
let decoder = StubDecoder::ok();
let decode_ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
for method in counted_methods(u64::MAX) {
let decoded =
Partitioning::try_from_proto(&partitioning_node(method), &decode_ctx);
#[cfg(target_pointer_width = "64")]
assert_eq!(decoded.unwrap().unwrap().partition_count(), usize::MAX);
#[cfg(not(target_pointer_width = "64"))]
assert!(
decoded
.unwrap_err()
.to_string()
.contains("exceeds usize::MAX")
);
}
}
#[test]
fn try_to_proto_widens_every_counted_variant() {
use protobuf::partitioning::PartitionMethod;
let encoder = StubEncoder::ok();
let encode_ctx = PhysicalExprEncodeCtx::new(&encoder);
let hash_key: Arc<dyn PhysicalExpr> = Arc::new(Column::new("a", 0));
let encoded = [
Partitioning::RoundRobinBatch(usize::MAX),
Partitioning::UnknownPartitioning(usize::MAX),
Partitioning::Hash(vec![hash_key], usize::MAX),
]
.iter()
.map(|partitioning| {
match partitioning
.try_to_proto(&encode_ctx)
.unwrap()
.partition_method
{
Some(PartitionMethod::RoundRobin(n) | PartitionMethod::Unknown(n)) => n,
Some(PartitionMethod::Hash(hash)) => hash.partition_count,
other => panic!("expected a counted partition method, got {other:?}"),
}
})
.collect::<Vec<_>>();
assert_eq!(encoded, vec![u64::try_from(usize::MAX).unwrap(); 3]);
}
}