use super::super::super::super::operators::spec::{JoinSpec, SetSpec};
use super::super::super::types::PhysicalOperatorIdAllocator;
use super::super::assembler::{
ArenaFragmentAllocator, ArenaPlanAssembler, BinaryOperatorSpec, FragmentCtx, HashExchangeParams,
};
use super::super::specs::build_inner_join_spec;
use super::chain::{
build_chain_group, build_partition_local_fragments, decompose, PartitionedChain, ScanDivision,
};
use super::PartitionBuildResult;
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::SingleInputNode;
use crate::planning::plan::{PartitionSource, PartitionStrategy};
use linkrs_core::types::expr::contextual::ContextualExpression;
use linkrs_core::types::expr::Expression;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IndependentBranchOp {
Union,
UnionAll,
Minus,
Intersect,
CrossJoin,
InnerJoin,
}
fn split_independent_branches(
node: &PlanNodeEnum,
) -> Option<(&PlanNodeEnum, &PlanNodeEnum, IndependentBranchOp)> {
match node {
PlanNodeEnum::Union(union) => {
let op = if union.distinct() {
IndependentBranchOp::Union
} else {
IndependentBranchOp::UnionAll
};
Some((union.input(), union.union_input(), op))
}
PlanNodeEnum::Minus(minus) => Some((
minus.input(),
minus.minus_input(),
IndependentBranchOp::Minus,
)),
PlanNodeEnum::Intersect(intersect) => Some((
intersect.input(),
intersect.intersect_input(),
IndependentBranchOp::Intersect,
)),
PlanNodeEnum::CrossJoin(join) => Some((
join.left_input(),
join.right_input(),
IndependentBranchOp::CrossJoin,
)),
PlanNodeEnum::InnerJoin(join) => {
if equality_join_keys_are_simple(join.hash_keys(), join.probe_keys()) {
Some((
join.left_input(),
join.right_input(),
IndependentBranchOp::InnerJoin,
))
} else {
None
}
}
_ => None,
}
}
fn equality_join_keys_are_simple(
hash_keys: &[ContextualExpression],
probe_keys: &[ContextualExpression],
) -> bool {
hash_keys.len() == 1
&& probe_keys.len() == 1
&& hash_keys
.first()
.and_then(|k| k.expression())
.is_some_and(|m| matches!(m.inner(), linkrs_core::types::expr::Expression::Variable(_)))
&& probe_keys
.first()
.and_then(|k| k.expression())
.is_some_and(|m| matches!(m.inner(), linkrs_core::types::expr::Expression::Variable(_)))
}
fn key_references_vid(expr: &ContextualExpression) -> bool {
expr.expression().is_some_and(|meta| {
matches!(
meta.inner(),
Expression::Variable(name) if name == "vid" || name.ends_with(".vid")
)
})
}
fn equality_join_keys_reference_vid(node: &PlanNodeEnum) -> bool {
match node {
PlanNodeEnum::InnerJoin(join) => {
!join.hash_keys().is_empty()
&& join.hash_keys().iter().all(key_references_vid)
&& !join.probe_keys().is_empty()
&& join.probe_keys().iter().all(key_references_vid)
}
_ => false,
}
}
pub(super) fn build_partitioned_multi(
node: &PlanNodeEnum,
spec: &crate::planning::plan::PartitionSpec,
exec_ctx: &ExecutionContext,
) -> PartitionBuildResult {
let Some((left, right, op)) = split_independent_branches(node) else {
return Ok(None);
};
let PartitionSource::VertexId { .. } = spec.source() else {
return Ok(None);
};
let Some(left_chain) = decompose(left) else {
return Ok(None);
};
let Some(right_chain) = decompose(right) else {
return Ok(None);
};
let (PlanNodeEnum::ScanVertices(_), PlanNodeEnum::ScanVertices(_)) =
(left_chain.scan, right_chain.scan)
else {
return Ok(None);
};
let join_spec: Option<JoinSpec> = match node {
PlanNodeEnum::InnerJoin(join) => Some(build_inner_join_spec(join)?),
_ => None,
};
if let Some(join_spec) = join_spec.as_ref() {
if let Some(result) = build_co_partitioned_join(
node,
join_spec.clone(),
&left_chain,
&right_chain,
spec,
exec_ctx,
)? {
return Ok(Some(result));
}
}
if let Some(join_spec) = join_spec.as_ref() {
if let Some(result) = build_hash_exchange_join(
node,
join_spec.clone(),
&left_chain,
&right_chain,
spec,
exec_ctx,
)? {
return Ok(Some(result));
}
}
let mut operators = Vec::new();
let mut fragments = Vec::new();
let mut op_alloc = PhysicalOperatorIdAllocator::new();
let mut frag_alloc = ArenaFragmentAllocator::new();
let (left_fid, _) = build_chain_group(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
&left_chain,
left_chain.scan,
spec,
exec_ctx,
)?;
let (right_fid, _) = build_chain_group(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
&right_chain,
right_chain.scan,
spec,
exec_ctx,
)?;
let binary_spec: BinaryOperatorSpec = match op {
IndependentBranchOp::Union => SetSpec::Union.into(),
IndependentBranchOp::UnionAll => SetSpec::UnionAll.into(),
IndependentBranchOp::Minus => SetSpec::Minus.into(),
IndependentBranchOp::Intersect => SetSpec::Intersect.into(),
IndependentBranchOp::CrossJoin => JoinSpec::CrossJoin.into(),
IndependentBranchOp::InnerJoin => join_spec
.ok_or_else(|| {
PlanBuildError::unsupported(
"PhysicalPlan",
node.id(),
"partitioned equality join is missing its join condition",
)
})?
.into(),
};
let (root_fid, root_op) = ArenaPlanAssembler::push_binary_op(
&mut FragmentCtx {
operators: &mut operators,
fragments: &mut fragments,
op_alloc: &mut op_alloc,
},
&mut frag_alloc,
left_fid,
right_fid,
node.id(),
binary_spec,
)?;
Ok(Some((operators, fragments, root_fid, root_op)))
}
#[allow(clippy::too_many_arguments)]
fn build_co_partitioned_join(
node: &PlanNodeEnum,
join_spec: JoinSpec,
left_chain: &PartitionedChain,
right_chain: &PartitionedChain,
spec: &crate::planning::plan::PartitionSpec,
exec_ctx: &ExecutionContext,
) -> PartitionBuildResult {
if !equality_join_keys_reference_vid(node)
|| !matches!(spec.strategy(), PartitionStrategy::Range)
{
return Ok(None);
}
if !left_chain.global.is_empty()
|| !right_chain.global.is_empty()
|| left_chain.aggregate_split.is_some()
|| right_chain.aggregate_split.is_some()
{
return Ok(None);
}
let mut operators = Vec::new();
let mut fragments = Vec::new();
let mut op_alloc = PhysicalOperatorIdAllocator::new();
let mut frag_alloc = ArenaFragmentAllocator::new();
let left_frags = build_partition_local_fragments(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
left_chain,
left_chain.scan,
spec,
ScanDivision::ByValueRange,
exec_ctx,
)?;
let right_frags = build_partition_local_fragments(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
right_chain,
right_chain.scan,
spec,
ScanDivision::ByValueRange,
exec_ctx,
)?;
let mut join_fids = Vec::with_capacity(spec.partition_count());
for index in 0..spec.partition_count() {
let (fid, _) = ArenaPlanAssembler::push_binary_op(
&mut FragmentCtx {
operators: &mut operators,
fragments: &mut fragments,
op_alloc: &mut op_alloc,
},
&mut frag_alloc,
left_frags[index],
right_frags[index],
node.id(),
join_spec.clone(),
)?;
join_fids.push(fid);
}
let (root_fid, _) = ArenaPlanAssembler::push_exchange_op(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
join_fids,
spec.partition_count(),
);
let root_operator = fragments
.get(root_fid.0)
.map(|f| f.root_operator)
.ok_or_else(|| PlanBuildError::unsupported("PhysicalPlan", 0, "root fragment missing"))?;
Ok(Some((operators, fragments, root_fid, root_operator)))
}
fn build_hash_exchange_join(
node: &PlanNodeEnum,
join_spec: JoinSpec,
left_chain: &PartitionedChain,
right_chain: &PartitionedChain,
spec: &crate::planning::plan::PartitionSpec,
exec_ctx: &ExecutionContext,
) -> PartitionBuildResult {
if !equality_join_keys_are_simple(hash_keys_of(node), probe_keys_of(node)) {
return Ok(None);
}
match spec.strategy() {
PartitionStrategy::Range | PartitionStrategy::Hash { .. } => {}
PartitionStrategy::RoundRobin => return Ok(None),
}
if equality_join_keys_reference_vid(node) {
return Ok(None);
}
if !left_chain.global.is_empty()
|| !right_chain.global.is_empty()
|| left_chain.aggregate_split.is_some()
|| right_chain.aggregate_split.is_some()
{
return Ok(None);
}
let mut operators = Vec::new();
let mut fragments = Vec::new();
let mut op_alloc = PhysicalOperatorIdAllocator::new();
let mut frag_alloc = ArenaFragmentAllocator::new();
let left_frags = build_partition_local_fragments(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
left_chain,
left_chain.scan,
spec,
ScanDivision::ByShardSlice,
exec_ctx,
)?;
let right_frags = build_partition_local_fragments(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
right_chain,
right_chain.scan,
spec,
ScanDivision::ByShardSlice,
exec_ctx,
)?;
let hash_exprs = extract_join_key_expressions(node);
if hash_exprs.is_empty() {
return Ok(None);
}
let (left_hash_fid, _) = ArenaPlanAssembler::push_hash_exchange_op(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
HashExchangeParams {
partition_fids: left_frags,
partition_count: spec.partition_count(),
hash_expressions: hash_exprs.clone(),
input_layout: None,
output_layout: None,
},
);
let (right_hash_fid, _) = ArenaPlanAssembler::push_hash_exchange_op(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
HashExchangeParams {
partition_fids: right_frags,
partition_count: spec.partition_count(),
hash_expressions: hash_exprs,
input_layout: None,
output_layout: None,
},
);
let mut join_fids = Vec::with_capacity(spec.partition_count());
for _ in 0..spec.partition_count() {
let (fid, _) = ArenaPlanAssembler::push_binary_op(
&mut FragmentCtx {
operators: &mut operators,
fragments: &mut fragments,
op_alloc: &mut op_alloc,
},
&mut frag_alloc,
left_hash_fid,
right_hash_fid,
node.id(),
join_spec.clone(),
)?;
join_fids.push(fid);
}
let (root_fid, _) = ArenaPlanAssembler::push_exchange_op(
&mut operators,
&mut fragments,
&mut op_alloc,
&mut frag_alloc,
join_fids,
spec.partition_count(),
);
let root_operator = fragments
.get(root_fid.0)
.map(|f| f.root_operator)
.ok_or_else(|| PlanBuildError::unsupported("PhysicalPlan", 0, "root fragment missing"))?;
Ok(Some((operators, fragments, root_fid, root_operator)))
}
fn hash_keys_of(node: &PlanNodeEnum) -> &[ContextualExpression] {
match node {
PlanNodeEnum::InnerJoin(join) => join.hash_keys(),
_ => &[],
}
}
fn probe_keys_of(node: &PlanNodeEnum) -> &[ContextualExpression] {
match node {
PlanNodeEnum::InnerJoin(join) => join.probe_keys(),
_ => &[],
}
}
fn extract_join_key_expressions(node: &PlanNodeEnum) -> Vec<Expression> {
match node {
PlanNodeEnum::InnerJoin(join) => join
.hash_keys()
.iter()
.filter_map(|k| k.get_expression())
.collect(),
_ => Vec::new(),
}
}