use std::{collections::BTreeSet, sync::Arc};
use super::{
AggregateNode, AggregateSpec, Annotations, EdgeNormalizeNode, EdgeNormalizeSpec, Expr, FilterNode,
GraphAlgorithmNode, GraphAlgorithmSpec, GraphConstructionPolicy, JoinNode, JoinSpec, LimitNode, LimitSpec,
MlAlgorithmNode, MlAlgorithmSpec, NodeId, PlanFacts, PlanNode, ProjectionNode, ProjectionOutput, QueryNode,
QueryPlan, ResultSink, SchemaRef, SortNode, SortSpec, SourceNode, UnionNode, VectorAlgorithmNode,
VectorAlgorithmSpec, WindowNode, WindowSpec, graph::derive_output, uniqueness::partial_plan_node_is_unique_on,
};
use crate::error::{QueryError, QueryErrorCode, Result};
const OPERATION: &str = "query_engine.plan.builder";
#[derive(Debug, Clone, Default)]
pub struct QueryPlanBuilder {
nodes: Vec<QueryNode>,
advisory_outputs: Vec<super::AdvisoryOutputBinding>,
}
impl QueryPlanBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn set_advisory_output(&mut self, binding: super::AdvisoryOutputBinding) -> Result<()> {
if self.node_output_schema(binding.node) != Some(&binding.schema) {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"advisory output schema must match its native occurrence",
)
.with_fact("node", binding.node));
}
if let Some(existing) = self.advisory_outputs.iter_mut().find(|value| value.node == binding.node) {
if existing != &binding {
existing.invalidate(super::AdvisoryUnknownReason::ConflictingOccurrences);
}
} else {
self.advisory_outputs.push(binding);
}
Ok(())
}
pub(crate) fn advisory_output(&self, node: NodeId) -> Option<&super::AdvisoryOutputBinding> {
self.advisory_outputs.iter().find(|binding| binding.node == node)
}
pub fn node_output_schema(&self, node: NodeId) -> Option<&SchemaRef> {
self.nodes.get(node.index()).map(QueryNode::output_schema)
}
pub fn add_source(&mut self, source: SourceNode) -> NodeId {
let output_schema = Arc::clone(source.output_schema());
let id = NodeId(self.nodes.len());
let kind = PlanNode::Source(Box::new(source));
let facts = PlanFacts::new(output_schema);
if let Some(existing) = self.nodes.iter().find(|node| node.kind == kind && node.facts == facts) {
return existing.id;
}
self.nodes.push(QueryNode { id, kind, facts });
id
}
pub fn add_filter(&mut self, input: NodeId, predicate: Expr) -> Result<NodeId> {
let ordering = self.input_ordering(input)?;
self.push(PlanNode::Filter(FilterNode { input, predicate }), None, ordering)
}
pub fn add_projection(&mut self, input: NodeId, outputs: Vec<ProjectionOutput>) -> Result<NodeId> {
self.push(PlanNode::Projection(ProjectionNode::new(input, outputs)), None, Vec::new())
}
pub fn add_aggregate(&mut self, input: NodeId, spec: AggregateSpec, output_schema: SchemaRef) -> Result<NodeId> {
self.push(PlanNode::Aggregate(AggregateNode { input, spec }), Some(output_schema), Vec::new())
}
pub fn add_window(&mut self, input: NodeId, spec: WindowSpec) -> Result<NodeId> {
let ordering = self.input_ordering(input)?;
self.push(PlanNode::Window(WindowNode { input, spec }), None, ordering)
}
pub fn add_sort(&mut self, input: NodeId, spec: SortSpec) -> Result<NodeId> {
let ordering = spec.expressions.clone();
self.push(PlanNode::Sort(SortNode { input, spec }), None, ordering)
}
pub fn add_limit(&mut self, input: NodeId, spec: LimitSpec) -> Result<NodeId> {
let ordering = self.input_ordering(input)?;
if spec.skip == 0 && matches!(&self.nodes[input.index()].kind, PlanNode::Limit(limit) if limit.spec == spec) {
return Ok(input);
}
let node = self.push(PlanNode::Limit(LimitNode { input, spec }), None, ordering)?;
if let Some(mut binding) = self.advisory_output(input).cloned() {
binding.node = node;
binding.schema = Arc::clone(self.nodes[node.index()].output_schema());
let limit_rows = |rows: u64| {
let remaining = rows.saturating_sub(spec.skip as u64);
spec.fetch.map_or(remaining, |fetch| remaining.min(fetch as u64))
};
binding.statistics.rows = match binding.statistics.rows {
super::AdvisoryStatistic::Exact(rows) => super::AdvisoryStatistic::Exact(limit_rows(rows)),
super::AdvisoryStatistic::Inexact(rows) => super::AdvisoryStatistic::Inexact(limit_rows(rows)),
super::AdvisoryStatistic::Absent => super::AdvisoryStatistic::Absent,
};
binding.statistics.bytes = super::AdvisoryStatistic::Absent;
if binding.statistics.rows.value().is_some() {
binding.unknown_reason = Some(super::AdvisoryUnknownReason::LimitBytesUnavailable);
}
self.set_advisory_output(binding)?;
}
Ok(node)
}
pub fn add_join(
&mut self,
left: NodeId,
right: NodeId,
spec: JoinSpec,
output_schema: SchemaRef,
) -> Result<NodeId> {
self.push(PlanNode::Join(JoinNode { inputs: [left, right], spec }), Some(output_schema), Vec::new())
}
pub fn add_union(&mut self, inputs: Vec<NodeId>, output_schema: SchemaRef) -> Result<NodeId> {
self.push(PlanNode::Union(UnionNode { inputs }), Some(output_schema), Vec::new())
}
pub fn add_edge_normalize(&mut self, input: NodeId, spec: EdgeNormalizeSpec) -> Result<NodeId> {
self.push(PlanNode::EdgeNormalize(EdgeNormalizeNode { input, spec }), None, Vec::new())
}
pub fn add_graph_algorithm(
&mut self,
input: NodeId,
side_inputs: Vec<NodeId>,
mut spec: GraphAlgorithmSpec,
) -> Result<NodeId> {
let input = if spec.graph_options.policy == GraphConstructionPolicy::PythonCugraph {
let normalize_spec = EdgeNormalizeSpec::python_cugraph(spec.edge_columns.clone(), &spec.graph_options);
spec.graph_options.policy = GraphConstructionPolicy::RawLibcugraph;
spec.edge_normalize_inserted = true;
self.add_edge_normalize(input, normalize_spec)?
} else {
input
};
let inputs = std::iter::once(input).chain(side_inputs).collect();
self.push(PlanNode::GraphAlgorithm(GraphAlgorithmNode { inputs, spec }), None, Vec::new())
}
pub fn output_is_unique_on(&self, node: NodeId, columns: &BTreeSet<String>) -> bool {
partial_plan_node_is_unique_on(&self.nodes, node, columns)
}
pub fn add_vector_algorithm(&mut self, inputs: Vec<NodeId>, spec: VectorAlgorithmSpec) -> Result<NodeId> {
self.push(PlanNode::VectorAlgorithm(VectorAlgorithmNode { inputs, spec }), None, Vec::new())
}
pub fn add_ml_algorithm(&mut self, inputs: Vec<NodeId>, spec: MlAlgorithmSpec) -> Result<NodeId> {
self.push(PlanNode::MlAlgorithm(MlAlgorithmNode { inputs, spec }), None, Vec::new())
}
pub fn finish_with_host_sink(self, root: NodeId) -> Result<QueryPlan> {
let schema = Arc::clone(self.node_output_schema(root).ok_or_else(|| missing_input(root))?);
self.finish(ResultSink::HostArrow { input: root, schema })
}
pub fn finish_with_host_sink_schema(self, root: NodeId, schema: SchemaRef) -> Result<QueryPlan> {
self.finish(ResultSink::HostArrow { input: root, schema })
}
pub fn finish_gpu_resident(self, root: NodeId) -> Result<QueryPlan> {
self.finish(ResultSink::GpuResident { input: root })
}
fn finish(self, sink: ResultSink) -> Result<QueryPlan> {
QueryPlan::from_parts(
self.nodes,
sink,
Annotations { dynamic_filter_routes: Vec::new(), advisory: self.advisory_outputs },
)
}
fn input_ordering(&self, input: NodeId) -> Result<Vec<super::SortExpr>> {
self.nodes.get(input.index()).map(|node| node.facts.output_ordering.clone()).ok_or_else(|| missing_input(input))
}
fn push(&mut self, kind: PlanNode, declared: Option<SchemaRef>, ordering: Vec<super::SortExpr>) -> Result<NodeId> {
let id = NodeId(self.nodes.len());
if let Some(input) = kind.input_ids().iter().find(|input| input.index() >= self.nodes.len()) {
return Err(missing_input(*input));
}
let output_schema = derive_output(id, &kind, declared.as_ref(), &self.nodes)?;
let facts = PlanFacts::new(output_schema).with_output_ordering(ordering);
if let Some(existing) = self.nodes.iter().find(|node| node.kind == kind && node.facts == facts) {
return Ok(existing.id);
}
self.nodes.push(QueryNode { id, kind, facts });
Ok(id)
}
}
fn missing_input(input: NodeId) -> QueryError {
QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"query plan builder input does not name a node of this builder",
)
.with_fact("input", input)
}