use super::*;
use crate::property_graph::query::{
MAX_LIST_DEPTH, MAX_LIST_ELEMENTS, MAX_QUERY_BYTES, QueryError, QueryList, list::ListArena,
};
#[derive(Clone, Copy)]
pub enum Aggregate {
Avg {
slot: SlotId,
distinct: bool,
},
Sum {
slot: SlotId,
distinct: bool,
},
Min {
slot: SlotId,
},
Max {
slot: SlotId,
},
CountAll,
Count {
slot: SlotId,
distinct: bool,
},
Collect {
slot: SlotId,
distinct: bool,
},
}
#[derive(Clone, Copy)]
pub struct AggregateColumn {
pub output: SlotId,
pub operation: Aggregate,
}
impl<'v, 'm, 'g> Rows<'v, 'm, 'g> {
pub fn aggregate(
self,
keys: &[SlotProjection],
aggregates: &[AggregateColumn],
capacity: StorageCapacity,
context: &mut RuntimeContext<'v, 'm, 'g>,
) -> Result<Self, RuntimeError> {
if !self.belongs_to(context)
|| keys
.len()
.checked_add(aggregates.len())
.is_none_or(|width| width > 256)
{
return Err(RuntimeError::Batch);
}
let representatives = Self::new(context, self.schema().slots(), self.capacity)?;
let mut state =
streaming::StreamAggregate::new(representatives, keys, aggregates, capacity, context)?;
let mut values = QueryArena::new(context.memory(), self.schema().slots().len())?;
for row in 0..self.len() {
context.charge(WorkKind::OperatorRows, 1)?;
context.charge(WorkKind::RowsIn, 1)?;
values.clear();
for column in 0..self.schema().slots().len() {
values.push(self.value(row, column).ok_or(RuntimeError::Batch)?)?;
}
state.push(values.as_slice(), aggregates, context)?;
}
state.finish(keys, aggregates, context)
}
}
struct Gathered<'a, 'v, 'm, 'g> {
rows: &'a Rows<'v, 'm, 'g>,
selected: &'a [usize],
column: usize,
}
impl std::fmt::Debug for Gathered<'_, '_, '_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CollectedValues")
.field("len", &self.selected.len())
.finish()
}
}
impl ListArena for Gathered<'_, '_, '_, '_> {
fn value(&self, index: usize) -> Option<QueryValue<'_>> {
self.rows.cell(*self.selected.get(index)?, self.column)
}
}
pub(super) fn collect<'v, 'm, 'g>(
rows: &Rows<'v, 'm, 'g>,
column: usize,
selected: &[usize],
output: &mut RowBatch<'v, 'm, 'g>,
context: &mut RuntimeContext<'v, 'm, 'g>,
) -> Result<(), RuntimeError> {
if selected.len() > MAX_LIST_ELEMENTS {
return Err(QueryError::ListLimit.into());
}
let gathered = Gathered {
rows,
selected,
column,
};
let (mut elements, mut depth, mut bytes, mut entities, mut nodes) = (
selected.len(),
1,
selected
.len()
.checked_mul(std::mem::size_of::<QueryValue<'_>>())
.ok_or(QueryError::ListLimit)?,
false,
!selected.is_empty(),
);
for index in 0..selected.len() {
context.checkpoint()?;
let value = gathered.value(index).ok_or(RuntimeError::Batch)?;
value.validate(context.values())?;
entities |= value.view().is_some();
nodes &= matches!(value, QueryValue::NodeRef(_));
match value {
QueryValue::Map(crate::property_graph::query::QueryMap { entries: list })
| QueryValue::List(list) => {
elements = elements
.checked_add(list.elements())
.ok_or(QueryError::ListLimit)?;
depth = depth.max(list.depth() + 1);
bytes = bytes
.checked_add(list.borrowed_bytes())
.ok_or(QueryError::ListLimit)?;
}
QueryValue::String(text) => {
bytes = bytes.checked_add(text.len()).ok_or(QueryError::ListLimit)?
}
_ => {}
}
if elements > MAX_LIST_ELEMENTS || depth > MAX_LIST_DEPTH {
return Err(QueryError::ListLimit.into());
}
}
if nodes {
let mut ids = QueryArena::new(context.memory(), selected.len())?;
for index in 0..selected.len() {
let Some(QueryValue::NodeRef(node)) = gathered.value(index) else {
return Err(RuntimeError::Batch);
};
context.charge(WorkKind::CopiedBytes, 16)?;
ids.push(node.id())?;
}
let list = QueryList::nodes(context.view(), ids.as_slice(), context.values())?;
output.push_row(&[QueryValue::List(list)], context)
} else {
if bytes > MAX_QUERY_BYTES {
return Err(QueryError::ListLimit.into());
}
let list = QueryList::arena(
&gathered,
0,
selected.len(),
elements,
depth,
bytes,
if entities { Some(context.view()) } else { None },
);
output.push_row(&[QueryValue::List(list)], context)
}
}