#![allow(
clippy::result_large_err,
reason = "the typed graph cause stays unboxed and allocation-free, as in GraphQueryError"
)]
use super::GraphStoreError;
use crate::lifecycle::QueryControl;
use crate::lifecycle::Store;
use crate::property_graph::GraphName;
use crate::property_graph::query::completed::{
CompletedGraphResult, Executed, GraphBoundary, GraphQuery, GraphQueryError, GraphQueryExecutor,
GraphQueryOptions,
};
use crate::property_graph::query::plan::{
Expression, NodeFacts, Operator, Parameter, ParameterBinding, PlanBacking, PlanDescription,
PlanFootprint, PlanNodeId, RetainedRegion, VALIDATION_SCRATCH_BYTES,
};
use crate::property_graph::query::resources::{QueryArena, RetainedAllocation, RetentionInventory};
use crate::property_graph::query::runtime::RuntimeContext;
use std::mem::size_of;
#[derive(Default)]
pub struct GraphPlanBacking<'a> {
regions: Vec<RetainedRegion>,
owners: Vec<RetainedAllocation<'a>>,
}
impl<'a> GraphPlanBacking<'a> {
pub fn vec<T>(&mut self, value: &'a Vec<T>) -> Result<(), GraphStoreError> {
if value.capacity() != 0 {
self.regions
.push(RetainedRegion::vector(value).map_err(GraphQueryError::from)?);
self.owners
.push(RetainedAllocation::vector(value).map_err(GraphQueryError::from)?);
}
Ok(())
}
pub fn string(&mut self, value: &'a String) -> Result<(), GraphStoreError> {
if value.capacity() != 0 {
self.regions.push(
RetainedRegion::declared(value.as_ptr() as usize, value.capacity())
.map_err(GraphQueryError::from)?,
);
self.owners
.push(RetainedAllocation::string(value).map_err(GraphQueryError::from)?);
}
Ok(())
}
}
pub struct GraphQueryPlan<'a> {
pub operators: &'a Vec<Operator<'a>>,
pub expressions: &'a Vec<Expression<'a>>,
pub parameters: &'a Vec<Parameter<'a>>,
pub eager_searches: &'a Vec<PlanNodeId>,
pub root: PlanNodeId,
pub backing: &'a GraphPlanBacking<'a>,
pub bindings: &'a [ParameterBinding<'a>],
pub columns: &'a [&'a str],
}
impl Store {
pub fn graph_query(
&self,
control: &QueryControl,
options: &GraphQueryOptions,
plan: &GraphQueryPlan<'_>,
) -> Result<CompletedGraphResult, GraphStoreError> {
Ok(
self.execute_graph_statement(control, options, |runtime, executor| {
run_query_plan(runtime, executor, plan, options.slot_column_names)
})?,
)
}
#[doc(hidden)]
pub fn graph_query_with_boundary(
&self,
control: &QueryControl,
options: &GraphQueryOptions,
plan: &GraphQueryPlan<'_>,
boundary: &dyn GraphBoundary,
) -> Result<CompletedGraphResult, GraphStoreError> {
Ok(self.execute_graph_statement_with_boundary(
control,
options,
|runtime, executor| run_query_plan(runtime, executor, plan, options.slot_column_names),
Some(boundary),
)?)
}
}
fn run_query_plan<'lease, 'm, 'g>(
runtime: &mut RuntimeContext<'lease, 'm, 'g>,
executor: GraphQueryExecutor<'_, '_, '_, 'lease, 'm, 'g>,
plan: &GraphQueryPlan<'_>,
slot_column_names: bool,
) -> Result<Executed, GraphQueryError> {
let memory = runtime.memory();
let operators = plan.operators;
let expressions = plan.expressions;
let eager = plan.eager_searches;
let mut facts = QueryArena::new(memory, operators.len())?;
for _ in 0..operators.len() {
facts.push(NodeFacts::default())?;
}
let mut regions = Vec::new();
regions.push(RetainedRegion::vector(operators)?);
if expressions.capacity() != 0 {
regions.push(RetainedRegion::vector(expressions)?);
}
if eager.capacity() != 0 {
regions.push(RetainedRegion::vector(eager)?);
}
regions.push(RetainedRegion::declared(
facts.as_slice().as_ptr() as usize,
facts.heap_bytes(),
)?);
regions.extend(plan.backing.regions.iter().copied());
regions.sort();
let retained = regions
.iter()
.try_fold(0usize, |total, region| {
total.checked_add(region.end() - region.start())
})
.ok_or(GraphQueryError::contract("retained plan bytes overflow"))?;
let mut external = memory.reserve_external_capacity()?;
external.reserve_additional(
retained
+ VALIDATION_SCRATCH_BYTES
+ regions.capacity() * size_of::<RetainedRegion>()
+ size_of::<PlanDescription<'_>>(),
)?;
let description = PlanDescription {
operators,
expressions,
parameters: plan.parameters,
root: plan.root,
eager_searches: eager,
};
let (validated, facts_owner) = facts.validate_plan(
description,
PlanFootprint::declared(memory.reserved_bytes()),
PlanBacking::vector(®ions)?,
runtime.values(),
)?;
let mut owners = vec![RetainedAllocation::vector(operators)?, facts_owner];
if expressions.capacity() != 0 {
owners.push(RetainedAllocation::vector(expressions)?);
}
if eager.capacity() != 0 {
owners.push(RetainedAllocation::vector(eager)?);
}
owners.extend(plan.backing.owners.iter().copied());
let no_return = matches!(
plan.operators.get(plan.root.0 as usize).map(|o| o.kind),
Some(crate::property_graph::query::plan::OperatorKind::Mutate(_))
);
let slot_names = if slot_column_names && !no_return {
let root = validated
.facts(plan.root)
.ok_or(GraphQueryError::contract("missing root facts"))?;
let mut result = Vec::new();
result
.try_reserve_exact(root.width())
.map_err(|_| GraphQueryError::contract("column name capacity"))?;
for ordinal in 0..root.width() {
let (slot, _) = root
.slot_at(ordinal)
.ok_or(GraphQueryError::contract("missing root slot"))?;
result.push(format!("slot_{}", slot.0));
}
result
} else {
Vec::new()
};
let slot_refs = slot_names.iter().map(String::as_str).collect::<Vec<_>>();
let columns = if slot_column_names {
slot_refs.as_slice()
} else {
plan.columns
};
let names = columns
.iter()
.map(|column| GraphName::new(column))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| GraphQueryError::contract("invalid column name"))?;
let generated_bytes = slot_names
.iter()
.try_fold(0usize, |total, name| total.checked_add(name.capacity()))
.and_then(|bytes| bytes.checked_add(slot_names.capacity() * size_of::<String>()))
.and_then(|bytes| bytes.checked_add(slot_refs.capacity() * size_of::<&str>()))
.and_then(|bytes| bytes.checked_add(names.capacity() * size_of::<GraphName<'_>>()))
.and_then(|bytes| {
bytes.checked_add(owners.capacity() * size_of::<RetainedAllocation<'_>>())
})
.ok_or(GraphQueryError::contract("column capacity overflow"))?;
external.reserve_additional(generated_bytes)?;
executor.run(
runtime,
GraphQuery {
plan: &validated,
inventory: RetentionInventory::vector(&owners)?,
bindings: plan.bindings,
columns: &names,
},
)
}
#[cfg(test)]
mod tests;