use super::super::super::super::operators::spec::{BlockingSpec, SourceSpec};
use super::super::super::properties::PhysicalProperties;
use super::super::super::types::{
FragmentId, FragmentSpec, PhysicalOperatorId, PhysicalOperatorIdAllocator, PhysicalOperatorSpec,
};
use super::super::assembler::{ArenaFragmentAllocator, ArenaPlanAssembler, FragmentCtx};
use super::super::specs::{
build_expand_all_spec_with_flags, build_filter_spec, build_flatten_spec, build_project_spec,
build_source_spec, count_only_expand_below, is_count_only_aggregate, COUNT_ONLY_COLUMN,
};
use super::global::push_global_op;
use crate::executor::base::ExecutionContext;
use crate::executor::build_error::PlanBuildError;
use crate::planning::plan::core::nodes::base::plan_node_enum::PlanNodeEnum;
use crate::planning::plan::core::nodes::base::plan_node_traits::{
MultipleInputNode, SingleInputNode,
};
use crate::planning::plan::core::nodes::graph_operations::aggregate_node::AggregateNode;
use linkrs_core::types::expr::Expression;
use linkrs_core::types::operators::AggregateFunction;
pub(super) struct PartitionedChain<'a> {
pub(super) scan: &'a PlanNodeEnum,
pub(super) local: Vec<&'a PlanNodeEnum>,
pub(super) global: Vec<&'a PlanNodeEnum>,
pub(super) aggregate_split: Option<&'a AggregateNode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScanDivision {
ByValueRange,
ByShardSlice,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_chain_group(
operators: &mut Vec<PhysicalOperatorSpec>,
fragments: &mut Vec<FragmentSpec>,
op_alloc: &mut PhysicalOperatorIdAllocator,
frag_alloc: &mut ArenaFragmentAllocator,
chain: &PartitionedChain,
scan: &PlanNodeEnum,
spec: &crate::planning::plan::PartitionSpec,
exec_ctx: &ExecutionContext,
) -> Result<(FragmentId, PhysicalOperatorId), PlanBuildError> {
let partition_fids = build_partition_local_fragments(
operators,
fragments,
op_alloc,
frag_alloc,
chain,
scan,
spec,
ScanDivision::ByShardSlice,
exec_ctx,
)?;
let mut child_fid = ArenaPlanAssembler::push_exchange_op(
operators,
fragments,
op_alloc,
frag_alloc,
partition_fids,
spec.partition_count(),
)
.0;
for (index, op) in chain.global.iter().enumerate() {
if index == 0 {
if let Some(agg) = chain.aggregate_split {
let (_, final_spec) = split_aggregate(agg);
child_fid = ArenaPlanAssembler::push_global_blocking_op(
operators,
fragments,
op_alloc,
frag_alloc,
child_fid,
agg.id(),
final_spec,
PhysicalProperties::single_blocking_with_budget(),
)?
.0;
continue;
}
}
child_fid = push_global_op(
operators, fragments, op_alloc, frag_alloc, child_fid, op, exec_ctx,
)?
.0;
}
let root_operator = fragments
.get(child_fid.0)
.map(|f| f.root_operator)
.ok_or_else(|| PlanBuildError::unsupported("PhysicalPlan", 0, "root fragment missing"))?;
Ok((child_fid, root_operator))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_partition_local_fragments(
operators: &mut Vec<PhysicalOperatorSpec>,
fragments: &mut Vec<FragmentSpec>,
op_alloc: &mut PhysicalOperatorIdAllocator,
frag_alloc: &mut ArenaFragmentAllocator,
chain: &PartitionedChain,
scan: &PlanNodeEnum,
spec: &crate::planning::plan::PartitionSpec,
division: ScanDivision,
exec_ctx: &ExecutionContext,
) -> Result<Vec<FragmentId>, PlanBuildError> {
let mut partition_fids = Vec::with_capacity(spec.partition_count());
for (index, range) in spec.ranges().iter().enumerate() {
let mut scan_spec = build_source_spec(scan, exec_ctx)?;
match &mut scan_spec {
SourceSpec::StorageScanVertices {
partition_range,
shard_slice,
..
} => match division {
ScanDivision::ByValueRange => *partition_range = Some(range.clone()),
ScanDivision::ByShardSlice => {
*shard_slice = Some(crate::storage::ShardSlice::new(
index,
spec.partition_count(),
));
}
},
SourceSpec::StorageScanEdges { shard_slice, .. } => {
*shard_slice = Some(crate::storage::ShardSlice::new(
index,
spec.partition_count(),
));
}
_ => {
return Err(PlanBuildError::unsupported(
"PhysicalPlan",
scan.id(),
"partitioned scan must lower to a storage vertex or edge scan",
));
}
}
let (mut fid, _) = ArenaPlanAssembler::push_source_op(
operators,
fragments,
op_alloc,
frag_alloc,
scan.id(),
scan_spec,
);
for op in &chain.local {
match op {
PlanNodeEnum::Filter(filter) => {
let subquery_runners = super::super::assembler::build_subquery_runner_specs(
filter.subqueries(),
exec_ctx,
)?;
let spec = build_filter_spec(filter, subquery_runners)?;
let (new_fid, op_id) = ArenaPlanAssembler::push_unary_op(
operators,
fragments,
op_alloc,
fid,
op.id(),
spec,
)?;
operators[op_id.0].has_folded_expressions = filter.has_folded_expressions();
fid = new_fid;
}
PlanNodeEnum::Project(project) => {
let subquery_runners = super::super::assembler::build_subquery_runner_specs(
project.subqueries(),
exec_ctx,
)?;
let spec = build_project_spec(project, subquery_runners)?;
let (new_fid, op_id) = ArenaPlanAssembler::push_unary_op(
operators,
fragments,
op_alloc,
fid,
op.id(),
spec,
)?;
operators[op_id.0].has_folded_expressions = project.has_folded_expressions();
fid = new_fid;
}
PlanNodeEnum::ExpandAll(expand) => {
let spec =
build_expand_all_spec_with_flags(expand, exec_ctx, expand.count_only())?;
fid = ArenaPlanAssembler::push_graph_op(
operators,
fragments,
op_alloc,
frag_alloc,
fid,
op.id(),
spec,
)?
.0;
}
PlanNodeEnum::Flatten(flatten) => {
let spec = build_flatten_spec(flatten)?;
let (new_fid, _) = ArenaPlanAssembler::push_unary_op(
operators,
fragments,
op_alloc,
fid,
op.id(),
spec,
)?;
fid = new_fid;
}
_ => unreachable!("local chain holds filter/project/expand/flatten operators only"),
}
}
if let Some(agg) = chain.aggregate_split {
let (partial, _) = split_aggregate(agg);
ArenaPlanAssembler::push_blocking_op(
&mut FragmentCtx {
operators,
fragments,
op_alloc,
},
fid,
agg.id(),
partial,
PhysicalProperties::single_blocking_with_budget(),
)?;
}
partition_fids.push(fid);
}
Ok(partition_fids)
}
pub(super) fn decompose(node: &PlanNodeEnum) -> Option<PartitionedChain<'_>> {
let mut chain: Vec<&PlanNodeEnum> = Vec::new();
if !collect_chain(node, &mut chain) {
return None;
}
let scan_index = chain.len() - 1;
let mut i = scan_index;
let mut local: Vec<&PlanNodeEnum> = Vec::new();
while i > 0 {
let op = chain[i - 1];
if matches!(
op,
PlanNodeEnum::Filter(_)
| PlanNodeEnum::Project(_)
| PlanNodeEnum::Flatten(_)
| PlanNodeEnum::ExpandAll(_)
) {
local.push(op);
i -= 1;
} else {
break;
}
}
let mut global: Vec<&PlanNodeEnum> = Vec::new();
for j in (0..i).rev() {
global.push(chain[j]);
}
let aggregate_split = match global.first() {
Some(PlanNodeEnum::Aggregate(agg))
if all_functions_support_partial(agg.aggregation_functions()) =>
{
Some(agg)
}
_ => None,
};
Some(PartitionedChain {
scan: chain[scan_index],
local,
global,
aggregate_split,
})
}
fn collect_chain<'a>(node: &'a PlanNodeEnum, chain: &mut Vec<&'a PlanNodeEnum>) -> bool {
chain.push(node);
match node {
PlanNodeEnum::ScanVertices(_) | PlanNodeEnum::ScanEdges(_) => true,
PlanNodeEnum::Filter(filter) => collect_chain(filter.input(), chain),
PlanNodeEnum::Project(project) => collect_chain(project.input(), chain),
PlanNodeEnum::Flatten(flatten) => collect_chain(flatten.input(), chain),
PlanNodeEnum::Limit(limit) => collect_chain(limit.input(), chain),
PlanNodeEnum::Sort(sort) => collect_chain(sort.input(), chain),
PlanNodeEnum::Aggregate(agg) => collect_chain(agg.input(), chain),
PlanNodeEnum::TopN(topn) => collect_chain(topn.input(), chain),
PlanNodeEnum::Dedup(dedup) => collect_chain(dedup.input(), chain),
PlanNodeEnum::Window(window) => collect_chain(window.input(), chain),
PlanNodeEnum::ExpandAll(expand) => {
if let Some(input) = expand.inputs().first() {
collect_chain(input, chain)
} else {
false
}
}
_ => false,
}
}
fn split_aggregate(node: &AggregateNode) -> (BlockingSpec, BlockingSpec) {
let group_by_expressions: Vec<Expression> = node
.group_keys()
.iter()
.map(|key| Expression::Variable(key.clone()))
.collect();
let count_only =
is_count_only_aggregate(node) && count_only_expand_below(node.input()).is_some();
let agg_args = node.aggregation_args();
let aggregate_functions: Vec<(AggregateFunction, Vec<Expression>)> = node
.aggregation_functions()
.iter()
.enumerate()
.map(|(i, func)| {
if count_only {
(
AggregateFunction::Sum,
vec![Expression::Variable(COUNT_ONLY_COLUMN.to_string())],
)
} else {
let args = agg_args.get(i).cloned().unwrap_or_default();
(*func, args)
}
})
.collect();
let output_col_names = node.col_names().to_vec();
(
BlockingSpec::PartialAggregate {
group_by_expressions: group_by_expressions.clone(),
aggregate_functions: aggregate_functions.clone(),
output_col_names: output_col_names.clone(),
},
BlockingSpec::FinalAggregate {
group_by_expressions,
aggregate_functions,
output_col_names,
},
)
}
fn all_functions_support_partial(funcs: &[AggregateFunction]) -> bool {
funcs.iter().all(|f| {
matches!(
f,
AggregateFunction::Count
| AggregateFunction::Sum
| AggregateFunction::Min
| AggregateFunction::Max
| AggregateFunction::Avg
)
})
}