use crate::config::ConfigOptions;
use crate::error::Result;
use crate::physical_optimizer::sort_pushdown::{pushdown_sorts, SortPushDown};
use crate::physical_optimizer::utils::{
add_sort_above, find_indices, is_coalesce_partitions, is_limit, is_repartition,
is_sort, is_sort_preserving_merge, is_sorted, is_union, is_window,
merge_and_order_indices, set_difference,
};
use crate::physical_optimizer::PhysicalOptimizerRule;
use crate::physical_plan::coalesce_partitions::CoalescePartitionsExec;
use crate::physical_plan::sorts::sort::SortExec;
use crate::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
use crate::physical_plan::windows::{
BoundedWindowAggExec, PartitionSearchMode, WindowAggExec,
};
use crate::physical_plan::{with_new_children_if_necessary, Distribution, ExecutionPlan};
use arrow::datatypes::SchemaRef;
use datafusion_common::tree_node::{Transformed, TreeNode, VisitRecursion};
use datafusion_common::utils::{get_at_indices, longest_consecutive_prefix};
use datafusion_common::DataFusionError;
use datafusion_physical_expr::utils::{
convert_to_expr, get_indices_of_matching_exprs, ordering_satisfy,
ordering_satisfy_requirement_concrete,
};
use datafusion_physical_expr::{PhysicalExpr, PhysicalSortExpr, PhysicalSortRequirement};
use itertools::{concat, izip, Itertools};
use std::sync::Arc;
#[derive(Default)]
pub struct EnforceSorting {}
impl EnforceSorting {
#[allow(missing_docs)]
pub fn new() -> Self {
Self {}
}
}
#[derive(Debug, Clone)]
struct ExecTree {
pub plan: Arc<dyn ExecutionPlan>,
pub idx: usize,
pub children: Vec<ExecTree>,
}
impl ExecTree {
pub fn new(
plan: Arc<dyn ExecutionPlan>,
idx: usize,
children: Vec<ExecTree>,
) -> Self {
ExecTree {
plan,
idx,
children,
}
}
fn get_leaves(&self) -> Vec<Arc<dyn ExecutionPlan>> {
if self.children.is_empty() {
vec![self.plan.clone()]
} else {
concat(self.children.iter().map(|e| e.get_leaves()))
}
}
}
#[derive(Debug, Clone)]
struct PlanWithCorrespondingSort {
plan: Arc<dyn ExecutionPlan>,
sort_onwards: Vec<Option<ExecTree>>,
}
impl PlanWithCorrespondingSort {
pub fn new(plan: Arc<dyn ExecutionPlan>) -> Self {
let length = plan.children().len();
PlanWithCorrespondingSort {
plan,
sort_onwards: vec![None; length],
}
}
pub fn new_from_children_nodes(
children_nodes: Vec<PlanWithCorrespondingSort>,
parent_plan: Arc<dyn ExecutionPlan>,
) -> Result<Self> {
let children_plans = children_nodes
.iter()
.map(|item| item.plan.clone())
.collect::<Vec<_>>();
let sort_onwards = children_nodes
.into_iter()
.enumerate()
.map(|(idx, item)| {
let plan = &item.plan;
if is_sort(plan) {
return Some(ExecTree::new(item.plan, idx, vec![]));
} else if is_limit(plan) {
return None;
}
let is_spm = is_sort_preserving_merge(plan);
let required_orderings = plan.required_input_ordering();
let flags = plan.maintains_input_order();
let children = izip!(flags, item.sort_onwards, required_orderings)
.filter_map(|(maintains, element, required_ordering)| {
if (required_ordering.is_none() && maintains) || is_spm {
element
} else {
None
}
})
.collect::<Vec<ExecTree>>();
if !children.is_empty() {
Some(ExecTree::new(item.plan, idx, children))
} else {
None
}
})
.collect();
let plan = with_new_children_if_necessary(parent_plan, children_plans)?.into();
Ok(PlanWithCorrespondingSort { plan, sort_onwards })
}
pub fn children(&self) -> Vec<PlanWithCorrespondingSort> {
self.plan
.children()
.into_iter()
.map(|child| PlanWithCorrespondingSort::new(child))
.collect()
}
}
impl TreeNode for PlanWithCorrespondingSort {
fn apply_children<F>(&self, op: &mut F) -> Result<VisitRecursion>
where
F: FnMut(&Self) -> Result<VisitRecursion>,
{
let children = self.children();
for child in children {
match op(&child)? {
VisitRecursion::Continue => {}
VisitRecursion::Skip => return Ok(VisitRecursion::Continue),
VisitRecursion::Stop => return Ok(VisitRecursion::Stop),
}
}
Ok(VisitRecursion::Continue)
}
fn map_children<F>(self, transform: F) -> Result<Self>
where
F: FnMut(Self) -> Result<Self>,
{
let children = self.children();
if children.is_empty() {
Ok(self)
} else {
let children_nodes = children
.into_iter()
.map(transform)
.collect::<Result<Vec<_>>>()?;
PlanWithCorrespondingSort::new_from_children_nodes(children_nodes, self.plan)
}
}
}
#[derive(Debug, Clone)]
struct PlanWithCorrespondingCoalescePartitions {
plan: Arc<dyn ExecutionPlan>,
coalesce_onwards: Vec<Option<ExecTree>>,
}
impl PlanWithCorrespondingCoalescePartitions {
pub fn new(plan: Arc<dyn ExecutionPlan>) -> Self {
let length = plan.children().len();
PlanWithCorrespondingCoalescePartitions {
plan,
coalesce_onwards: vec![None; length],
}
}
pub fn new_from_children_nodes(
children_nodes: Vec<PlanWithCorrespondingCoalescePartitions>,
parent_plan: Arc<dyn ExecutionPlan>,
) -> Result<Self> {
let children_plans = children_nodes
.iter()
.map(|item| item.plan.clone())
.collect();
let coalesce_onwards = children_nodes
.into_iter()
.enumerate()
.map(|(idx, item)| {
let plan = item.plan;
if plan.children().is_empty() {
None
} else if is_coalesce_partitions(&plan) {
Some(ExecTree::new(plan, idx, vec![]))
} else {
let children = item
.coalesce_onwards
.into_iter()
.flatten()
.filter(|item| {
!matches!(
plan.required_input_distribution()[item.idx],
Distribution::SinglePartition
)
})
.collect::<Vec<_>>();
if children.is_empty() {
None
} else {
Some(ExecTree::new(plan, idx, children))
}
}
})
.collect();
let plan = with_new_children_if_necessary(parent_plan, children_plans)?.into();
Ok(PlanWithCorrespondingCoalescePartitions {
plan,
coalesce_onwards,
})
}
pub fn children(&self) -> Vec<PlanWithCorrespondingCoalescePartitions> {
self.plan
.children()
.into_iter()
.map(|child| PlanWithCorrespondingCoalescePartitions::new(child))
.collect()
}
}
impl TreeNode for PlanWithCorrespondingCoalescePartitions {
fn apply_children<F>(&self, op: &mut F) -> Result<VisitRecursion>
where
F: FnMut(&Self) -> Result<VisitRecursion>,
{
let children = self.children();
for child in children {
match op(&child)? {
VisitRecursion::Continue => {}
VisitRecursion::Skip => return Ok(VisitRecursion::Continue),
VisitRecursion::Stop => return Ok(VisitRecursion::Stop),
}
}
Ok(VisitRecursion::Continue)
}
fn map_children<F>(self, transform: F) -> Result<Self>
where
F: FnMut(Self) -> Result<Self>,
{
let children = self.children();
if children.is_empty() {
Ok(self)
} else {
let children_nodes = children
.into_iter()
.map(transform)
.collect::<Result<Vec<_>>>()?;
PlanWithCorrespondingCoalescePartitions::new_from_children_nodes(
children_nodes,
self.plan,
)
}
}
}
impl PhysicalOptimizerRule for EnforceSorting {
fn optimize(
&self,
plan: Arc<dyn ExecutionPlan>,
config: &ConfigOptions,
) -> Result<Arc<dyn ExecutionPlan>> {
let plan_requirements = PlanWithCorrespondingSort::new(plan);
let adjusted = plan_requirements.transform_up(&ensure_sorting)?;
let new_plan = if config.optimizer.repartition_sorts {
let plan_with_coalesce_partitions =
PlanWithCorrespondingCoalescePartitions::new(adjusted.plan);
let parallel =
plan_with_coalesce_partitions.transform_up(¶llelize_sorts)?;
parallel.plan
} else {
adjusted.plan
};
let sort_pushdown = SortPushDown::init(new_plan);
let adjusted = sort_pushdown.transform_down(&pushdown_sorts)?;
Ok(adjusted.plan)
}
fn name(&self) -> &str {
"EnforceSorting"
}
fn schema_check(&self) -> bool {
true
}
}
fn parallelize_sorts(
requirements: PlanWithCorrespondingCoalescePartitions,
) -> Result<Transformed<PlanWithCorrespondingCoalescePartitions>> {
let plan = requirements.plan;
let mut coalesce_onwards = requirements.coalesce_onwards;
if plan.children().is_empty() || coalesce_onwards[0].is_none() {
return Ok(Transformed::No(PlanWithCorrespondingCoalescePartitions {
plan,
coalesce_onwards,
}));
} else if (is_sort(&plan) || is_sort_preserving_merge(&plan))
&& plan.output_partitioning().partition_count() <= 1
{
let mut prev_layer = plan.clone();
update_child_to_remove_coalesce(&mut prev_layer, &mut coalesce_onwards[0])?;
let sort_exprs = get_sort_exprs(&plan)?;
add_sort_above(&mut prev_layer, sort_exprs.to_vec())?;
let spm = SortPreservingMergeExec::new(sort_exprs.to_vec(), prev_layer);
return Ok(Transformed::Yes(PlanWithCorrespondingCoalescePartitions {
plan: Arc::new(spm),
coalesce_onwards: vec![None],
}));
} else if is_coalesce_partitions(&plan) {
let mut prev_layer = plan.clone();
update_child_to_remove_coalesce(&mut prev_layer, &mut coalesce_onwards[0])?;
let new_plan = plan.with_new_children(vec![prev_layer])?;
return Ok(Transformed::Yes(PlanWithCorrespondingCoalescePartitions {
plan: new_plan,
coalesce_onwards: vec![None],
}));
}
Ok(Transformed::Yes(PlanWithCorrespondingCoalescePartitions {
plan,
coalesce_onwards,
}))
}
fn ensure_sorting(
requirements: PlanWithCorrespondingSort,
) -> Result<Transformed<PlanWithCorrespondingSort>> {
if requirements.plan.children().is_empty() {
return Ok(Transformed::No(requirements));
}
let plan = requirements.plan;
let mut children = plan.children();
let mut sort_onwards = requirements.sort_onwards;
if let Some(result) = analyze_immediate_sort_removal(&plan, &sort_onwards) {
return Ok(Transformed::Yes(result));
}
for (idx, (child, sort_onwards, required_ordering)) in izip!(
children.iter_mut(),
sort_onwards.iter_mut(),
plan.required_input_ordering()
)
.enumerate()
{
let physical_ordering = child.output_ordering();
match (required_ordering, physical_ordering) {
(Some(required_ordering), Some(physical_ordering)) => {
if !ordering_satisfy_requirement_concrete(
physical_ordering,
&required_ordering,
|| child.equivalence_properties(),
|| child.ordering_equivalence_properties(),
) {
update_child_to_remove_unnecessary_sort(child, sort_onwards, &plan)?;
let sort_expr =
PhysicalSortRequirement::to_sort_exprs(required_ordering);
add_sort_above(child, sort_expr)?;
if is_sort(child) {
*sort_onwards = Some(ExecTree::new(child.clone(), idx, vec![]));
} else {
*sort_onwards = None;
}
}
}
(Some(required), None) => {
let sort_expr = PhysicalSortRequirement::to_sort_exprs(required);
add_sort_above(child, sort_expr)?;
*sort_onwards = Some(ExecTree::new(child.clone(), idx, vec![]));
}
(None, Some(_)) => {
if !plan.maintains_input_order()[idx] || is_union(&plan) {
update_child_to_remove_unnecessary_sort(child, sort_onwards, &plan)?;
}
}
(None, None) => {}
}
}
if is_window(&plan) {
if let Some(tree) = &mut sort_onwards[0] {
if let Some(result) = analyze_window_sort_removal(tree, &plan)? {
return Ok(Transformed::Yes(result));
}
}
} else if is_sort_preserving_merge(&plan)
&& children[0].output_partitioning().partition_count() <= 1
{
return Ok(Transformed::Yes(PlanWithCorrespondingSort {
plan: children[0].clone(),
sort_onwards: vec![sort_onwards[0].clone()],
}));
}
Ok(Transformed::Yes(PlanWithCorrespondingSort {
plan: plan.with_new_children(children)?,
sort_onwards,
}))
}
fn analyze_immediate_sort_removal(
plan: &Arc<dyn ExecutionPlan>,
sort_onwards: &[Option<ExecTree>],
) -> Option<PlanWithCorrespondingSort> {
if let Some(sort_exec) = plan.as_any().downcast_ref::<SortExec>() {
let sort_input = sort_exec.input().clone();
if ordering_satisfy(
sort_input.output_ordering(),
sort_exec.output_ordering(),
|| sort_input.equivalence_properties(),
|| sort_input.ordering_equivalence_properties(),
) {
return Some(
if !sort_exec.preserve_partitioning()
&& sort_input.output_partitioning().partition_count() > 1
{
let new_plan: Arc<dyn ExecutionPlan> =
Arc::new(SortPreservingMergeExec::new(
sort_exec.expr().to_vec(),
sort_input,
));
let new_tree = ExecTree::new(
new_plan.clone(),
0,
sort_onwards.iter().flat_map(|e| e.clone()).collect(),
);
PlanWithCorrespondingSort {
plan: new_plan,
sort_onwards: vec![Some(new_tree)],
}
} else {
PlanWithCorrespondingSort {
plan: sort_input,
sort_onwards: sort_onwards.to_vec(),
}
},
);
}
}
None
}
fn analyze_window_sort_removal(
sort_tree: &mut ExecTree,
window_exec: &Arc<dyn ExecutionPlan>,
) -> Result<Option<PlanWithCorrespondingSort>> {
let (window_expr, partition_keys) = if let Some(exec) =
window_exec.as_any().downcast_ref::<BoundedWindowAggExec>()
{
(exec.window_expr(), &exec.partition_keys)
} else if let Some(exec) = window_exec.as_any().downcast_ref::<WindowAggExec>() {
(exec.window_expr(), &exec.partition_keys)
} else {
return Err(DataFusionError::Plan(
"Expects to receive either WindowAggExec of BoundedWindowAggExec".to_string(),
));
};
let partitionby_exprs = window_expr[0].partition_by();
let orderby_sort_keys = window_expr[0].order_by();
let mut search_flags = None;
for sort_any in sort_tree.get_leaves() {
let sort_input = &sort_any.children()[0];
let flags = can_skip_sort(partitionby_exprs, orderby_sort_keys, sort_input)?;
if flags.is_some() && (search_flags.is_none() || search_flags == flags) {
search_flags = flags;
continue;
}
return Ok(None);
}
let (should_reverse, partition_search_mode) = if let Some(search_flags) = search_flags
{
search_flags
} else {
return Ok(None);
};
let is_unbounded = unbounded_output(window_exec);
if !is_unbounded && partition_search_mode != PartitionSearchMode::Sorted {
return Ok(None);
};
let new_window_expr = if should_reverse {
window_expr
.iter()
.map(|e| e.get_reverse_expr())
.collect::<Option<Vec<_>>>()
} else {
Some(window_expr.to_vec())
};
if let Some(window_expr) = new_window_expr {
let requires_single_partition = matches!(
window_exec.required_input_distribution()[sort_tree.idx],
Distribution::SinglePartition
);
let mut new_child = remove_corresponding_sort_from_sub_plan(
sort_tree,
requires_single_partition,
)?;
let new_schema = new_child.schema();
let uses_bounded_memory = window_expr.iter().all(|e| e.uses_bounded_memory());
let new_plan = if uses_bounded_memory {
Arc::new(BoundedWindowAggExec::try_new(
window_expr,
new_child,
new_schema,
partition_keys.to_vec(),
partition_search_mode,
)?) as _
} else {
if partition_search_mode != PartitionSearchMode::Sorted {
let reqs = window_exec
.required_input_ordering()
.swap_remove(0)
.unwrap_or(vec![]);
let sort_expr = PhysicalSortRequirement::to_sort_exprs(reqs);
add_sort_above(&mut new_child, sort_expr)?;
};
Arc::new(WindowAggExec::try_new(
window_expr,
new_child,
new_schema,
partition_keys.to_vec(),
)?) as _
};
return Ok(Some(PlanWithCorrespondingSort::new(new_plan)));
}
Ok(None)
}
fn update_child_to_remove_coalesce(
child: &mut Arc<dyn ExecutionPlan>,
coalesce_onwards: &mut Option<ExecTree>,
) -> Result<()> {
if let Some(coalesce_onwards) = coalesce_onwards {
*child = remove_corresponding_coalesce_in_sub_plan(coalesce_onwards, child)?;
}
Ok(())
}
fn remove_corresponding_coalesce_in_sub_plan(
coalesce_onwards: &mut ExecTree,
parent: &Arc<dyn ExecutionPlan>,
) -> Result<Arc<dyn ExecutionPlan>> {
Ok(if is_coalesce_partitions(&coalesce_onwards.plan) {
let mut new_plan = coalesce_onwards.plan.children()[0].clone();
while new_plan.output_partitioning() == parent.output_partitioning()
&& is_repartition(&new_plan)
&& is_repartition(parent)
{
new_plan = new_plan.children()[0].clone()
}
new_plan
} else {
let plan = coalesce_onwards.plan.clone();
let mut children = plan.children();
for item in &mut coalesce_onwards.children {
children[item.idx] = remove_corresponding_coalesce_in_sub_plan(item, &plan)?;
}
plan.with_new_children(children)?
})
}
fn update_child_to_remove_unnecessary_sort(
child: &mut Arc<dyn ExecutionPlan>,
sort_onwards: &mut Option<ExecTree>,
parent: &Arc<dyn ExecutionPlan>,
) -> Result<()> {
if let Some(sort_onwards) = sort_onwards {
let requires_single_partition = matches!(
parent.required_input_distribution()[sort_onwards.idx],
Distribution::SinglePartition
);
*child = remove_corresponding_sort_from_sub_plan(
sort_onwards,
requires_single_partition,
)?;
}
*sort_onwards = None;
Ok(())
}
fn remove_corresponding_sort_from_sub_plan(
sort_onwards: &mut ExecTree,
requires_single_partition: bool,
) -> Result<Arc<dyn ExecutionPlan>> {
let mut updated_plan = if is_sort(&sort_onwards.plan) {
sort_onwards.plan.children()[0].clone()
} else {
let plan = &sort_onwards.plan;
let mut children = plan.children();
for item in &mut sort_onwards.children {
let requires_single_partition = matches!(
plan.required_input_distribution()[item.idx],
Distribution::SinglePartition
);
children[item.idx] =
remove_corresponding_sort_from_sub_plan(item, requires_single_partition)?;
}
if is_sort_preserving_merge(plan) {
children[0].clone()
} else {
plan.clone().with_new_children(children)?
}
};
if requires_single_partition
&& updated_plan.output_partitioning().partition_count() > 1
{
if let Some(ordering) = updated_plan.output_ordering() {
updated_plan = Arc::new(SortPreservingMergeExec::new(
ordering.to_vec(),
updated_plan,
));
} else {
updated_plan = Arc::new(CoalescePartitionsExec::new(updated_plan.clone()));
}
}
Ok(updated_plan)
}
fn get_sort_exprs(sort_any: &Arc<dyn ExecutionPlan>) -> Result<&[PhysicalSortExpr]> {
if let Some(sort_exec) = sort_any.as_any().downcast_ref::<SortExec>() {
Ok(sort_exec.expr())
} else if let Some(sort_preserving_merge_exec) =
sort_any.as_any().downcast_ref::<SortPreservingMergeExec>()
{
Ok(sort_preserving_merge_exec.expr())
} else {
Err(DataFusionError::Plan(
"Given ExecutionPlan is not a SortExec or a SortPreservingMergeExec"
.to_string(),
))
}
}
fn can_skip_sort(
partitionby_exprs: &[Arc<dyn PhysicalExpr>],
orderby_keys: &[PhysicalSortExpr],
input: &Arc<dyn ExecutionPlan>,
) -> Result<Option<(bool, PartitionSearchMode)>> {
let physical_ordering = if let Some(physical_ordering) = input.output_ordering() {
physical_ordering
} else {
return Ok(None);
};
let orderby_exprs = convert_to_expr(orderby_keys);
let physical_ordering_exprs = convert_to_expr(physical_ordering);
let equal_properties = || input.equivalence_properties();
let ob_indices = get_indices_of_matching_exprs(
&orderby_exprs,
&physical_ordering_exprs,
equal_properties,
);
if ob_indices.len() != orderby_exprs.len() {
return Ok(None);
}
let pb_indices = get_indices_of_matching_exprs(
partitionby_exprs,
&physical_ordering_exprs,
equal_properties,
);
let ordered_merged_indices = merge_and_order_indices(&pb_indices, &ob_indices);
let unique_ob_indices = set_difference(&ob_indices, &pb_indices);
if !is_sorted(&unique_ob_indices) {
return Ok(None);
}
let first_n = longest_consecutive_prefix(ordered_merged_indices);
let furthest_ob_index = *unique_ob_indices.last().unwrap_or(&0);
if first_n <= furthest_ob_index {
return Ok(None);
}
let input_orderby_columns = get_at_indices(physical_ordering, &unique_ob_indices)?;
let expected_orderby_columns =
get_at_indices(orderby_keys, find_indices(&ob_indices, &unique_ob_indices)?)?;
let should_reverse = if let Some(should_reverse) = check_alignments(
&input.schema(),
&input_orderby_columns,
&expected_orderby_columns,
)? {
should_reverse
} else {
return Ok(None);
};
let ordered_pb_indices = pb_indices.iter().copied().sorted().collect::<Vec<_>>();
let first_n = longest_consecutive_prefix(&ordered_pb_indices);
let mode = if first_n == partitionby_exprs.len() {
PartitionSearchMode::Sorted
} else if first_n > 0 {
let ordered_range = &ordered_pb_indices[0..first_n];
let input_pb_exprs = get_at_indices(&physical_ordering_exprs, ordered_range)?;
let partially_ordered_indices = get_indices_of_matching_exprs(
&input_pb_exprs,
partitionby_exprs,
equal_properties,
);
PartitionSearchMode::PartiallySorted(partially_ordered_indices)
} else {
PartitionSearchMode::Linear
};
Ok(Some((should_reverse, mode)))
}
fn check_alignments(
schema: &SchemaRef,
physical_ordering: &[PhysicalSortExpr],
required: &[PhysicalSortExpr],
) -> Result<Option<bool>> {
let res = izip!(physical_ordering, required)
.map(|(lhs, rhs)| check_alignment(schema, lhs, rhs))
.collect::<Result<Option<Vec<_>>>>()?;
Ok(if let Some(res) = res {
if !res.is_empty() {
let first = res[0];
let all_same = res.into_iter().all(|elem| elem == first);
all_same.then_some(first)
} else {
Some(false)
}
} else {
None
})
}
fn check_alignment(
input_schema: &SchemaRef,
physical_ordering: &PhysicalSortExpr,
required: &PhysicalSortExpr,
) -> Result<Option<bool>> {
Ok(if required.expr.eq(&physical_ordering.expr) {
let physical_opts = physical_ordering.options;
let required_opts = required.options;
if required.expr.nullable(input_schema)? {
let reverse = physical_opts == !required_opts;
(reverse || physical_opts == required_opts).then_some(reverse)
} else {
Some(physical_opts.descending != required_opts.descending)
}
} else {
None
})
}
fn unbounded_output(plan: &Arc<dyn ExecutionPlan>) -> bool {
let res = if plan.children().is_empty() {
plan.unbounded_output(&[])
} else {
let children_unbounded_output = plan
.children()
.iter()
.map(unbounded_output)
.collect::<Vec<_>>();
plan.unbounded_output(&children_unbounded_output)
};
res.unwrap_or(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datasource::listing::PartitionedFile;
use crate::datasource::object_store::ObjectStoreUrl;
use crate::datasource::physical_plan::{FileScanConfig, ParquetExec};
use crate::physical_optimizer::dist_enforcement::EnforceDistribution;
use crate::physical_plan::aggregates::PhysicalGroupBy;
use crate::physical_plan::aggregates::{AggregateExec, AggregateMode};
use crate::physical_plan::coalesce_batches::CoalesceBatchesExec;
use crate::physical_plan::filter::FilterExec;
use crate::physical_plan::joins::utils::JoinOn;
use crate::physical_plan::joins::SortMergeJoinExec;
use crate::physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
use crate::physical_plan::memory::MemoryExec;
use crate::physical_plan::repartition::RepartitionExec;
use crate::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
use crate::physical_plan::union::UnionExec;
use crate::physical_plan::windows::create_window_expr;
use crate::physical_plan::windows::PartitionSearchMode::{
Linear, PartiallySorted, Sorted,
};
use crate::physical_plan::{displayable, Partitioning};
use crate::prelude::SessionContext;
use crate::test::csv_exec_sorted;
use arrow::compute::SortOptions;
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion_common::{Result, Statistics};
use datafusion_expr::JoinType;
use datafusion_expr::{AggregateFunction, WindowFrame, WindowFunction};
use datafusion_physical_expr::expressions::Column;
use datafusion_physical_expr::expressions::{col, NotExpr};
use datafusion_physical_expr::PhysicalSortExpr;
use std::sync::Arc;
fn create_test_schema() -> Result<SchemaRef> {
let nullable_column = Field::new("nullable_col", DataType::Int32, true);
let non_nullable_column = Field::new("non_nullable_col", DataType::Int32, false);
let schema = Arc::new(Schema::new(vec![nullable_column, non_nullable_column]));
Ok(schema)
}
fn create_test_schema2() -> Result<SchemaRef> {
let col_a = Field::new("col_a", DataType::Int32, true);
let col_b = Field::new("col_b", DataType::Int32, true);
let schema = Arc::new(Schema::new(vec![col_a, col_b]));
Ok(schema)
}
fn create_test_schema3() -> Result<SchemaRef> {
let a = Field::new("a", DataType::Int32, true);
let b = Field::new("b", DataType::Int32, false);
let c = Field::new("c", DataType::Int32, true);
let d = Field::new("d", DataType::Int32, false);
let e = Field::new("e", DataType::Int32, false);
let schema = Arc::new(Schema::new(vec![a, b, c, d, e]));
Ok(schema)
}
fn get_plan_string(plan: &Arc<dyn ExecutionPlan>) -> Vec<String> {
let formatted = displayable(plan.as_ref()).indent(true).to_string();
let actual: Vec<&str> = formatted.trim().lines().collect();
actual.iter().map(|elem| elem.to_string()).collect()
}
#[tokio::test]
async fn test_is_column_aligned_nullable() -> Result<()> {
let schema = create_test_schema()?;
let params = vec![
((true, true), (false, false), Some(true)),
((true, true), (false, true), None),
((true, true), (true, false), None),
((true, false), (false, true), Some(true)),
((true, false), (false, false), None),
((true, false), (true, true), None),
];
for (
(physical_desc, physical_nulls_first),
(req_desc, req_nulls_first),
expected,
) in params
{
let physical_ordering = PhysicalSortExpr {
expr: col("nullable_col", &schema)?,
options: SortOptions {
descending: physical_desc,
nulls_first: physical_nulls_first,
},
};
let required_ordering = PhysicalSortExpr {
expr: col("nullable_col", &schema)?,
options: SortOptions {
descending: req_desc,
nulls_first: req_nulls_first,
},
};
let res = check_alignment(&schema, &physical_ordering, &required_ordering)?;
assert_eq!(res, expected);
}
Ok(())
}
#[tokio::test]
async fn test_is_column_aligned_non_nullable() -> Result<()> {
let schema = create_test_schema()?;
let params = vec![
((true, true), (false, false), Some(true)),
((true, true), (false, true), Some(true)),
((true, true), (true, false), Some(false)),
((true, false), (false, true), Some(true)),
((true, false), (false, false), Some(true)),
((true, false), (true, true), Some(false)),
];
for (
(physical_desc, physical_nulls_first),
(req_desc, req_nulls_first),
expected,
) in params
{
let physical_ordering = PhysicalSortExpr {
expr: col("non_nullable_col", &schema)?,
options: SortOptions {
descending: physical_desc,
nulls_first: physical_nulls_first,
},
};
let required_ordering = PhysicalSortExpr {
expr: col("non_nullable_col", &schema)?,
options: SortOptions {
descending: req_desc,
nulls_first: req_nulls_first,
},
};
let res = check_alignment(&schema, &physical_ordering, &required_ordering)?;
assert_eq!(res, expected);
}
Ok(())
}
#[tokio::test]
async fn test_can_skip_ordering_exhaustive() -> Result<()> {
let test_schema = create_test_schema3()?;
let sort_exprs = vec![
sort_expr("a", &test_schema),
sort_expr("b", &test_schema),
sort_expr("c", &test_schema),
sort_expr("d", &test_schema),
];
let exec_unbounded = csv_exec_sorted(&test_schema, sort_exprs, true);
let test_cases = vec![
(vec!["a"], vec!["a"], Some(Sorted)),
(vec!["a"], vec!["b"], Some(Sorted)),
(vec!["a"], vec!["c"], None),
(vec!["a"], vec!["a", "b"], Some(Sorted)),
(vec!["a"], vec!["b", "c"], Some(Sorted)),
(vec!["a"], vec!["a", "c"], None),
(vec!["a"], vec!["a", "b", "c"], Some(Sorted)),
(vec!["b"], vec!["a"], Some(Linear)),
(vec!["b"], vec!["b"], None),
(vec!["b"], vec!["c"], None),
(vec!["b"], vec!["a", "b"], Some(Linear)),
(vec!["b"], vec!["b", "c"], None),
(vec!["b"], vec!["a", "c"], Some(Linear)),
(vec!["b"], vec!["a", "b", "c"], Some(Linear)),
(vec!["c"], vec!["a"], Some(Linear)),
(vec!["c"], vec!["b"], None),
(vec!["c"], vec!["c"], None),
(vec!["c"], vec!["a", "b"], Some(Linear)),
(vec!["c"], vec!["b", "c"], None),
(vec!["c"], vec!["a", "c"], Some(Linear)),
(vec!["c"], vec!["a", "b", "c"], Some(Linear)),
(vec!["b", "a"], vec!["a"], Some(Sorted)),
(vec!["b", "a"], vec!["b"], Some(Sorted)),
(vec!["b", "a"], vec!["c"], Some(Sorted)),
(vec!["b", "a"], vec!["a", "b"], Some(Sorted)),
(vec!["b", "a"], vec!["b", "c"], Some(Sorted)),
(vec!["b", "a"], vec!["a", "c"], Some(Sorted)),
(vec!["b", "a"], vec!["a", "b", "c"], Some(Sorted)),
(vec!["c", "b"], vec!["a"], Some(Linear)),
(vec!["c", "b"], vec!["b"], None),
(vec!["c", "b"], vec!["c"], None),
(vec!["c", "b"], vec!["a", "b"], Some(Linear)),
(vec!["c", "b"], vec!["b", "c"], None),
(vec!["c", "b"], vec!["a", "c"], Some(Linear)),
(vec!["c", "b"], vec!["a", "b", "c"], Some(Linear)),
(vec!["c", "a"], vec!["a"], Some(PartiallySorted(vec![1]))),
(vec!["c", "a"], vec!["b"], Some(PartiallySorted(vec![1]))),
(vec!["c", "a"], vec!["c"], Some(PartiallySorted(vec![1]))),
(
vec!["c", "a"],
vec!["a", "b"],
Some(PartiallySorted(vec![1])),
),
(
vec!["c", "a"],
vec!["b", "c"],
Some(PartiallySorted(vec![1])),
),
(
vec!["c", "a"],
vec!["a", "c"],
Some(PartiallySorted(vec![1])),
),
(
vec!["c", "a"],
vec!["a", "b", "c"],
Some(PartiallySorted(vec![1])),
),
(vec!["c", "b", "a"], vec!["a"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["b"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["c"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["a", "b"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["b", "c"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["a", "c"], Some(Sorted)),
(vec!["c", "b", "a"], vec!["a", "b", "c"], Some(Sorted)),
];
for (case_idx, test_case) in test_cases.iter().enumerate() {
let (partition_by_columns, order_by_params, expected) = &test_case;
let mut partition_by_exprs = vec![];
for col_name in partition_by_columns {
partition_by_exprs.push(col(col_name, &test_schema)?);
}
let mut order_by_exprs = vec![];
for col_name in order_by_params {
let expr = col(col_name, &test_schema)?;
let options = SortOptions::default();
order_by_exprs.push(PhysicalSortExpr { expr, options });
}
let res =
can_skip_sort(&partition_by_exprs, &order_by_exprs, &exec_unbounded)?;
let res = res.map(|(_, mode)| mode);
assert_eq!(
res, *expected,
"Unexpected result for in unbounded test case#: {case_idx:?}, case: {test_case:?}"
);
}
Ok(())
}
#[tokio::test]
async fn test_can_skip_ordering() -> Result<()> {
let test_schema = create_test_schema3()?;
let sort_exprs = vec![
sort_expr("a", &test_schema),
sort_expr("b", &test_schema),
sort_expr("c", &test_schema),
sort_expr("d", &test_schema),
];
let exec_unbounded = csv_exec_sorted(&test_schema, sort_exprs, true);
let test_cases = vec![
(vec!["a", "b"], vec![("c", false, false)], None),
(vec![], vec![("c", false, true)], None),
(vec!["b"], vec![("c", false, true)], None),
(vec!["a"], vec![("c", false, true)], None),
(
vec!["a", "b"],
vec![("c", false, true), ("e", false, true)],
None,
),
(vec!["a"], vec![("b", false, true)], Some((false, Sorted))),
(vec!["a"], vec![("a", false, true)], Some((false, Sorted))),
(vec!["a"], vec![("a", false, false)], Some((false, Sorted))),
(vec!["a"], vec![("a", true, true)], Some((false, Sorted))),
(vec!["a"], vec![("a", true, false)], Some((false, Sorted))),
(vec!["a"], vec![("b", false, false)], Some((false, Sorted))),
(vec!["a"], vec![("b", true, false)], Some((true, Sorted))),
(
vec!["a", "b"],
vec![("c", false, true)],
Some((false, Sorted)),
),
(
vec!["b", "a"],
vec![("c", false, true)],
Some((false, Sorted)),
),
(
vec!["a", "b"],
vec![("c", true, false)],
Some((true, Sorted)),
),
(
vec!["e"],
vec![("a", false, true)],
Some((false, Linear)),
),
(
vec!["b", "c"],
vec![("a", false, true), ("c", false, true)],
Some((false, Linear)),
),
(vec!["b"], vec![("a", false, true)], Some((false, Linear))),
(
vec!["a", "e"],
vec![("b", false, true)],
Some((false, PartiallySorted(vec![0]))),
),
(
vec!["a", "c"],
vec![("b", false, true)],
Some((false, PartiallySorted(vec![0]))),
),
(
vec!["c", "a"],
vec![("b", false, true)],
Some((false, PartiallySorted(vec![1]))),
),
(
vec!["d", "b", "a"],
vec![("c", false, true)],
Some((false, PartiallySorted(vec![2, 1]))),
),
(
vec!["e", "b", "a"],
vec![("c", false, true)],
Some((false, PartiallySorted(vec![2, 1]))),
),
(
vec!["d", "a"],
vec![("b", false, true)],
Some((false, PartiallySorted(vec![1]))),
),
(
vec!["a"],
vec![("b", false, true), ("a", false, true)],
Some((false, Sorted)),
),
(vec![], vec![("b", false, true), ("a", false, true)], None),
];
for (case_idx, test_case) in test_cases.iter().enumerate() {
let (partition_by_columns, order_by_params, expected) = &test_case;
let mut partition_by_exprs = vec![];
for col_name in partition_by_columns {
partition_by_exprs.push(col(col_name, &test_schema)?);
}
let mut order_by_exprs = vec![];
for (col_name, descending, nulls_first) in order_by_params {
let expr = col(col_name, &test_schema)?;
let options = SortOptions {
descending: *descending,
nulls_first: *nulls_first,
};
order_by_exprs.push(PhysicalSortExpr { expr, options });
}
assert_eq!(
can_skip_sort(&partition_by_exprs, &order_by_exprs, &exec_unbounded)?,
*expected,
"Unexpected result for in unbounded test case#: {case_idx:?}, case: {test_case:?}"
);
}
Ok(())
}
macro_rules! assert_optimized {
($EXPECTED_PLAN_LINES: expr, $EXPECTED_OPTIMIZED_PLAN_LINES: expr, $PLAN: expr) => {
let session_ctx = SessionContext::new();
let state = session_ctx.state();
let physical_plan = $PLAN;
let formatted = displayable(physical_plan.as_ref()).indent(true).to_string();
let actual: Vec<&str> = formatted.trim().lines().collect();
let expected_plan_lines: Vec<&str> = $EXPECTED_PLAN_LINES
.iter().map(|s| *s).collect();
assert_eq!(
expected_plan_lines, actual,
"\n**Original Plan Mismatch\n\nexpected:\n\n{expected_plan_lines:#?}\nactual:\n\n{actual:#?}\n\n"
);
let expected_optimized_lines: Vec<&str> = $EXPECTED_OPTIMIZED_PLAN_LINES
.iter().map(|s| *s).collect();
let optimized_physical_plan =
EnforceSorting::new().optimize(physical_plan, state.config_options())?;
let actual = get_plan_string(&optimized_physical_plan);
assert_eq!(
expected_optimized_lines, actual,
"\n**Optimized Plan Mismatch\n\nexpected:\n\n{expected_optimized_lines:#?}\nactual:\n\n{actual:#?}\n\n"
);
};
}
#[tokio::test]
async fn test_remove_unnecessary_sort() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let input = sort_exec(vec![sort_expr("non_nullable_col", &schema)], source);
let physical_plan = sort_exec(vec![sort_expr("nullable_col", &schema)], input);
let expected_input = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" SortExec: expr=[non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_sort_window_multilayer() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![sort_expr_options(
"non_nullable_col",
&source.schema(),
SortOptions {
descending: true,
nulls_first: true,
},
)];
let sort = sort_exec(sort_exprs.clone(), source);
let coalesce_batches = coalesce_batches_exec(sort);
let window_agg =
bounded_window_exec("non_nullable_col", sort_exprs, coalesce_batches);
let sort_exprs = vec![sort_expr_options(
"non_nullable_col",
&window_agg.schema(),
SortOptions {
descending: false,
nulls_first: false,
},
)];
let sort = sort_exec(sort_exprs.clone(), window_agg);
let filter = filter_exec(
Arc::new(NotExpr::new(
col("non_nullable_col", schema.as_ref()).unwrap(),
)),
sort,
);
let physical_plan = bounded_window_exec("non_nullable_col", sort_exprs, filter);
let expected_input = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" FilterExec: NOT non_nullable_col@1",
" SortExec: expr=[non_nullable_col@1 ASC NULLS LAST]",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" CoalesceBatchesExec: target_batch_size=128",
" SortExec: expr=[non_nullable_col@1 DESC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"WindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: CurrentRow, end_bound: Following(NULL) }]",
" FilterExec: NOT non_nullable_col@1",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" CoalesceBatchesExec: target_batch_size=128",
" SortExec: expr=[non_nullable_col@1 DESC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_add_required_sort() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let physical_plan = sort_preserving_merge_exec(sort_exprs, source);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_sort1() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), source);
let spm = sort_preserving_merge_exec(sort_exprs, sort);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), spm);
let physical_plan = sort_preserving_merge_exec(sort_exprs, sort);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_sort2() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![sort_expr("non_nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), source);
let spm = sort_preserving_merge_exec(sort_exprs, sort);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort2 = sort_exec(sort_exprs.clone(), spm);
let spm2 = sort_preserving_merge_exec(sort_exprs, sort2);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort3 = sort_exec(sort_exprs, spm2);
let physical_plan = repartition_exec(repartition_exec(sort3));
let expected_input = vec![
"RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=10",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" SortExec: expr=[nullable_col@0 ASC]",
" SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" SortExec: expr=[non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=10",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_sort3() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![sort_expr("non_nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), source);
let spm = sort_preserving_merge_exec(sort_exprs, sort);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let repartition_exec = repartition_exec(spm);
let sort2 = Arc::new(
SortExec::new(sort_exprs.clone(), repartition_exec)
.with_preserve_partitioning(true),
) as _;
let spm2 = sort_preserving_merge_exec(sort_exprs, sort2);
let physical_plan = aggregate_exec(spm2);
let expected_input = vec![
"AggregateExec: mode=Final, gby=[], aggr=[]",
" SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" SortExec: expr=[non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"AggregateExec: mode=Final, gby=[], aggr=[]",
" CoalescePartitionsExec",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_sort4() -> Result<()> {
let schema = create_test_schema()?;
let source1 = repartition_exec(memory_exec(&schema));
let source2 = repartition_exec(memory_exec(&schema));
let union = union_exec(vec![source1, source2]);
let sort_exprs = vec![sort_expr("non_nullable_col", &schema)];
let sort = Arc::new(
SortExec::new(sort_exprs.clone(), union).with_preserve_partitioning(true),
) as _;
let spm = sort_preserving_merge_exec(sort_exprs, sort);
let filter = filter_exec(
Arc::new(NotExpr::new(
col("non_nullable_col", schema.as_ref()).unwrap(),
)),
spm,
);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let physical_plan = sort_exec(sort_exprs, filter);
let expected_input = vec![
"SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" FilterExec: NOT non_nullable_col@1",
" SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" SortExec: expr=[non_nullable_col@1 ASC]",
" UnionExec",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" FilterExec: NOT non_nullable_col@1",
" UnionExec",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_remove_unnecessary_spm1() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let input = sort_preserving_merge_exec(
vec![sort_expr("non_nullable_col", &schema)],
source,
);
let input2 = sort_preserving_merge_exec(
vec![sort_expr("non_nullable_col", &schema)],
input,
);
let physical_plan =
sort_preserving_merge_exec(vec![sort_expr("nullable_col", &schema)], input2);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_do_not_remove_sort_with_limit() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort = sort_exec(sort_exprs.clone(), source1);
let limit = limit_exec(sort);
let parquet_sort_exprs = vec![sort_expr("nullable_col", &schema)];
let source2 = parquet_exec_sorted(&schema, parquet_sort_exprs);
let union = union_exec(vec![source2, limit]);
let repartition = repartition_exec(union);
let physical_plan = sort_preserving_merge_exec(sort_exprs, repartition);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" GlobalLimitExec: skip=0, fetch=100",
" LocalLimitExec: fetch=100",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" GlobalLimitExec: skip=0, fetch=100",
" LocalLimitExec: fetch=100",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_change_wrong_sorting() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort = sort_exec(vec![sort_exprs[0].clone()], source);
let physical_plan = sort_preserving_merge_exec(sort_exprs, sort);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_change_wrong_sorting2() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let spm1 = sort_preserving_merge_exec(sort_exprs.clone(), source);
let sort2 = sort_exec(vec![sort_exprs[0].clone()], spm1);
let physical_plan =
sort_preserving_merge_exec(vec![sort_exprs[1].clone()], sort2);
let expected_input = vec![
"SortPreservingMergeExec: [non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortExec: expr=[non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_sorted() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), source1);
let source2 = parquet_exec_sorted(&schema, sort_exprs.clone());
let union = union_exec(vec![source2, sort]);
let physical_plan = sort_preserving_merge_exec(sort_exprs, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = expected_input.clone();
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort = sort_exec(sort_exprs.clone(), source1);
let parquet_sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let source2 = parquet_exec_sorted(&schema, parquet_sort_exprs);
let union = union_exec(vec![source2, sort]);
let physical_plan = sort_preserving_merge_exec(sort_exprs, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC, non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = expected_input.clone();
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted2() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort = sort_exec(sort_exprs.clone(), source1);
let parquet_sort_exprs = vec![sort_expr("nullable_col", &schema)];
let source2 = parquet_exec_sorted(&schema, parquet_sort_exprs);
let union = union_exec(vec![source2, sort]);
let physical_plan = sort_preserving_merge_exec(sort_exprs, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted3() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort1 = sort_exec(sort_exprs1, source1.clone());
let sort_exprs2 = vec![sort_expr("nullable_col", &schema)];
let sort2 = sort_exec(sort_exprs2, source1);
let parquet_sort_exprs = vec![sort_expr("nullable_col", &schema)];
let source2 = parquet_exec_sorted(&schema, parquet_sort_exprs.clone());
let union = union_exec(vec![sort1, source2, sort2]);
let physical_plan = sort_preserving_merge_exec(parquet_sort_exprs, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted4() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![sort_expr("nullable_col", &schema)];
let sort1 = sort_exec(sort_exprs2.clone(), source1.clone());
let sort2 = sort_exec(sort_exprs2.clone(), source1);
let source2 = parquet_exec_sorted(&schema, sort_exprs2);
let union = union_exec(vec![sort1, source2, sort2]);
let physical_plan = sort_preserving_merge_exec(sort_exprs1, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted5() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![
sort_expr("nullable_col", &schema),
sort_expr_options(
"non_nullable_col",
&schema,
SortOptions {
descending: true,
nulls_first: false,
},
),
];
let sort_exprs3 = vec![sort_expr("nullable_col", &schema)];
let sort1 = sort_exec(sort_exprs1, source1.clone());
let sort2 = sort_exec(sort_exprs2, source1);
let union = union_exec(vec![sort1, sort2]);
let physical_plan = sort_preserving_merge_exec(sort_exprs3, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted6() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![sort_expr("nullable_col", &schema)];
let sort1 = sort_exec(sort_exprs1, source1.clone());
let sort_exprs2 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let repartition = repartition_exec(source1);
let spm = sort_preserving_merge_exec(sort_exprs2, repartition);
let parquet_sort_exprs = vec![sort_expr("nullable_col", &schema)];
let source2 = parquet_exec_sorted(&schema, parquet_sort_exprs.clone());
let union = union_exec(vec![sort1, source2, spm]);
let physical_plan = sort_preserving_merge_exec(parquet_sort_exprs, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted7() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs3 = vec![sort_expr("nullable_col", &schema)];
let sort1 = sort_exec(sort_exprs1.clone(), source1.clone());
let sort2 = sort_exec(sort_exprs1, source1);
let union = union_exec(vec![sort1, sort2]);
let physical_plan = sort_preserving_merge_exec(sort_exprs3, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_output = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_output, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted8() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![
sort_expr_options(
"nullable_col",
&schema,
SortOptions {
descending: true,
nulls_first: false,
},
),
sort_expr_options(
"non_nullable_col",
&schema,
SortOptions {
descending: true,
nulls_first: false,
},
),
];
let sort1 = sort_exec(sort_exprs1, source1.clone());
let sort2 = sort_exec(sort_exprs2, source1);
let physical_plan = union_exec(vec![sort1, sort2]);
let expected_input = vec![
"UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[nullable_col@0 DESC NULLS LAST,non_nullable_col@1 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_window_multi_path_sort() -> Result<()> {
let schema = create_test_schema()?;
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![sort_expr("nullable_col", &schema)];
let sort_exprs3 = vec![sort_expr_options(
"nullable_col",
&schema,
SortOptions {
descending: true,
nulls_first: false,
},
)];
let source1 = parquet_exec_sorted(&schema, sort_exprs1);
let source2 = parquet_exec_sorted(&schema, sort_exprs2);
let sort1 = sort_exec(sort_exprs3.clone(), source1);
let sort2 = sort_exec(sort_exprs3.clone(), source2);
let union = union_exec(vec![sort1, sort2]);
let spm = sort_preserving_merge_exec(sort_exprs3.clone(), union);
let physical_plan = bounded_window_exec("nullable_col", sort_exprs3, spm);
let expected_input = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" SortPreservingMergeExec: [nullable_col@0 DESC NULLS LAST]",
" UnionExec",
" SortExec: expr=[nullable_col@0 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC, non_nullable_col@1 ASC]",
" SortExec: expr=[nullable_col@0 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
];
let expected_optimized = vec![
"WindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: CurrentRow, end_bound: Following(NULL) }]",
" SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC, non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_window_multi_path_sort2() -> Result<()> {
let schema = create_test_schema()?;
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![sort_expr("nullable_col", &schema)];
let source1 = parquet_exec_sorted(&schema, sort_exprs2.clone());
let source2 = parquet_exec_sorted(&schema, sort_exprs2.clone());
let sort1 = sort_exec(sort_exprs1.clone(), source1);
let sort2 = sort_exec(sort_exprs1.clone(), source2);
let union = union_exec(vec![sort1, sort2]);
let spm = Arc::new(SortPreservingMergeExec::new(sort_exprs1, union)) as _;
let physical_plan = bounded_window_exec("nullable_col", sort_exprs2, spm);
let expected_input = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
];
let expected_optimized = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col], output_ordering=[nullable_col@0 ASC]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_union_inputs_different_sorted_with_limit() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let sort_exprs1 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort_exprs2 = vec![
sort_expr("nullable_col", &schema),
sort_expr_options(
"non_nullable_col",
&schema,
SortOptions {
descending: true,
nulls_first: false,
},
),
];
let sort_exprs3 = vec![sort_expr("nullable_col", &schema)];
let sort1 = sort_exec(sort_exprs1, source1.clone());
let sort2 = sort_exec(sort_exprs2, source1);
let limit = local_limit_exec(sort2);
let limit = global_limit_exec(limit);
let union = union_exec(vec![sort1, limit]);
let physical_plan = sort_preserving_merge_exec(sort_exprs3, union);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" GlobalLimitExec: skip=0, fetch=100",
" LocalLimitExec: fetch=100",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" UnionExec",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" GlobalLimitExec: skip=0, fetch=100",
" LocalLimitExec: fetch=100",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 DESC NULLS LAST]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_sort_merge_join_order_by_left() -> Result<()> {
let left_schema = create_test_schema()?;
let right_schema = create_test_schema2()?;
let left = parquet_exec(&left_schema);
let right = parquet_exec(&right_schema);
let join_on = vec![(
Column::new_with_schema("nullable_col", &left.schema()).unwrap(),
Column::new_with_schema("col_a", &right.schema()).unwrap(),
)];
let join_types = vec![
JoinType::Inner,
JoinType::Left,
JoinType::Right,
JoinType::Full,
JoinType::LeftSemi,
JoinType::LeftAnti,
];
for join_type in join_types {
let join =
sort_merge_join_exec(left.clone(), right.clone(), &join_on, &join_type);
let sort_exprs = vec![
sort_expr("nullable_col", &join.schema()),
sort_expr("non_nullable_col", &join.schema()),
];
let physical_plan = sort_preserving_merge_exec(sort_exprs.clone(), join);
let join_plan = format!(
"SortMergeJoin: join_type={join_type}, on=[(nullable_col@0, col_a@0)]"
);
let join_plan2 = format!(
" SortMergeJoin: join_type={join_type}, on=[(nullable_col@0, col_a@0)]"
);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,non_nullable_col@1 ASC]",
join_plan2.as_str(),
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
let expected_optimized = match join_type {
JoinType::Inner
| JoinType::Left
| JoinType::LeftSemi
| JoinType::LeftAnti => {
vec![
join_plan.as_str(),
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
]
}
_ => {
vec![
"SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
join_plan2.as_str(),
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
]
}
};
assert_optimized!(expected_input, expected_optimized, physical_plan);
}
Ok(())
}
#[tokio::test]
async fn test_sort_merge_join_order_by_right() -> Result<()> {
let left_schema = create_test_schema()?;
let right_schema = create_test_schema2()?;
let left = parquet_exec(&left_schema);
let right = parquet_exec(&right_schema);
let join_on = vec![(
Column::new_with_schema("nullable_col", &left.schema()).unwrap(),
Column::new_with_schema("col_a", &right.schema()).unwrap(),
)];
let join_types = vec![
JoinType::Inner,
JoinType::Left,
JoinType::Right,
JoinType::Full,
JoinType::RightAnti,
];
for join_type in join_types {
let join =
sort_merge_join_exec(left.clone(), right.clone(), &join_on, &join_type);
let sort_exprs = vec![
sort_expr("col_a", &join.schema()),
sort_expr("col_b", &join.schema()),
];
let physical_plan = sort_preserving_merge_exec(sort_exprs, join);
let join_plan = format!(
"SortMergeJoin: join_type={join_type}, on=[(nullable_col@0, col_a@0)]"
);
let spm_plan = match join_type {
JoinType::RightAnti => {
"SortPreservingMergeExec: [col_a@0 ASC,col_b@1 ASC]"
}
_ => "SortPreservingMergeExec: [col_a@2 ASC,col_b@3 ASC]",
};
let join_plan2 = format!(
" SortMergeJoin: join_type={join_type}, on=[(nullable_col@0, col_a@0)]"
);
let expected_input = vec![
spm_plan,
join_plan2.as_str(),
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
let expected_optimized = match join_type {
JoinType::Inner | JoinType::Right | JoinType::RightAnti => {
vec![
join_plan.as_str(),
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC,col_b@1 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
]
}
_ => {
vec![
"SortExec: expr=[col_a@2 ASC,col_b@3 ASC]",
join_plan2.as_str(),
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
]
}
};
assert_optimized!(expected_input, expected_optimized, physical_plan);
}
Ok(())
}
#[tokio::test]
async fn test_sort_merge_join_complex_order_by() -> Result<()> {
let left_schema = create_test_schema()?;
let right_schema = create_test_schema2()?;
let left = parquet_exec(&left_schema);
let right = parquet_exec(&right_schema);
let join_on = vec![(
Column::new_with_schema("nullable_col", &left.schema()).unwrap(),
Column::new_with_schema("col_a", &right.schema()).unwrap(),
)];
let join = sort_merge_join_exec(left, right, &join_on, &JoinType::Inner);
let sort_exprs1 = vec![
sort_expr("col_b", &join.schema()),
sort_expr("col_a", &join.schema()),
];
let physical_plan = sort_preserving_merge_exec(sort_exprs1, join.clone());
let expected_input = vec![
"SortPreservingMergeExec: [col_b@3 ASC,col_a@2 ASC]",
" SortMergeJoin: join_type=Inner, on=[(nullable_col@0, col_a@0)]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
let expected_optimized = vec![
"SortExec: expr=[col_b@3 ASC,col_a@2 ASC]",
" SortMergeJoin: join_type=Inner, on=[(nullable_col@0, col_a@0)]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
let sort_exprs2 = vec![
sort_expr("nullable_col", &join.schema()),
sort_expr("col_b", &join.schema()),
sort_expr("col_a", &join.schema()),
];
let physical_plan = sort_preserving_merge_exec(sort_exprs2, join);
let expected_input = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC,col_b@3 ASC,col_a@2 ASC]",
" SortMergeJoin: join_type=Inner, on=[(nullable_col@0, col_a@0)]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
let expected_optimized = vec![
"SortExec: expr=[nullable_col@0 ASC,col_b@3 ASC,col_a@2 ASC]",
" SortMergeJoin: join_type=Inner, on=[(nullable_col@0, col_a@0)]",
" SortExec: expr=[nullable_col@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
" SortExec: expr=[col_a@0 ASC]",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[col_a, col_b]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_multiple_sort_window_exec() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let sort_exprs1 = vec![sort_expr("nullable_col", &schema)];
let sort_exprs2 = vec![
sort_expr("nullable_col", &schema),
sort_expr("non_nullable_col", &schema),
];
let sort1 = sort_exec(sort_exprs1.clone(), source);
let window_agg1 =
bounded_window_exec("non_nullable_col", sort_exprs1.clone(), sort1);
let window_agg2 =
bounded_window_exec("non_nullable_col", sort_exprs2, window_agg1);
let physical_plan =
bounded_window_exec("non_nullable_col", sort_exprs1, window_agg2);
let expected_input = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" SortExec: expr=[nullable_col@0 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" BoundedWindowAggExec: wdw=[count: Ok(Field { name: \"count\", data_type: Int64, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(NULL), end_bound: CurrentRow }], mode=[Sorted]",
" SortExec: expr=[nullable_col@0 ASC,non_nullable_col@1 ASC]",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_multilayer_coalesce_partitions() -> Result<()> {
let schema = create_test_schema()?;
let source1 = parquet_exec(&schema);
let repartition = repartition_exec(source1);
let coalesce = Arc::new(CoalescePartitionsExec::new(repartition)) as _;
let filter = filter_exec(
Arc::new(NotExpr::new(
col("non_nullable_col", schema.as_ref()).unwrap(),
)),
coalesce,
);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let physical_plan = sort_exec(sort_exprs, filter);
let expected_input = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" FilterExec: NOT non_nullable_col@1",
" CoalescePartitionsExec",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" FilterExec: NOT non_nullable_col@1",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" ParquetExec: file_groups={1 group: [[x]]}, projection=[nullable_col, non_nullable_col]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
#[tokio::test]
async fn test_commutativity() -> Result<()> {
let schema = create_test_schema()?;
let session_ctx = SessionContext::new();
let state = session_ctx.state();
let memory_exec = memory_exec(&schema);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let window = bounded_window_exec("nullable_col", sort_exprs.clone(), memory_exec);
let repartition = repartition_exec(window);
let orig_plan =
Arc::new(SortExec::new(sort_exprs, repartition)) as Arc<dyn ExecutionPlan>;
let mut plan = orig_plan.clone();
let rules = vec![
Arc::new(EnforceDistribution::new()) as Arc<dyn PhysicalOptimizerRule>,
Arc::new(EnforceSorting::new()) as Arc<dyn PhysicalOptimizerRule>,
];
for rule in rules {
plan = rule.optimize(plan, state.config_options())?;
}
let first_plan = plan.clone();
let mut plan = orig_plan.clone();
let rules = vec![
Arc::new(EnforceSorting::new()) as Arc<dyn PhysicalOptimizerRule>,
Arc::new(EnforceDistribution::new()) as Arc<dyn PhysicalOptimizerRule>,
Arc::new(EnforceSorting::new()) as Arc<dyn PhysicalOptimizerRule>,
];
for rule in rules {
plan = rule.optimize(plan, state.config_options())?;
}
let second_plan = plan.clone();
assert_eq!(get_plan_string(&first_plan), get_plan_string(&second_plan));
Ok(())
}
#[tokio::test]
async fn test_coalesce_propagate() -> Result<()> {
let schema = create_test_schema()?;
let source = memory_exec(&schema);
let repartition = repartition_exec(source);
let coalesce_partitions = Arc::new(CoalescePartitionsExec::new(repartition));
let repartition = repartition_exec(coalesce_partitions);
let sort_exprs = vec![sort_expr("nullable_col", &schema)];
let sort = Arc::new(
SortExec::new(sort_exprs.clone(), repartition)
.with_preserve_partitioning(true),
) as _;
let spm = sort_preserving_merge_exec(sort_exprs.clone(), sort);
let sort = sort_exec(sort_exprs, spm);
let physical_plan = sort.clone();
let expected_input = vec![
"SortExec: expr=[nullable_col@0 ASC]",
" SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=1",
" CoalescePartitionsExec",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
let expected_optimized = vec![
"SortPreservingMergeExec: [nullable_col@0 ASC]",
" SortExec: expr=[nullable_col@0 ASC]",
" RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=0",
" MemoryExec: partitions=0, partition_sizes=[]",
];
assert_optimized!(expected_input, expected_optimized, physical_plan);
Ok(())
}
fn sort_expr(name: &str, schema: &Schema) -> PhysicalSortExpr {
sort_expr_options(name, schema, SortOptions::default())
}
fn sort_expr_options(
name: &str,
schema: &Schema,
options: SortOptions,
) -> PhysicalSortExpr {
PhysicalSortExpr {
expr: col(name, schema).unwrap(),
options,
}
}
fn memory_exec(schema: &SchemaRef) -> Arc<dyn ExecutionPlan> {
Arc::new(MemoryExec::try_new(&[], schema.clone(), None).unwrap())
}
fn sort_exec(
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
input: Arc<dyn ExecutionPlan>,
) -> Arc<dyn ExecutionPlan> {
let sort_exprs = sort_exprs.into_iter().collect();
Arc::new(SortExec::new(sort_exprs, input))
}
fn sort_preserving_merge_exec(
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
input: Arc<dyn ExecutionPlan>,
) -> Arc<dyn ExecutionPlan> {
let sort_exprs = sort_exprs.into_iter().collect();
Arc::new(SortPreservingMergeExec::new(sort_exprs, input))
}
fn filter_exec(
predicate: Arc<dyn PhysicalExpr>,
input: Arc<dyn ExecutionPlan>,
) -> Arc<dyn ExecutionPlan> {
Arc::new(FilterExec::try_new(predicate, input).unwrap())
}
fn bounded_window_exec(
col_name: &str,
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
input: Arc<dyn ExecutionPlan>,
) -> Arc<dyn ExecutionPlan> {
let sort_exprs: Vec<_> = sort_exprs.into_iter().collect();
let schema = input.schema();
Arc::new(
BoundedWindowAggExec::try_new(
vec![create_window_expr(
&WindowFunction::AggregateFunction(AggregateFunction::Count),
"count".to_owned(),
&[col(col_name, &schema).unwrap()],
&[],
&sort_exprs,
Arc::new(WindowFrame::new(true)),
schema.as_ref(),
)
.unwrap()],
input.clone(),
input.schema(),
vec![],
Sorted,
)
.unwrap(),
)
}
fn parquet_exec(schema: &SchemaRef) -> Arc<ParquetExec> {
Arc::new(ParquetExec::new(
FileScanConfig {
object_store_url: ObjectStoreUrl::parse("test:///").unwrap(),
file_schema: schema.clone(),
file_groups: vec![vec![PartitionedFile::new("x".to_string(), 100)]],
statistics: Statistics::default(),
projection: None,
limit: None,
table_partition_cols: vec![],
output_ordering: vec![],
infinite_source: false,
},
None,
None,
))
}
fn parquet_exec_sorted(
schema: &SchemaRef,
sort_exprs: impl IntoIterator<Item = PhysicalSortExpr>,
) -> Arc<dyn ExecutionPlan> {
let sort_exprs = sort_exprs.into_iter().collect();
Arc::new(ParquetExec::new(
FileScanConfig {
object_store_url: ObjectStoreUrl::parse("test:///").unwrap(),
file_schema: schema.clone(),
file_groups: vec![vec![PartitionedFile::new("x".to_string(), 100)]],
statistics: Statistics::default(),
projection: None,
limit: None,
table_partition_cols: vec![],
output_ordering: vec![sort_exprs],
infinite_source: false,
},
None,
None,
))
}
fn union_exec(input: Vec<Arc<dyn ExecutionPlan>>) -> Arc<dyn ExecutionPlan> {
Arc::new(UnionExec::new(input))
}
fn limit_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
global_limit_exec(local_limit_exec(input))
}
fn local_limit_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
Arc::new(LocalLimitExec::new(input, 100))
}
fn global_limit_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
Arc::new(GlobalLimitExec::new(input, 0, Some(100)))
}
fn repartition_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
Arc::new(
RepartitionExec::try_new(input, Partitioning::RoundRobinBatch(10)).unwrap(),
)
}
fn aggregate_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
let schema = input.schema();
Arc::new(
AggregateExec::try_new(
AggregateMode::Final,
PhysicalGroupBy::default(),
vec![],
vec![],
vec![],
input,
schema,
)
.unwrap(),
)
}
fn sort_merge_join_exec(
left: Arc<dyn ExecutionPlan>,
right: Arc<dyn ExecutionPlan>,
join_on: &JoinOn,
join_type: &JoinType,
) -> Arc<dyn ExecutionPlan> {
Arc::new(
SortMergeJoinExec::try_new(
left,
right,
join_on.clone(),
*join_type,
vec![SortOptions::default(); join_on.len()],
false,
)
.unwrap(),
)
}
fn coalesce_batches_exec(input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
Arc::new(CoalesceBatchesExec::new(input, 128))
}
}