use crate::graph::GraphStorage;
use crate::traversal::side_effect::SideEffectValue;
use crate::*;
pub(crate) mod reason {
pub(crate) const TOO_MANY: &str = "aggregate() and store() take a single by()";
}
pub(crate) fn push_side_effect_n<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
key: &CompactString,
value: TraverserValue<S>,
n: u64,
step: &str,
) -> TraverserResult<()> {
let limits = crate::traversal::bulk::ExpansionLimits::of(&context.options);
let storage: &S = context.storage;
let side_effect = &mut context.side_effect;
let deadline = &mut context.deadline;
let too_many = |requested: u64| {
Box::new(TraverserError::BulkExpansionLimit {
step: step.to_string(),
requested,
limit: limits.max_values,
})
};
let copies = if n > 1 {
limits.check_copies(&value, n, step)?;
usize::try_from(n).map_err(|_| too_many(n))?
} else {
1
};
if let Some(SideEffectValue::Set(items)) = side_effect.get_mut(key) {
let value = match value {
TraverserValue::VertexProperty(_, _, PropertyHandleKind::Value)
| TraverserValue::EdgeProperty(_, _, PropertyHandleKind::Value) => {
TraverserValue::Computed(value.into_node_property(storage)?, None)
}
other => other,
};
items.insert(value);
return Ok(());
}
if let Some(SideEffectValue::Reduced { pending, .. }) = side_effect.get_mut(key) {
let value = value.into_node_property(storage)?;
pending.try_reserve(copies).map_err(|_| too_many(n))?;
return crate::traversal::bulk::push_copies(pending, value, n, deadline);
}
let entry = side_effect
.entry(key.clone())
.or_insert_with(|| SideEffectValue::List(Vec::new()));
if let SideEffectValue::Value(_) = entry {
let SideEffectValue::Value(existing) =
std::mem::replace(entry, SideEffectValue::List(Vec::new()))
else {
return Ok(());
};
if let SideEffectValue::List(items) = entry {
items.push(existing);
}
}
if let SideEffectValue::List(items) = entry {
items.try_reserve(copies).map_err(|_| too_many(n))?;
crate::traversal::bulk::push_copies(items, value, n, deadline)?;
}
Ok(())
}
pub(crate) fn flush_reduced<S: GraphStorage>(
side_effect: &mut std::collections::HashMap<CompactString, SideEffectValue<S>>,
key: &str,
) -> TraverserResult<()> {
let Some(SideEffectValue::Reduced { op, value, pending }) = side_effect.get_mut(key) else {
return Ok(());
};
if pending.is_empty() {
return Ok(());
}
let items = std::mem::take(pending);
let mut acc = std::mem::replace(value, ElementProperty::Null);
match op {
Operator::Assign | Operator::AddAll => {
acc = op.apply_owned(acc, ElementProperty::Array(items))?;
}
_ => {
for item in items {
acc = op.apply_owned(acc, item)?;
}
}
}
*value = acc;
Ok(())
}
struct StepAggregate<'g, S: GraphStorage + 'g> {
key: CompactString,
by: Option<By<'g, S>>,
by_check: ByModulatorCheck,
}
impl<'g, S: GraphStorage + 'g> Clone for StepAggregate<'g, S> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
by: self.by.clone(),
by_check: self.by_check.clone(),
}
}
}
impl<'g, S: GraphStorage + 'g> fmt::Display for StepAggregate<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut by = String::new();
if let Some(b) = self.by.as_ref() {
by.push_str(&crate::traversal::render_by(b));
}
write!(
f,
"aggregate({}){}",
crate::value::literal::rhai_string(&self.key),
by
)
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn aggregate(self, key: &str) -> Self {
self.push_step(GraphStep::SideEffect(
"aggregate",
Box::new(StepAggregate {
key: CompactString::new(key),
by: None,
by_check: ByModulatorCheck::default(),
}),
))
}
}
impl __ {
pub fn aggregate<'g, S: GraphStorage + 'g>(key: &str) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().aggregate(key)
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn aggregate(self, key: &str) -> Self {
let step = GraphStep::SideEffect(
"aggregate",
Box::new(StepAggregate {
key: CompactString::new(key),
by: None,
by_check: ByModulatorCheck::default(),
}),
);
self.push_step(step)
}
}
impl<'g, S: GraphStorage + 'g> GraphStepSideEffect<'g, S> for StepAggregate<'g, S> {
fn materializes(&self) -> bool {
true
}
fn declares_side_effect_key(&self) -> Option<&str> {
Some(self.key.as_str())
}
#[inline]
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<()> {
self.by_check.check("aggregate")?;
let resolved = match &self.by {
Some(by) => {
TraverserValue::fetch_traverser_value_by(context, source, &source.value, by)?
}
None => Some(source.value.clone()),
};
if let Some(resolved) = resolved {
push_side_effect_n(context, &self.key, resolved, source.bulk, "aggregate")?;
}
Ok(())
}
fn finish_batch(&self, context: &mut TraverserContext<'_, S>) -> TraverserResult<()> {
flush_reduced(&mut context.side_effect, self.key.as_str())
}
fn push_modulator_by(&mut self, args: By<'g, S>, _modulator_name: &str) -> ByModulation {
self.by_check.observe(&args);
if self.by.is_some() {
self.by_check.reject_last(reason::TOO_MANY);
}
self.by = Some(args);
ByModulation::Accepted
}
fn child_traversals_mut(&mut self) -> Vec<&mut crate::traversal::AnonymousTraversal<'g, S>> {
self.by
.as_mut()
.and_then(By::traversal_mut)
.into_iter()
.collect()
}
fn analyze_nested_paths(
&mut self,
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
_downstream: &crate::traversal::optimizer::path_analysis::PathRequirement,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
match &mut self.by {
Some(by) => by.analyze_paths(analysis),
None => crate::traversal::optimizer::path_analysis::PathRequirement::None,
}
}
fn clone_box(&self) -> Box<dyn GraphStepSideEffect<'g, S> + 'g> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v("1").out("knows").aggregate("friends").cap("friends").to_list(),
rhai: [
r#"g.V("1").out("knows").aggregate("friends").cap("friends")"#,
r#"g.v("1").out("knows").aggregate("friends").cap("friends")"#,
],
assert: |res| {
assert_eq!(res.len(), 1);
match &res[0] {
ElementProperty::Array(items) => assert_eq!(items.len(), 2),
_ => panic!(),
}
},
);
}
#[test]
fn by_name() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v("1").out("knows").aggregate("names").by("name").cap("names").to_list(),
rhai: [
r#"g.V("1").out("knows").aggregate("names").by("name").cap("names")"#,
r#"g.v("1").out("knows").aggregate("names").by("name").cap("names")"#,
],
assert: |res| {
match &res[0] {
ElementProperty::Array(items) => {
let names: Vec<_> = items.iter().map(|v| v.as_string().unwrap()).collect();
assert!(names.contains(&"vadas"));
assert!(names.contains(&"josh"));
}
_ => panic!(),
}
},
);
}
#[test]
fn passthrough_does_not_filter() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).aggregate("all").count().to_list(),
rhai: [
r#"g.V().aggregate("all").count()"#,
r#"g.v().aggregate("all").count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(6)),
);
}
#[test]
fn duplicates_preserved() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let result = g
.v(None)
.has_label("person")
.aggregate("labels")
.by(By::Label)
.cap("labels")
.next()
.unwrap()
.unwrap();
match result {
ElementProperty::Array(items) => {
assert_eq!(items.len(), 4);
assert!(items.iter().all(|v| v.as_string() == Some("person")));
}
_ => panic!(),
}
}
#[test]
fn where_within_side_effect_matches() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v("1")
.out("knows")
.aggregate("friends")
.where_t(__::where_p(vec!["friends"]))
.count()
.to_list(),
rhai: [
r#"g.V("1").out("knows").aggregate("friends").where(P.within("friends")).count()"#,
r#"g.v("1").out("knows").aggregate("friends").where(P.within("friends")).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(2)),
);
}
#[test]
fn where_without_side_effect_excludes() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v("1")
.out("knows")
.aggregate("friends")
.in_("knows")
.where_t(__::not(__::where_p(vec!["friends"])))
.count()
.to_list(),
rhai: [
r#"g.V("1").out("knows").aggregate("friends").in("knows").where(__.not(__.where(P.within("friends")))).count()"#,
r#"g.v("1").out("knows").aggregate("friends").in("knows").where(__.not(__.where(P.within("friends")))).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(2)),
);
}
#[test]
fn roadmap_acceptance_query_literal() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g
.v("1")
.out("knows")
.aggregate("friends")
.out("knows")
.where_t(__::not(__::where_p(vec!["friends"])))
.count()
.to_list(),
rhai: [
r#"g.V("1").out("knows").aggregate("friends").out("knows").where(__.not(__.where(P.within("friends")))).count()"#,
r#"g.v("1").out("knows").aggregate("friends").out("knows").where(__.not(__.where(P.within("friends")))).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(0)),
);
}
#[test]
fn profile() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let result = g.v(None).aggregate("x").cap("x").profile().unwrap();
assert!(result.to_string().contains("aggregate("));
}
}