use crate::{
capability::{
JoinForm, JoinStrategy, JoinStrategyChoice, KeyedHashJoinStrategyInputFacts, KeyedHashJoinStrategyOptions,
choose_join_strategy,
},
error::{QueryError, QueryErrorCode, Result},
pipeline::model::{PipelineTaskKind, SortStrategy},
plan::{JoinNode, NodeId},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskPhysical {
SourceRead(SourceReadPlan),
GpuResidentSource,
LiteralBatchSource,
Unary,
Aggregate(AggregatePlan),
Window,
JoinBuild(JoinPlan),
JoinProbe(JoinPlan),
JoinMaterialize,
FinalSort(SortStrategy),
Limit,
Union,
EdgeNormalize,
GraphAlgorithm,
VectorAlgorithm,
MlAlgorithm,
SinkMaterialize,
}
impl TaskPhysical {
pub const fn kind(&self) -> PipelineTaskKind {
match self {
Self::SourceRead(_) => PipelineTaskKind::SourceRead,
Self::GpuResidentSource => PipelineTaskKind::GpuResidentSource,
Self::LiteralBatchSource => PipelineTaskKind::LiteralBatchSource,
Self::Unary => PipelineTaskKind::Unary,
Self::Aggregate(_) => PipelineTaskKind::Aggregate,
Self::Window => PipelineTaskKind::Window,
Self::JoinBuild(_) => PipelineTaskKind::JoinBuild,
Self::JoinProbe(_) => PipelineTaskKind::JoinProbe,
Self::JoinMaterialize => PipelineTaskKind::JoinMaterialize,
Self::FinalSort(_) => PipelineTaskKind::FinalSort,
Self::Limit => PipelineTaskKind::Limit,
Self::Union => PipelineTaskKind::Union,
Self::EdgeNormalize => PipelineTaskKind::EdgeNormalize,
Self::GraphAlgorithm => PipelineTaskKind::GraphAlgorithm,
Self::VectorAlgorithm => PipelineTaskKind::VectorAlgorithm,
Self::MlAlgorithm => PipelineTaskKind::MlAlgorithm,
Self::SinkMaterialize => PipelineTaskKind::SinkMaterialize,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceReadPlan {
pub output: SourceOutput,
pub dynamic_filter_routes: bool,
pub prefetch_eligible: bool,
}
impl SourceReadPlan {
pub const FRAME: Self =
Self { output: SourceOutput::Frame, dynamic_filter_routes: false, prefetch_eligible: false };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceOutput {
Frame,
JoinLeafFrame {
adjacent_stages: bool,
},
JoinLeafStream {
adjacent_stages: bool,
},
AggregateInputStream {
adjacent_stages: bool,
chunk_hints: AggregateInputHints,
},
SharedJoinAggregateStream {
adjacent_stages: bool,
chunk_hints: AggregateInputHints,
},
UnaryChainStream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AggregateInputHints {
pub adjacent_stages: bool,
pub grouped_string_key: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JoinPlan {
form: JoinForm,
planned: JoinStrategy,
allowed: Vec<JoinStrategy>,
}
impl JoinPlan {
#[doc(hidden)]
pub fn new(form: JoinForm, planned: JoinStrategy, allowed: Vec<JoinStrategy>) -> Result<Self> {
if !allowed.contains(&planned) || allowed.iter().any(|strategy| !form.strategies().contains(strategy)) {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.pipeline.join_plan",
"a join plan allows a strategy its form cannot run or plans one it does not allow",
)
.with_fact("planned_strategy", planned.as_str())
.with_fact("allowed_strategies", strategy_list(&allowed)));
}
Ok(Self { form, planned, allowed })
}
pub(crate) fn for_form(form: JoinForm, planned: JoinStrategy) -> Self {
Self { form, planned, allowed: form.strategies().to_vec() }
}
pub(crate) fn narrowed(&self, keep: impl Fn(JoinStrategy) -> bool) -> Self {
let allowed = self.allowed.iter().copied().filter(|strategy| *strategy == self.planned || keep(*strategy));
Self { form: self.form, planned: self.planned, allowed: allowed.collect() }
}
#[doc(hidden)]
pub const fn form(&self) -> JoinForm {
self.form
}
#[doc(hidden)]
pub const fn planned(&self) -> JoinStrategy {
self.planned
}
#[doc(hidden)]
pub fn allowed(&self) -> &[JoinStrategy] {
&self.allowed
}
#[doc(hidden)]
pub fn refine(
&self,
join: &JoinNode,
input: KeyedHashJoinStrategyInputFacts,
options: &KeyedHashJoinStrategyOptions,
) -> Result<JoinStrategyChoice> {
let choice = choose_join_strategy(self.form, join, input, options);
if !self.allowed.contains(&choice.strategy()) {
return Err(QueryError::new(
QueryErrorCode::InternalInvariantViolation,
"query_engine.pipeline.join_plan",
"observed facts chose a join strategy outside the compiled join plan",
)
.with_fact("planned_strategy", self.planned.as_str())
.with_fact("chosen_strategy", choice.strategy().as_str())
.with_fact("allowed_strategies", strategy_list(&self.allowed)));
}
Ok(choice)
}
}
fn strategy_list(strategies: &[JoinStrategy]) -> String {
strategies.iter().map(|strategy| strategy.as_str()).collect::<Vec<_>>().join(",")
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AggregatePlan {
pub topk: Option<TopKFusion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopKFusion {
pub post_aggregate_projections: Vec<NodeId>,
pub sort_node: NodeId,
pub limit_node: NodeId,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
expr::ColumnRef,
plan::{JoinKey, JoinOutputColumn, JoinSpec, JoinType},
};
fn inner_join() -> JoinNode {
JoinNode {
inputs: [NodeId::from_index_for_test(0), NodeId::from_index_for_test(1)],
spec: JoinSpec::new(
JoinType::Inner,
vec![JoinKey::new(ColumnRef::input(0, "key", "key"), ColumnRef::input(1, "key", "key"))],
)
.with_output_projection(vec![JoinOutputColumn::left(ColumnRef::input(0, "key", "key"), "key")]),
}
}
fn small_build_facts() -> KeyedHashJoinStrategyInputFacts {
KeyedHashJoinStrategyInputFacts {
estimated_build_rows: Some(10),
observed_build_rows: Some(10),
build_side_bytes: Some(1024),
probe_side_bytes: None,
estimated_output_rows: None,
estimated_output_bytes: None,
build_key_unique: None,
probe_reuse_count: 1,
reusable_prebuild_supported: true,
}
}
#[test]
fn join_plan_refinement_never_leaves_the_allowed_strategies() {
let join = inner_join();
let options = KeyedHashJoinStrategyOptions::default();
let full = JoinPlan::for_form(JoinForm::KeyedEqui, JoinStrategy::OneShotEqui);
assert_eq!(
full.refine(&join, small_build_facts(), &options).expect("an allowed refinement").strategy(),
JoinStrategy::ReusableHashEqui
);
let narrowed = full.narrowed(|strategy| strategy != JoinStrategy::ReusableHashEqui);
assert_eq!(narrowed.allowed(), [JoinStrategy::OneShotEqui, JoinStrategy::PartitionedHashEqui]);
let error = narrowed.refine(&join, small_build_facts(), &options).expect_err("refinement outside the plan");
assert_eq!(error.code(), QueryErrorCode::InternalInvariantViolation);
assert_eq!(error.fact_value("chosen_strategy"), Some("reusable_hash_equi"));
assert_eq!(error.fact_value("allowed_strategies"), Some("one_shot_equi,partitioned_hash_equi"));
}
#[test]
fn join_plan_rejects_strategies_its_form_cannot_run() {
let error = JoinPlan::new(
JoinForm::KeyedEqui,
JoinStrategy::MixedKeyedResidual,
vec![JoinStrategy::MixedKeyedResidual],
)
.expect_err("a keyed equi-join cannot run the mixed residual strategy");
assert_eq!(error.code(), QueryErrorCode::InvalidPlan);
JoinPlan::new(JoinForm::KeyedEqui, JoinStrategy::OneShotEqui, vec![JoinStrategy::ReusableHashEqui])
.expect_err("the planned strategy must be allowed");
}
}