use super::{
ArenaCapacity, RetainedView, RowBatch, RuntimeContext, RuntimeError, RuntimeLimits,
WorkCounters, WorkKind,
};
use crate::lifecycle::QueryControl;
use crate::property_graph::query::{
QueryValue,
plan::PlanNodeId,
resources::{QueryMemory, RuntimePlan},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PullState {
More,
Done,
}
pub trait PullOperator<'v, 'm, 'g, E = RuntimeError> {
fn node(&self) -> PlanNodeId;
fn prepare_search(
&mut self,
node: PlanNodeId,
context: &mut RuntimeContext<'v, 'm, 'g>,
) -> Result<(), E>;
fn pull(
&mut self,
context: &mut RuntimeContext<'v, 'm, 'g>,
output: &mut RowBatch<'v, 'm, 'g>,
) -> Result<PullState, E>;
}
pub trait OperatorFactory<'m, 'g: 'm, E = RuntimeError> {
type Operator<'v>: PullOperator<'v, 'm, 'g, E>;
fn build<'v>(
&mut self,
context: &mut RuntimeContext<'v, 'm, 'g>,
) -> Result<Self::Operator<'v>, E>;
}
#[derive(Clone, Copy)]
pub struct ExecutionCapacity {
pub batch_rows: usize,
pub result_rows: usize,
pub batch_payload_bytes: usize,
pub result_payload_bytes: usize,
pub batch: ArenaCapacity,
pub result: ArenaCapacity,
}
impl Default for ExecutionCapacity {
fn default() -> Self {
Self {
batch_rows: 256,
result_rows: 256,
batch_payload_bytes: 4 * 1024 * 1024,
result_payload_bytes: 4 * 1024 * 1024,
batch: ArenaCapacity::default(),
result: ArenaCapacity::default(),
}
}
}
pub struct PreparedRows<'v, 'm, 'g> {
rows: RowBatch<'v, 'm, 'g>,
}
impl PreparedRows<'_, '_, '_> {
pub fn rows(&self) -> usize {
self.rows.rows()
}
pub fn columns(&self) -> usize {
self.rows.columns()
}
pub fn value(&self, row: usize, column: usize) -> Option<QueryValue<'_>> {
self.rows.value(row, column)
}
pub fn payload_bytes(&self) -> usize {
self.rows.payload_bytes()
}
}
pub trait Completion<'m, 'g, E = RuntimeError> {
type Output;
fn complete<'v>(
&mut self,
rows: &PreparedRows<'v, 'm, 'g>,
context: &mut RuntimeContext<'v, 'm, 'g>,
) -> Result<FrozenOutput<Self::Output>, E>;
}
pub struct FrozenOutput<T> {
output: T,
rows: usize,
core_bytes: usize,
abi_bytes: usize,
}
impl<T> FrozenOutput<T> {
#[doc(hidden)]
pub fn into_parts(self) -> (T, usize, usize, usize) {
(self.output, self.rows, self.core_bytes, self.abi_bytes)
}
pub fn new(
output: T,
rows: usize,
core_bytes: usize,
abi_bytes: usize,
) -> Result<Self, RuntimeError> {
if rows > 65536 {
return Err(RuntimeError::Limit(WorkKind::CompletedRows));
}
if core_bytes > 4 * 1024 * 1024 {
return Err(RuntimeError::Limit(WorkKind::CompletedBytes));
}
if abi_bytes > 4 * 1024 * 1024 {
return Err(RuntimeError::Limit(WorkKind::CompletedAbiBytes));
}
Ok(Self {
output,
rows,
core_bytes,
abi_bytes,
})
}
}
pub struct Execution<T> {
pub output: T,
pub counters: WorkCounters,
pub peak_query_bytes: usize,
}
#[derive(Debug)]
pub struct RuntimeFailure<E = RuntimeError> {
pub operator: PlanNodeId,
pub error: E,
pub counters: WorkCounters,
}
impl<E: std::fmt::Display> std::fmt::Display for RuntimeFailure<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "graph operator {}: {}", self.operator.0, self.error)
}
}
impl<E: std::error::Error> std::error::Error for RuntimeFailure<E> {}
#[allow(
clippy::too_many_arguments,
clippy::result_large_err,
reason = "typed owned execution inputs and allocation-free full failure counters"
)]
pub fn execute<
'm,
'g,
E: From<RuntimeError>,
V: RetainedView,
O: for<'v> PullOperator<'v, 'm, 'g, E>,
C: Completion<'m, 'g, E>,
>(
view: V,
control: &QueryControl,
memory: &'m QueryMemory<'g>,
plan: &RuntimePlan<'_, '_, '_, '_, '_, '_>,
source: &mut O,
completion: &mut C,
capacity: ExecutionCapacity,
limits: RuntimeLimits,
) -> Result<Execution<C::Output>, RuntimeFailure<E>> {
let root = plan.plan().description().root;
let mut context =
RuntimeContext::new(&view, control, memory, limits).map_err(|error| RuntimeFailure {
operator: root,
error: error.into(),
counters: WorkCounters::default(),
})?;
let _view_charge =
memory
.reserve(std::mem::size_of::<V>())
.map_err(|error| RuntimeFailure {
operator: root,
error: E::from(error.into()),
counters: context.counters(),
})?;
drain(&mut context, plan, source, completion, capacity)
}
#[allow(
clippy::too_many_arguments,
clippy::result_large_err,
reason = "typed owned execution inputs and allocation-free failure counters"
)]
pub fn execute_factory<
'm,
'g,
E: From<RuntimeError>,
V: RetainedView,
F: OperatorFactory<'m, 'g, E>,
C: Completion<'m, 'g, E>,
>(
view: V,
control: &QueryControl,
memory: &'m QueryMemory<'g>,
plan: &RuntimePlan<'_, '_, '_, '_, '_, '_>,
factory: &mut F,
completion: &mut C,
capacity: ExecutionCapacity,
limits: RuntimeLimits,
) -> Result<Execution<C::Output>, RuntimeFailure<E>> {
let root = plan.plan().description().root;
let mut context =
RuntimeContext::new(&view, control, memory, limits).map_err(|error| RuntimeFailure {
operator: root,
error: error.into(),
counters: WorkCounters::default(),
})?;
let _view_charge =
memory
.reserve(std::mem::size_of::<V>())
.map_err(|error| RuntimeFailure {
operator: root,
error: E::from(error.into()),
counters: context.counters(),
})?;
if !plan.belongs_to(memory) {
return Err(RuntimeFailure {
operator: root,
error: RuntimeError::Batch.into(),
counters: context.counters(),
});
}
let mut source = factory
.build(&mut context)
.map_err(|error| RuntimeFailure {
operator: root,
error,
counters: context.counters(),
})?;
drain(&mut context, plan, &mut source, completion, capacity)
}
#[allow(
clippy::result_large_err,
reason = "allocation-free full failure counters"
)]
pub fn execute_in<
'v,
'm,
'g,
E: From<RuntimeError>,
O: PullOperator<'v, 'm, 'g, E>,
C: Completion<'m, 'g, E>,
>(
context: &mut RuntimeContext<'v, 'm, 'g>,
plan: &RuntimePlan<'_, '_, '_, '_, '_, '_>,
source: &mut O,
completion: &mut C,
capacity: ExecutionCapacity,
) -> Result<Execution<C::Output>, RuntimeFailure<E>> {
drain(context, plan, source, completion, capacity)
}
#[allow(
clippy::result_large_err,
reason = "allocation-free full failure counters"
)]
fn drain<
'v,
'm,
'g,
E: From<RuntimeError>,
O: PullOperator<'v, 'm, 'g, E>,
C: Completion<'m, 'g, E>,
>(
context: &mut RuntimeContext<'v, 'm, 'g>,
plan: &RuntimePlan<'_, '_, '_, '_, '_, '_>,
source: &mut O,
completion: &mut C,
capacity: ExecutionCapacity,
) -> Result<Execution<C::Output>, RuntimeFailure<E>> {
let root = plan.plan().description().root;
let memory = context.memory();
let mut diagnostic = root;
let result: Result<C::Output, E> = (|| {
context.checkpoint()?;
if !plan.belongs_to(memory)
|| source.node() != root
|| capacity.result_rows > 65536
|| capacity.result_payload_bytes > 4 * 1024 * 1024
{
return Err(RuntimeError::Batch.into());
}
let width = plan.plan().facts(root).ok_or(RuntimeError::Batch)?.width();
let _driver = memory
.reserve(std::mem::size_of::<ExecutionCapacity>() + std::mem::size_of::<WorkCounters>())
.map_err(|error| E::from(RuntimeError::from(error)))?;
let mut batch = RowBatch::with_arenas(
context,
width,
capacity.batch_rows,
capacity.batch_payload_bytes,
capacity.batch,
)?;
let mut prepared = PreparedRows {
rows: RowBatch::storage(
context,
width,
capacity.result_rows,
capacity.result_payload_bytes,
capacity.result,
)?,
};
for node in plan.plan().description().eager_searches {
diagnostic = *node;
context.charge(WorkKind::SearchInvocations, 1)?;
source.prepare_search(*node, context)?;
context.checkpoint()?;
}
diagnostic = root;
loop {
context.checkpoint()?;
batch.clear();
let state = source.pull(context, &mut batch)?;
context.checkpoint()?;
if state == PullState::More && batch.rows() == 0 {
return Err(RuntimeError::Batch.into());
}
for row in 0..batch.rows() {
context.check_work(WorkKind::CompletedRows, 1)?;
context.check_work(
WorkKind::PreparedPayloadBytes,
batch.row_payload_bytes(row)? as u64,
)?;
context.charge(WorkKind::RowsIn, 1)?;
let previous = prepared.rows.payload_bytes();
prepared.rows.copy_row(&batch, row, context)?;
context.charge(WorkKind::CompletedRows, 1)?;
context.charge(
WorkKind::PreparedPayloadBytes,
(prepared.rows.payload_bytes() - previous) as u64,
)?;
}
if state == PullState::Done {
break;
}
}
context.checkpoint()?;
let frozen = completion.complete(&prepared, context)?;
if frozen.rows != prepared.rows() {
return Err(RuntimeError::Batch.into());
}
context.charge(WorkKind::CompletedBytes, frozen.core_bytes as u64)?;
context.charge(WorkKind::CompletedAbiBytes, frozen.abi_bytes as u64)?;
context.checkpoint()?;
Ok(frozen.output)
})();
match result {
Ok(output) => Ok(Execution {
output,
counters: context.counters(),
peak_query_bytes: memory.peak_reserved_bytes(),
}),
Err(error) => Err(RuntimeFailure {
operator: diagnostic,
error,
counters: context.counters(),
}),
}
}
#[cfg(all(test, feature = "graph-cypher"))]
mod error_transport;