use std::sync::Arc;
use super::{
EdgeNormalizeSpec, Field, NodeId, PlanNode, QueryNode, ResultSink, Schema, SchemaRef, UnionNode, WindowNode,
};
use crate::error::{QueryError, QueryErrorCode, Result};
const OPERATION: &str = "query_engine.plan.graph";
pub(super) fn check_node_order(nodes: &[QueryNode], root: NodeId) -> Result<()> {
for (position, node) in nodes.iter().enumerate() {
if node.id.index() != position {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"query plan node id must match its position",
)
.with_fact("node", node.id)
.with_fact("position", position));
}
if let Some(input) = node.input_ids().iter().find(|input| input.index() >= position) {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"query plan node input must precede its consumer",
)
.with_fact("consumer", node.id)
.with_fact("input", *input));
}
}
if root.index() >= nodes.len() {
return Err(QueryError::new(QueryErrorCode::InvalidPlan, OPERATION, "query plan sink input does not exist")
.with_fact("sink_input", root)
.with_fact("node_count", nodes.len()));
}
Ok(())
}
pub(super) fn validated_plan_parts(mut nodes: Vec<QueryNode>, sink: &ResultSink) -> Result<Vec<QueryNode>> {
check_node_order(&nodes, sink.input())?;
for position in 0..nodes.len() {
let (inputs, rest) = nodes.split_at_mut(position);
let node = &mut rest[0];
node.facts.output_schema = derive_output(node.id, &node.kind, Some(&node.facts.output_schema), inputs)?;
}
check_reachable(&nodes, sink.input())?;
check_sink(&nodes, sink)?;
Ok(nodes)
}
pub(super) fn derive_output(
id: NodeId,
kind: &PlanNode,
declared: Option<&SchemaRef>,
inputs: &[QueryNode],
) -> Result<SchemaRef> {
let input = |id: NodeId| inputs[id.index()].output_schema();
let declared = || {
declared.cloned().ok_or_else(|| {
QueryError::new(QueryErrorCode::InvalidPlan, OPERATION, "query plan node requires a declared output schema")
.with_fact("node", id)
})
};
Ok(match kind {
PlanNode::Source(source) => Arc::clone(source.output_schema()),
PlanNode::Filter(filter) => Arc::clone(input(filter.input)),
PlanNode::Sort(sort) => Arc::clone(input(sort.input)),
PlanNode::Limit(limit) => Arc::clone(input(limit.input)),
PlanNode::Projection(projection) => Arc::clone(projection.output_schema()),
PlanNode::Window(window) => window_output_schema(window, input(window.input)),
PlanNode::EdgeNormalize(edge_normalize) => {
edge_normalize_output_schema(input(edge_normalize.input), &edge_normalize.spec)?
},
PlanNode::GraphAlgorithm(graph) => {
check_input_ordinals(id, graph.spec.params.side_input_ordinals(), 1, graph.inputs.len())?;
Arc::clone(graph.output_schema())
},
PlanNode::VectorAlgorithm(vector) => {
let ordinals = vector.spec.params.relation_bindings().map(|binding| binding.input);
check_input_ordinals(id, ordinals, 0, vector.inputs.len())?;
Arc::clone(vector.output_schema())
},
PlanNode::MlAlgorithm(ml) => {
check_input_ordinals(
id,
ml.spec.params.relation_bindings().map(|binding| binding.input),
0,
ml.inputs.len(),
)?;
Arc::clone(ml.output_schema())
},
PlanNode::Aggregate(_) | PlanNode::Join(_) => declared()?,
PlanNode::Union(union) => {
let declared = declared()?;
check_union_inputs(id, union, &declared, inputs)?;
declared
},
})
}
fn check_input_ordinals(id: NodeId, ordinals: impl Iterator<Item = usize>, first: usize, inputs: usize) -> Result<()> {
let mut named = vec![false; inputs];
for ordinal in ordinals {
if ordinal < first || ordinal >= inputs || std::mem::replace(&mut named[ordinal], true) {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"algorithm relation binding must name a distinct input of its node",
)
.with_fact("node_id", id)
.with_fact("input_ordinal", ordinal)
.with_fact("input_count", inputs));
}
}
if inputs == 0 || named[first.min(inputs)..].iter().any(|named| !named) {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"every algorithm input must be named by one relation binding",
)
.with_fact("node_id", id)
.with_fact("input_count", inputs));
}
Ok(())
}
pub(super) fn window_output_schema(window: &WindowNode, input: &SchemaRef) -> SchemaRef {
let fields = input
.fields()
.iter()
.map(|field| field.as_ref().clone())
.chain(window.spec.functions.iter().map(|function| function.output().clone()))
.collect::<Vec<_>>();
Arc::new(Schema::new(fields))
}
pub(super) fn edge_normalize_output_schema(input_schema: &SchemaRef, spec: &EdgeNormalizeSpec) -> Result<SchemaRef> {
let mut fields = Vec::with_capacity(
2 + usize::from(spec.edge_columns.weight.is_some()) + usize::from(spec.edge_columns.edge_id.is_some()),
);
fields.push(edge_normalize_field(input_schema, &spec.edge_columns.source, "source")?);
fields.push(edge_normalize_field(input_schema, &spec.edge_columns.destination, "destination")?);
if let Some(weight) = &spec.edge_columns.weight {
fields.push(edge_normalize_field(input_schema, weight, "weight")?);
}
if let Some(edge_id) = &spec.edge_columns.edge_id {
fields.push(edge_normalize_field(input_schema, edge_id, "edge_id")?);
}
Ok(Arc::new(Schema::new(fields)))
}
fn edge_normalize_field(input_schema: &SchemaRef, column: &str, role: &'static str) -> Result<Field> {
input_schema.field_with_name(column).cloned().map_err(|error| {
QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.plan.graph_algorithm.edge_normalize_schema",
"edge-normalize column is not present in the input schema",
)
.with_fact("column", column)
.with_fact("role", role)
.with_source(error)
})
}
fn check_union_inputs(id: NodeId, union: &UnionNode, declared: &SchemaRef, inputs: &[QueryNode]) -> Result<()> {
if union.inputs.is_empty() {
return Err(QueryError::new(QueryErrorCode::InvalidPlan, OPERATION, "union node requires at least one input")
.with_fact("node_id", id));
}
for (input_index, input) in union.inputs.iter().enumerate() {
let input_schema = inputs[input.index()].output_schema();
if input_schema.fields().len() != declared.fields().len() {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"union input schema field count must match the union output schema",
)
.with_fact("node_id", id)
.with_fact("input_index", input_index)
.with_fact("input_node", *input)
.with_fact("input_fields", input_schema.fields().len())
.with_fact("output_fields", declared.fields().len()));
}
for (field_index, (input_field, output_field)) in
input_schema.fields().iter().zip(declared.fields().iter()).enumerate()
{
if input_field.data_type() != output_field.data_type()
|| (input_field.is_nullable() && !output_field.is_nullable())
{
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"union input field must match the union output field type and nullability by ordinal",
)
.with_fact("node_id", id)
.with_fact("input_index", input_index)
.with_fact("input_node", *input)
.with_fact("field_index", field_index)
.with_fact("input_type", format!("{:?}", input_field.data_type()))
.with_fact("output_type", format!("{:?}", output_field.data_type()))
.with_fact("input_nullable", input_field.is_nullable())
.with_fact("output_nullable", output_field.is_nullable()));
}
}
}
Ok(())
}
fn check_reachable(nodes: &[QueryNode], root: NodeId) -> Result<()> {
let mut reachable = vec![false; nodes.len()];
reachable[root.index()] = true;
for node in nodes.iter().rev() {
if reachable[node.id.index()] {
for input in node.input_ids() {
reachable[input.index()] = true;
}
}
}
match reachable.iter().position(|reachable| !reachable) {
Some(index) => Err(QueryError::new(
QueryErrorCode::InvalidPlan,
OPERATION,
"query plan contains a node the sink does not reach",
)
.with_fact("node", nodes[index].id)
.with_fact("sink_input", root)),
None => Ok(()),
}
}
pub(super) fn check_sink(nodes: &[QueryNode], sink: &ResultSink) -> Result<()> {
let Some(schema) = sink.caller_visible_host_schema() else {
return Ok(());
};
let root = &nodes[sink.input().index()];
let root_fields = root.output_schema().fields();
if schema.fields().len() != root_fields.len() {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.plan.validate_sink",
"host sink schema field count must match the root node output",
)
.with_fact("root_node", root.id)
.with_fact("sink_fields", schema.fields().len())
.with_fact("root_fields", root_fields.len()));
}
for (index, (sink_field, root_field)) in schema.fields().iter().zip(root_fields.iter()).enumerate() {
if sink_field.data_type() != root_field.data_type() {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.plan.validate_sink",
"host sink Arrow type must match the root node output by ordinal",
)
.with_fact("root_node", root.id)
.with_fact("field_index", index)
.with_fact("sink_type", format!("{:?}", sink_field.data_type()))
.with_fact("root_type", format!("{:?}", root_field.data_type())));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, sync::Arc};
use arrow::datatypes::{DataType, Field, Schema};
use crate::{
error::QueryErrorCode,
expr::{AggregateFunction, Expr, LimitSpec, SortExpr, SortSpec},
plan::{NodeId, PlanNode, QueryPlanBuilder, RewrittenNodes, WindowFunction, WindowSpec},
source::{GpuResidentSource, SourceNode},
};
fn source(builder: &mut QueryPlanBuilder, name: &str, fields: Vec<Field>) -> NodeId {
builder.add_source(SourceNode::GpuResident(GpuResidentSource::new(name, Arc::new(Schema::new(fields)))))
}
#[test]
fn rewrite_rejects_a_forward_input_before_compaction_reads_it() {
let mut builder = QueryPlanBuilder::new();
let input = source(&mut builder, "input", vec![Field::new("v", DataType::Int64, false)]);
let filter = builder.add_filter(input, Expr::is_not_null(Expr::column("v"))).unwrap();
let plan = builder.finish_gpu_resident(filter).unwrap();
let mut nodes = plan.nodes().to_vec();
let PlanNode::Filter(filter) = &mut nodes[1].kind else { unreachable!("node 1 is the filter") };
filter.input = NodeId(7);
let error = plan.rewrite(RewrittenNodes::in_place(nodes, plan.root_id())).unwrap_err();
assert_eq!(error.code(), QueryErrorCode::InvalidPlan);
assert_eq!(error.operation(), "query_engine.plan.graph");
assert_eq!(error.fact_value("input"), Some("n7"));
}
#[test]
fn row_preserving_and_window_nodes_derive_their_output_schema() {
let metadata = HashMap::from([("origin".to_string(), "frontend".to_string())]);
let mut builder = QueryPlanBuilder::new();
let input = source(
&mut builder,
"input",
vec![
Field::new("k", DataType::Int32, false).with_metadata(metadata),
Field::new("v", DataType::Int64, true),
],
);
let input_schema = Arc::clone(builder.node_output_schema(input).unwrap());
let filter = builder.add_filter(input, Expr::is_not_null(Expr::column("v"))).unwrap();
let sort =
builder.add_sort(filter, SortSpec::new(vec![SortExpr::new(Expr::column("k"), false, false)])).unwrap();
let limit = builder.add_limit(sort, LimitSpec::fetch(5)).unwrap();
for node in [filter, sort, limit] {
assert_eq!(builder.node_output_schema(node), Some(&input_schema));
}
let window = builder
.add_window(
limit,
WindowSpec::new(
vec![Expr::column("k")],
vec![SortExpr::new(Expr::column("v"), false, false)],
vec![
WindowFunction::rank("r"),
WindowFunction::cumulative_aggregate(
AggregateFunction::Sum,
Expr::column("v"),
Field::new("running", DataType::Int64, true),
),
],
),
)
.unwrap();
let fields = input_schema
.fields()
.iter()
.map(|field| field.as_ref().clone())
.chain([Field::new("r", DataType::UInt64, false), Field::new("running", DataType::Int64, true)])
.collect::<Vec<_>>();
assert_eq!(builder.node_output_schema(window).unwrap().as_ref(), &Schema::new(fields));
}
#[test]
fn union_output_keeps_its_declared_names_and_rejects_narrowed_nullability() {
let mut builder = QueryPlanBuilder::new();
let left = source(&mut builder, "left", vec![Field::new("a", DataType::Int64, false)]);
let right = source(&mut builder, "right", vec![Field::new("b", DataType::Int64, true)]);
let declared = Arc::new(Schema::new(vec![Field::new("value", DataType::Int64, true)]));
let forward = builder.add_union(vec![left, right], Arc::clone(&declared)).unwrap();
let reversed = builder.add_union(vec![right, left], Arc::clone(&declared)).unwrap();
let without_first = builder.add_union(vec![right], Arc::clone(&declared)).unwrap();
for union in [forward, reversed, without_first] {
assert_eq!(builder.node_output_schema(union), Some(&declared));
}
let narrowed = Arc::new(Schema::new(vec![Field::new("value", DataType::Int64, false)]));
let error = builder.add_union(vec![left, right], narrowed).unwrap_err();
assert_eq!(error.code(), QueryErrorCode::InvalidPlan);
assert_eq!(error.fact_value("input_index"), Some("1"));
let retyped = Arc::new(Schema::new(vec![Field::new("value", DataType::Int32, true)]));
assert_eq!(builder.add_union(vec![left], retyped).unwrap_err().code(), QueryErrorCode::InvalidPlan);
}
}