Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 114 additions & 4 deletions datafusion/core/tests/physical_optimizer/enforce_distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ use std::ops::Deref;
use std::sync::Arc;

use crate::physical_optimizer::test_utils::{
check_integrity, coalesce_partitions_exec, parquet_exec_with_sort,
parquet_exec_with_stats, repartition_exec, schema, sort_exec,
bounded_window_exec_with_can_repartition, check_integrity, coalesce_partitions_exec,
parquet_exec_with_sort, parquet_exec_with_stats, repartition_exec, schema, sort_exec,
sort_exec_with_preserve_partitioning, sort_merge_join_exec,
sort_preserving_merge_exec, union_exec,
};
Expand Down Expand Up @@ -265,8 +265,12 @@ impl ExecutionPlan for SinglePartitionMaintainsOrderExec {
vec![&self.input]
}

fn required_input_distribution(&self) -> Vec<Distribution> {
vec![Distribution::SinglePartition]
fn input_distribution_requirements(
&self,
) -> datafusion_physical_plan::InputDistributionRequirements {
datafusion_physical_plan::InputDistributionRequirements::new(vec![
Distribution::SinglePartition,
])
}

fn maintains_input_order(&self) -> Vec<bool> {
Expand Down Expand Up @@ -823,6 +827,112 @@ fn range_grouping_set_aggregate_rehashes_with_grouping_id() -> Result<()> {
Ok(())
}

#[test]
fn range_inner_hash_join_rehashes_incompatible_range_partitioning() -> Result<()> {
let left = parquet_exec_with_output_partitioning(range_partitioning(
"a",
[10, 20, 30],
SortOptions::default(),
)?);
let right = projection_exec_with_alias(
parquet_exec_with_output_partitioning(range_partitioning(
"a",
[10, 30, 40],
SortOptions::default(),
)?),
vec![
("a".to_string(), "a1".to_string()),
("b".to_string(), "b1".to_string()),
],
);
let join_on = vec![(
Arc::new(Column::new_with_schema("a", &left.schema())?) as _,
Arc::new(Column::new_with_schema("a1", &right.schema())?) as _,
)];
let join = hash_join_exec(left, right, &join_on, &JoinType::Inner);

let plan = TestConfig::default()
.with_query_execution_partitions(4)
.to_plan(join, &DISTRIB_DISTRIB_SORT);

assert_plan!(
plan,
@r"
HashJoinExec: mode=Partitioned, join_type=Inner, on=[(a@0, a1@0)]
RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=4
DataSourceExec: file_groups={4 groups: [[p0], [p1], [p2], [p3]]}, projection=[a, b, c, d, e], output_partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), file_type=parquet
RepartitionExec: partitioning=Hash([a1@0], 4), input_partitions=4
ProjectionExec: expr=[a@0 as a1, b@1 as b1]
DataSourceExec: file_groups={4 groups: [[p0], [p1], [p2], [p3]]}, projection=[a, b, c, d, e], output_partitioning=Range([a@0 ASC], [(10), (30), (40)], 4), file_type=parquet
"
);

Ok(())
}

#[test]
fn range_window_reuses_range_partitioning() -> Result<()> {
let input = parquet_exec_with_output_partitioning(range_partitioning(
"a",
[10, 20, 30],
SortOptions::default(),
)?);
let window = bounded_window_exec_with_can_repartition(
"a",
vec![],
&[col("a", &schema())?],
input,
true,
);

let plan = TestConfig::default()
.with_query_execution_partitions(4)
.to_plan(window, &DISTRIB_DISTRIB_SORT);

assert_plan!(
plan,
@r#"
BoundedWindowAggExec: wdw=[count: Field { "count": Int64 }, frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
SortExec: expr=[a@0 ASC NULLS LAST], preserve_partitioning=[true]
DataSourceExec: file_groups={4 groups: [[p0], [p1], [p2], [p3]]}, projection=[a, b, c, d, e], output_partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), file_type=parquet
"#
);

Ok(())
}

#[test]
fn range_window_rehashes_incompatible_range_partitioning() -> Result<()> {
let input = parquet_exec_with_output_partitioning(range_partitioning(
"a",
[10, 20, 30],
SortOptions::default(),
)?);
let window = bounded_window_exec_with_can_repartition(
"b",
vec![],
&[col("b", &schema())?],
input,
true,
);

let plan = TestConfig::default()
.with_query_execution_partitions(4)
.to_plan(window, &DISTRIB_DISTRIB_SORT);

assert_plan!(
plan,
@r#"
BoundedWindowAggExec: wdw=[count: Field { "count": Int64 }, frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
SortExec: expr=[b@1 ASC NULLS LAST], preserve_partitioning=[true]
RepartitionExec: partitioning=Hash([b@1], 4), input_partitions=4
DataSourceExec: file_groups={4 groups: [[p0], [p1], [p2], [p3]]}, projection=[a, b, c, d, e], output_partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), file_type=parquet
"#
);

Ok(())
}

#[test]
fn multi_hash_joins() -> Result<()> {
let left = parquet_exec();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -974,8 +974,12 @@ impl ExecutionPlan for MockReqExec {
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.input]
}
fn required_input_distribution(&self) -> Vec<Distribution> {
vec![self.dist.clone()]
fn input_distribution_requirements(
&self,
) -> datafusion_physical_plan::InputDistributionRequirements {
datafusion_physical_plan::InputDistributionRequirements::new(vec![
self.dist.clone(),
])
}
fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
vec![
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,9 @@ fn test_output_req_after_projection() -> Result<()> {
if let Distribution::KeyPartitioned(vec) = after_optimize
.downcast_ref::<OutputRequirementExec>()
.unwrap()
.required_input_distribution()[0]
.input_distribution_requirements()
.child_distribution(0)
.unwrap()
.clone()
{
assert!(
Expand Down
120 changes: 117 additions & 3 deletions datafusion/core/tests/physical_optimizer/sanity_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use insta::assert_snapshot;
use std::sync::Arc;

use crate::physical_optimizer::test_utils::{
bounded_window_exec, global_limit_exec, local_limit_exec, memory_exec,
projection_exec, repartition_exec, sort_exec, sort_expr, sort_expr_options,
bounded_window_exec, bounded_window_exec_with_can_repartition, global_limit_exec,
hash_join_exec, local_limit_exec, memory_exec, projection_exec, repartition_exec,
sort_exec, sort_exec_with_preserve_partitioning, sort_expr, sort_expr_options,
sort_merge_join_exec, sort_preserving_merge_exec, union_exec,
};

Expand All @@ -30,8 +31,8 @@ use datafusion::datasource::stream::{FileStreamProvider, StreamConfig, StreamTab
use datafusion::prelude::{CsvReadOptions, SessionContext};
use datafusion_common::config::ConfigOptions;
use datafusion_common::{JoinType, Result, ScalarValue};
use datafusion_physical_expr::Partitioning;
use datafusion_physical_expr::expressions::{Literal, col};
use datafusion_physical_expr::{Partitioning, RangePartitioning, SplitPoint};
use datafusion_physical_expr_common::sort_expr::LexOrdering;
use datafusion_physical_optimizer::PhysicalOptimizerRule;
use datafusion_physical_optimizer::sanity_checker::SanityCheckPlan;
Expand Down Expand Up @@ -400,6 +401,49 @@ fn assert_sanity_check(plan: &Arc<dyn ExecutionPlan>, is_sane: bool) {
);
}

fn range_partitioned_exec(
schema: &SchemaRef,
key: &str,
split_points: impl IntoIterator<Item = i32>,
) -> Result<Arc<dyn ExecutionPlan>> {
let split_points = split_points
.into_iter()
.map(|value| SplitPoint::new(vec![ScalarValue::Int32(Some(value))]))
.collect();
let partitioning = Partitioning::Range(RangePartitioning::try_new(
[sort_expr(key, schema)].into(),
split_points,
)?);
RepartitionExec::try_new(memory_exec(schema), partitioning)
.map(|exec| Arc::new(exec) as Arc<dyn ExecutionPlan>)
}

#[test]
fn test_partitioned_hash_join_requires_co_partitioned_children() -> Result<()> {
let schema = create_test_schema2();
let join_on = vec![(col("a", &schema)?, col("a", &schema)?)];

let compatible_join = hash_join_exec(
range_partitioned_exec(&schema, "a", [10])?,
range_partitioned_exec(&schema, "a", [10])?,
join_on.clone(),
None,
&JoinType::Inner,
)?;
assert_sanity_check(&compatible_join, true);

let incompatible_join = hash_join_exec(
range_partitioned_exec(&schema, "a", [10])?,
range_partitioned_exec(&schema, "a", [20])?,
join_on,
None,
&JoinType::Inner,
)?;
assert_sanity_check(&incompatible_join, false);

Ok(())
}

#[tokio::test]
/// Tests that plan is valid when the sort requirements are satisfied.
async fn test_bounded_window_agg_sort_requirement() -> Result<()> {
Expand Down Expand Up @@ -458,6 +502,76 @@ async fn test_bounded_window_agg_no_sort_requirement() -> Result<()> {
Ok(())
}

#[tokio::test]
/// Tests that a window over a compatible range-partitioned input satisfies
/// the window's key distribution requirement without a hash repartition.
async fn test_bounded_window_agg_range_partitioning() -> Result<()> {
let schema = create_test_schema2();
let source = range_partitioned_exec(&schema, "a", [10, 20, 30])?;
let ordering: LexOrdering = [sort_expr_options(
"a",
&schema,
SortOptions {
descending: false,
nulls_first: false,
},
)]
.into();
let partition_by = vec![col("a", &schema)?];
let sort = sort_exec_with_preserve_partitioning(ordering, source);
let bw =
bounded_window_exec_with_can_repartition("a", vec![], &partition_by, sort, true);
let plan_str = displayable(bw.as_ref()).indent(true).to_string();
let actual = plan_str.trim();
assert_snapshot!(
actual,
@r#"
BoundedWindowAggExec: wdw=[count: Field { "count": Int64 }, frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
SortExec: expr=[a@0 ASC NULLS LAST], preserve_partitioning=[true]
RepartitionExec: partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), input_partitions=1
DataSourceExec: partitions=1, partition_sizes=[0]
"#
);
assert_sanity_check(&bw, true);
Ok(())
}

#[tokio::test]
/// Tests that a window over an incompatible range-partitioned input fails
/// the window's key distribution requirement.
async fn test_bounded_window_agg_incompatible_range_partitioning() -> Result<()> {
let schema = create_test_schema2();
let source = range_partitioned_exec(&schema, "a", [10, 20, 30])?;
let ordering: LexOrdering = [sort_expr_options(
"b",
&schema,
SortOptions {
descending: false,
nulls_first: false,
},
)]
.into();
let partition_by = vec![col("b", &schema)?];
let sort = sort_exec_with_preserve_partitioning(ordering, source);
let bw =
bounded_window_exec_with_can_repartition("b", vec![], &partition_by, sort, true);
let plan_str = displayable(bw.as_ref()).indent(true).to_string();
let actual = plan_str.trim();
assert_snapshot!(
actual,
@r#"
BoundedWindowAggExec: wdw=[count: Field { "count": Int64 }, frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
SortExec: expr=[b@1 ASC NULLS LAST], preserve_partitioning=[true]
RepartitionExec: partitioning=Range([a@0 ASC], [(10), (20), (30)], 4), input_partitions=1
DataSourceExec: partitions=1, partition_sizes=[0]
"#
);
// Range([a]) does not colocate `b` values, so the window's key
// distribution requirement is not satisfied.
assert_sanity_check(&bw, false);
Ok(())
}

#[tokio::test]
/// A valid when a single partition requirement
/// is satisfied.
Expand Down
18 changes: 17 additions & 1 deletion datafusion/core/tests/physical_optimizer/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,22 @@ pub fn bounded_window_exec_with_partition(
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
partition_by: &[Arc<dyn PhysicalExpr>],
input: Arc<dyn ExecutionPlan>,
) -> Arc<dyn ExecutionPlan> {
bounded_window_exec_with_can_repartition(
col_name,
sort_exprs,
partition_by,
input,
false,
)
}

pub fn bounded_window_exec_with_can_repartition(
col_name: &str,
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
partition_by: &[Arc<dyn PhysicalExpr>],
input: Arc<dyn ExecutionPlan>,
can_repartition: bool,
) -> Arc<dyn ExecutionPlan> {
let sort_exprs = sort_exprs.into_iter().collect::<Vec<_>>();
let schema = input.schema();
Expand All @@ -296,7 +312,7 @@ pub fn bounded_window_exec_with_partition(
vec![window_expr],
Arc::clone(&input),
InputOrderMode::Sorted,
false,
can_repartition,
)
.unwrap(),
)
Expand Down
8 changes: 6 additions & 2 deletions datafusion/core/tests/user_defined/user_defined_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,12 @@ impl ExecutionPlan for TopKExec {
&self.cache
}

fn required_input_distribution(&self) -> Vec<Distribution> {
vec![Distribution::SinglePartition]
fn input_distribution_requirements(
&self,
) -> datafusion_physical_plan::InputDistributionRequirements {
datafusion_physical_plan::InputDistributionRequirements::new(vec![
Distribution::SinglePartition,
])
}

fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
Expand Down
14 changes: 11 additions & 3 deletions datafusion/datasource/src/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ use datafusion_physical_expr_common::sort_expr::{LexRequirement, OrderingRequire
use datafusion_physical_plan::metrics::MetricsSet;
use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
use datafusion_physical_plan::{
DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, Partitioning,
PlanProperties, SendableRecordBatchStream, execute_input_stream,
DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
InputDistributionRequirements, Partitioning, PlanProperties,
SendableRecordBatchStream, execute_input_stream,
};

use async_trait::async_trait;
Expand Down Expand Up @@ -189,9 +190,16 @@ impl ExecutionPlan for DataSinkExec {
}

fn required_input_distribution(&self) -> Vec<Distribution> {
self.input_distribution_requirements().into_per_child()
}

fn input_distribution_requirements(&self) -> InputDistributionRequirements {
// DataSink is responsible for dynamically partitioning its
// own input at execution time, and so requires a single input partition.
vec![Distribution::SinglePartition; self.children().len()]
InputDistributionRequirements::new(vec![
Distribution::SinglePartition;
self.children().len()
])
}

fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
Expand Down
Loading
Loading