use crate::error::{TraverserError, TraverserResult};
use crate::graph::GraphStorage;
use crate::traversal::context::{Deadline, ExecutionOptions, tick_deadline};
use crate::traversal::memory_budget::MemoryBudget;
use crate::traversal::{Traverser, TraverserContext};
use crate::value::{ElementProperty, PathRef, SackRef, TraverserValue};
use fnv::FnvBuildHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
fn overflow(step: &str) -> Box<TraverserError> {
Box::new(TraverserError::BulkOverflow {
step: step.to_string(),
})
}
pub(crate) fn total_bulk<S: GraphStorage>(
context: &TraverserContext<'_, S>,
items: &[Traverser<S>],
step: &str,
) -> TraverserResult<u64> {
if !context.bulk_active {
return Ok(items.len() as u64);
}
sum_bulks(items, step)
}
fn sum_bulks<S: GraphStorage>(items: &[Traverser<S>], step: &str) -> TraverserResult<u64> {
items
.iter()
.try_fold(0u64, |total, item| total.checked_add(item.bulk))
.ok_or_else(|| overflow(step))
}
pub(crate) fn mul_bulk(a: u64, b: u64, step: &str) -> TraverserResult<u64> {
a.checked_mul(b).ok_or_else(|| overflow(step))
}
pub(crate) fn count_property(total: u64, step: &str) -> TraverserResult<ElementProperty> {
let count = i64::try_from(total).map_err(|_| overflow(step))?;
Ok(ElementProperty::Number(count.into()))
}
pub(crate) fn scale_count(count: u64, factor: u64, step: &str) -> TraverserResult<u64> {
count.checked_mul(factor).ok_or_else(|| overflow(step))
}
pub(crate) fn slice_units<S: GraphStorage>(
items: Vec<Traverser<S>>,
skip: u64,
take: u64,
) -> Vec<Traverser<S>> {
let upper = skip.saturating_add(take);
let mut position: u64 = 0;
let mut kept = Vec::new();
for mut item in items {
if position >= upper {
break;
}
let end = position.saturating_add(item.bulk);
let low = position.max(skip);
let high = end.min(upper);
position = end;
if low < high {
item.bulk = high - low;
kept.push(item);
}
}
kept
}
pub(crate) fn tail_units<S: GraphStorage>(items: Vec<Traverser<S>>, n: u64) -> Vec<Traverser<S>> {
let total = items
.iter()
.fold(0u64, |total, item| total.saturating_add(item.bulk));
slice_units(items, total.saturating_sub(n), n)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ExpansionLimits {
pub(crate) max_values: u64,
pub(crate) max_bytes: u64,
pub(crate) memory: Option<MemoryBudget>,
}
impl ExpansionLimits {
#[inline]
pub(crate) fn of(options: &ExecutionOptions) -> Self {
Self {
max_values: options.bulk_max_expansion,
max_bytes: options.max_materialized_bytes,
memory: options.memory,
}
}
pub(crate) const UNBOUNDED: Self = Self {
max_values: u64::MAX,
max_bytes: u64::MAX,
memory: None,
};
#[cold]
fn too_many(&self, requested: u64, step: &str) -> Box<TraverserError> {
Box::new(TraverserError::BulkExpansionLimit {
step: step.to_string(),
requested,
limit: self.max_values,
})
}
pub(crate) fn check_copies<S: GraphStorage>(
&self,
value: &TraverserValue<S>,
copies: u64,
step: &str,
) -> TraverserResult<()> {
if copies > self.max_values {
return Err(self.too_many(copies, step));
}
let each = std::mem::size_of::<TraverserValue<S>>() as u64
+ crate::traversal::step::profile::estimate_traverser_value_bytes(value);
let bytes = each.saturating_mul(copies);
if bytes > self.max_bytes {
return Err(materialized_bytes_error(self.max_bytes, bytes));
}
self.check_memory(bytes)
}
#[inline]
fn check_memory(&self, bytes: u64) -> TraverserResult<()> {
match &self.memory {
None => Ok(()),
Some(budget) => budget.check(bytes),
}
}
}
#[cold]
pub(crate) fn materialized_bytes_error(max: u64, observed: u64) -> Box<TraverserError> {
Box::new(TraverserError::ResourceLimitExceeded {
limit: crate::traversal::context::MAX_MATERIALIZED_BYTES.to_string(),
max: Some(max),
observed: Some(observed),
})
}
pub(crate) fn expand<S: GraphStorage>(
items: Vec<Traverser<S>>,
limits: ExpansionLimits,
deadline: &mut Option<Deadline>,
step: &str,
) -> TraverserResult<Vec<Traverser<S>>> {
if items.iter().all(|item| item.bulk == 1) {
return Ok(items);
}
let total = sum_bulks(&items, step)?;
if total > limits.max_values {
return Err(limits.too_many(total, step));
}
let each_traverser = std::mem::size_of::<Traverser<S>>() as u64;
let mut bytes: u64 = 0;
for item in &items {
let each = each_traverser
+ crate::traversal::step::profile::estimate_traverser_value_bytes(&item.value);
bytes = bytes.saturating_add(each.saturating_mul(item.bulk));
if bytes > limits.max_bytes {
return Err(materialized_bytes_error(limits.max_bytes, bytes));
}
}
limits.check_memory(bytes)?;
let capacity = usize::try_from(total).map_err(|_| limits.too_many(total, step))?;
let mut expanded = Vec::new();
expanded
.try_reserve_exact(capacity)
.map_err(|_| limits.too_many(total, step))?;
for item in items {
let copies = item.bulk;
for _ in 1..copies {
tick_deadline(deadline)?;
expanded.push(Traverser::with_parts(
item.value.clone(),
item.path,
1,
item.sack,
));
}
tick_deadline(deadline)?;
expanded.push(Traverser::with_parts(item.value, item.path, 1, item.sack));
}
Ok(expanded)
}
#[inline]
pub(crate) fn expand_in<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
items: Vec<Traverser<S>>,
step: &str,
) -> TraverserResult<Vec<Traverser<S>>> {
expand(
items,
ExpansionLimits::of(&context.options),
&mut context.deadline,
step,
)
}
pub(crate) fn expand_values_in<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
items: Vec<Traverser<S>>,
step: &str,
) -> TraverserResult<Vec<crate::value::TraverserValue<S>>> {
Ok(expand_in(context, items, step)?
.into_iter()
.map(|traverser| traverser.value)
.collect())
}
pub(crate) fn push_copies<T: Clone>(
bucket: &mut Vec<T>,
value: T,
copies: u64,
deadline: &mut Option<Deadline>,
) -> TraverserResult<()> {
for _ in 1..copies {
tick_deadline(deadline)?;
bucket.push(value.clone());
}
bucket.push(value);
Ok(())
}
pub(crate) struct MergeKey<'a, S: GraphStorage> {
value: &'a TraverserValue<S>,
path: PathRef,
sack: SackRef,
}
impl<'a, S: GraphStorage> MergeKey<'a, S> {
#[inline]
pub(crate) fn of(traverser: &'a Traverser<S>) -> Option<MergeKey<'a, S>> {
let mergeable = match &traverser.value {
TraverserValue::Vertex(_)
| TraverserValue::Edge(_)
| TraverserValue::VertexProperty(_, _, _)
| TraverserValue::EdgeProperty(_, _, _) => true,
TraverserValue::Computed(scalar, _) => matches!(
scalar,
ElementProperty::Null
| ElementProperty::Boolean(_)
| ElementProperty::Number(_)
| ElementProperty::String(_)
| ElementProperty::Uuid(_)
),
TraverserValue::Path(_)
| TraverserValue::Record { .. }
| TraverserValue::Object(_)
| TraverserValue::PathProperty(_)
| TraverserValue::Entry(_)
| TraverserValue::Array(_)
| TraverserValue::Schema(_)
| TraverserValue::Graph(_) => false,
};
mergeable.then_some(MergeKey {
value: &traverser.value,
path: traverser.path,
sack: traverser.sack,
})
}
}
#[inline(always)]
fn key_for<'a, S: GraphStorage, const OPERATOR: bool, const ONE: bool>(
traverser: &'a Traverser<S>,
) -> Option<MergeKey<'a, S>> {
let mut key = MergeKey::of(traverser)?;
if OPERATOR {
key.sack = SackRef::NONE;
} else if ONE && !traverser.sack.is_none() {
return None;
}
Some(key)
}
impl<S: GraphStorage> PartialEq for MergeKey<'_, S> {
fn eq(&self, other: &Self) -> bool {
if self.path != other.path || self.sack != other.sack {
return false;
}
match (self.value, other.value) {
(TraverserValue::Vertex(a), TraverserValue::Vertex(b)) => a == b,
(TraverserValue::Edge(a), TraverserValue::Edge(b)) => a == b,
(
TraverserValue::VertexProperty(ka, a, _),
TraverserValue::VertexProperty(kb, b, _),
) => a == b && ka == kb,
(TraverserValue::EdgeProperty(ka, a, _), TraverserValue::EdgeProperty(kb, b, _)) => {
a == b && ka == kb
}
(TraverserValue::Computed(a, ta), TraverserValue::Computed(b, tb)) => {
ta == tb && a == b
}
_ => false,
}
}
}
impl<S: GraphStorage> Eq for MergeKey<'_, S> {}
impl<S: GraphStorage> Hash for MergeKey<'_, S> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
if !self.sack.is_none() {
self.sack.hash(state);
}
match self.value {
TraverserValue::Vertex(id) => {
0u8.hash(state);
id.hash(state);
}
TraverserValue::Edge(id) => {
1u8.hash(state);
id.hash(state);
}
TraverserValue::VertexProperty(key, id, _) => {
2u8.hash(state);
key.hash(state);
id.hash(state);
}
TraverserValue::EdgeProperty(key, id, _) => {
3u8.hash(state);
key.hash(state);
id.hash(state);
}
TraverserValue::Computed(scalar, tag) => {
4u8.hash(state);
scalar.hash(state);
tag.hash(state);
}
TraverserValue::Path(_)
| TraverserValue::Record { .. }
| TraverserValue::Object(_)
| TraverserValue::PathProperty(_)
| TraverserValue::Entry(_)
| TraverserValue::Array(_)
| TraverserValue::Schema(_)
| TraverserValue::Graph(_) => 5u8.hash(state),
}
}
}
const MERGE_PROBE: usize = 4096;
const MERGE_PROBE_RATIO: usize = 16;
pub(crate) fn merge_saves_work<'g, S: GraphStorage + 'g>(
context: &mut TraverserContext<'_, S>,
input: &[Traverser<S>],
next: &dyn crate::traversal::step::GraphStepFlatMap<'g, S>,
) -> bool {
if input.len() < MERGE_PROBE {
return true;
}
let sample = &input[..MERGE_PROBE];
#[allow(clippy::mutable_key_type)]
let mut groups: HashMap<MergeKey<'_, S>, Option<u64>, FnvBuildHasher> =
HashMap::with_capacity_and_hasher(sample.len(), FnvBuildHasher::default());
let mut saved: u64 = 0;
let needed = sample.len() as u64 / 2;
for traverser in sample {
let Some(key) = MergeKey::of(traverser) else {
continue;
};
let fan_out = match groups.entry(key) {
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(None);
continue;
}
std::collections::hash_map::Entry::Occupied(mut slot) => match *slot.get() {
Some(fan_out) => fan_out,
None => {
let fan_out = match next.count(context, traverser.clone()) {
Ok(total) => total / traverser.bulk().max(1),
Err(_) => return true,
};
slot.insert(Some(fan_out));
fan_out
}
},
};
saved = saved.saturating_add(fan_out.saturating_mul(traverser.bulk()));
if saved >= needed {
return true;
}
}
false
}
pub(crate) fn merge_traversers<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
input: Vec<Traverser<S>>,
step: &'static str,
) -> TraverserResult<Vec<Traverser<S>>> {
Ok(merge_dispatch(context, input, step, false)?.0)
}
pub(crate) fn merge_traversers_adaptive<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
input: Vec<Traverser<S>>,
step: &'static str,
) -> TraverserResult<(Vec<Traverser<S>>, bool)> {
merge_dispatch(context, input, step, true)
}
fn merge_dispatch<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
input: Vec<Traverser<S>>,
step: &'static str,
probe: bool,
) -> TraverserResult<(Vec<Traverser<S>>, bool)> {
match (context.sack_merge.is_some(), context.bulk_one) {
(true, true) => merge_inner::<S, true, true>(context, input, step, probe),
(true, false) => merge_inner::<S, true, false>(context, input, step, probe),
(false, true) => merge_inner::<S, false, true>(context, input, step, probe),
(false, false) => merge_inner::<S, false, false>(context, input, step, probe),
}
}
#[cold]
#[inline(never)]
fn combine_sacks<S: GraphStorage>(
context: &mut TraverserContext<'_, S>,
operator: crate::value::Operator,
left: SackRef,
right: SackRef,
) -> TraverserResult<SackRef> {
let sacks = &mut context.sacks;
let combined = operator
.apply_owned(sacks.get(left).clone(), sacks.get(right).clone())
.map_err(|error| Box::new(TraverserError::from(error)))?;
sacks.intern(combined)
}
fn merge_inner<S: GraphStorage, const OPERATOR: bool, const ONE: bool>(
context: &mut TraverserContext<'_, S>,
input: Vec<Traverser<S>>,
step: &'static str,
probe: bool,
) -> TraverserResult<(Vec<Traverser<S>>, bool)> {
if !context.bulk_merge || input.len() < 2 || context.path_mode.is_full() {
return Ok((input, true));
}
let reserve = if probe {
input.len().min(MERGE_PROBE)
} else {
input.len()
};
let mut target: Vec<usize> = Vec::with_capacity(reserve);
let mut bulks: Vec<u64> = Vec::with_capacity(reserve);
let sack_merge = context.sack_merge;
let mut acc: Vec<SackRef> = Vec::new();
if OPERATOR {
acc.reserve(reserve);
}
let mut merged = false;
{
#[allow(clippy::mutable_key_type)]
let mut first: HashMap<MergeKey<'_, S>, usize, FnvBuildHasher> =
HashMap::with_capacity_and_hasher(reserve, FnvBuildHasher::default());
let mut merges = 0usize;
for (index, traverser) in input.iter().enumerate() {
context.tick()?;
if probe && index == MERGE_PROBE && merges * MERGE_PROBE_RATIO < MERGE_PROBE {
return Ok((input, false));
}
bulks.push(traverser.bulk);
if OPERATOR {
acc.push(traverser.sack);
}
let Some(key) = key_for::<S, OPERATOR, ONE>(traverser) else {
target.push(index);
continue;
};
match first.entry(key) {
std::collections::hash_map::Entry::Occupied(slot) => {
let first_index = *slot.get();
if !ONE {
bulks[first_index] = bulks[first_index]
.checked_add(traverser.bulk)
.ok_or_else(|| overflow(step))?;
}
if OPERATOR && let Some(operator) = sack_merge {
acc[first_index] =
combine_sacks(context, operator, acc[first_index], traverser.sack)?;
}
target.push(first_index);
merged = true;
merges += 1;
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(index);
target.push(index);
}
}
}
}
if !merged {
return Ok((input, true));
}
if !ONE {
context.bulk_active = true;
}
let mut output = Vec::with_capacity(input.len());
for (index, mut traverser) in input.into_iter().enumerate() {
if target[index] == index {
traverser.bulk = bulks[index];
if OPERATOR {
traverser.sack = acc[index];
}
output.push(traverser);
}
}
Ok((output, true))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::TraversalGraph;
use crate::value::{PathRef, TraverserValue};
type T = Traverser<TraversalGraph>;
fn limits(max_values: u64) -> ExpansionLimits {
ExpansionLimits {
max_values,
max_bytes: u64::MAX,
memory: None,
}
}
fn item(n: i64, bulk: u64) -> T {
Traverser::with_parts(
TraverserValue::Computed(ElementProperty::Number(n.into()), None),
PathRef::NONE,
bulk,
SackRef::NONE,
)
}
fn number(t: &T) -> Option<i64> {
match &t.value {
TraverserValue::Computed(ElementProperty::Number(n), _) => n.as_i64(),
_ => None,
}
}
#[test]
fn mul_bulk_multiplies_and_reports_overflow() {
assert_eq!(mul_bulk(3, 4, "out").unwrap(), 12);
let err = mul_bulk(u64::MAX, 2, "out").unwrap_err();
assert!(matches!(*err, TraverserError::BulkOverflow { ref step } if step == "out"));
}
#[test]
fn count_property_rejects_totals_above_i64_max() {
assert_eq!(
count_property(7, "count").unwrap(),
ElementProperty::Number(7i64.into())
);
assert!(count_property(i64::MAX as u64, "count").is_ok());
let err = count_property(i64::MAX as u64 + 1, "count").unwrap_err();
assert!(matches!(*err, TraverserError::BulkOverflow { .. }));
}
#[test]
fn sums_add_bulks_and_detect_overflow() {
let items = vec![item(1, 2), item(2, 5)];
assert_eq!(sum_bulks(&items, "count").unwrap(), 7);
let items = vec![item(1, u64::MAX), item(2, 1)];
let err = sum_bulks(&items, "count").unwrap_err();
assert!(matches!(*err, TraverserError::BulkOverflow { .. }));
}
#[test]
fn expand_repeats_values_in_place_and_resets_bulk() {
let expanded = expand(
vec![item(1, 1), item(2, 3), item(3, 2)],
limits(100),
&mut None,
"fold",
)
.unwrap();
let values: Vec<_> = expanded.iter().map(|t| number(t).unwrap()).collect();
assert_eq!(values, vec![1, 2, 2, 2, 3, 3]);
assert!(expanded.iter().all(|t| t.bulk() == 1));
}
#[test]
fn expand_without_bulk_returns_the_input() {
let expanded = expand(vec![item(1, 1), item(2, 1)], limits(1), &mut None, "fold").unwrap();
assert_eq!(expanded.len(), 2);
}
#[test]
fn expand_enforces_the_limit() {
let err = expand(vec![item(1, 6), item(2, 5)], limits(10), &mut None, "fold").unwrap_err();
assert!(matches!(
*err,
TraverserError::BulkExpansionLimit { ref step, requested: 11, limit: 10 } if step == "fold"
));
assert_eq!(
expand(vec![item(1, 6), item(2, 4)], limits(10), &mut None, "fold")
.unwrap()
.len(),
10
);
}
#[test]
fn expand_enforces_the_byte_budget_before_it_allocates() {
let each = std::mem::size_of::<T>() as u64;
let bounded = |max_bytes| ExpansionLimits {
max_values: u64::MAX,
max_bytes,
memory: None,
};
let expanded = expand(vec![item(1, 5)], bounded(5 * each), &mut None, "fold").unwrap();
assert_eq!(expanded.len(), 5);
let err = expand(vec![item(1, 6)], bounded(5 * each), &mut None, "fold").unwrap_err();
assert!(matches!(
*err,
TraverserError::ResourceLimitExceeded { ref limit, max: Some(_), observed: Some(o) }
if limit == "traversal.max_materialized_bytes" && o == 6 * each
));
assert_eq!(
expand(vec![item(1, 1), item(2, 1)], bounded(1), &mut None, "fold")
.unwrap()
.len(),
2
);
let text = TraverserValue::<TraversalGraph>::Computed(
ElementProperty::String("x".repeat(1000).into()),
None,
);
assert!(bounded(10_000).check_copies(&text, 9, "aggregate").is_ok());
assert!(
bounded(10_000)
.check_copies(&text, 10, "aggregate")
.is_err()
);
let err = limits(3).check_copies(&text, 4, "aggregate").unwrap_err();
assert!(matches!(
*err,
TraverserError::BulkExpansionLimit { requested: 4, .. }
));
}
#[test]
fn expand_enforces_the_memory_budget_before_it_allocates() {
let each = std::mem::size_of::<T>() as u64;
let budgeted = |limit| ExpansionLimits {
max_values: u64::MAX,
max_bytes: u64::MAX,
memory: Some(MemoryBudget::new(limit)),
};
assert_eq!(
expand(vec![item(1, 5)], budgeted(5 * each), &mut None, "fold")
.unwrap()
.len(),
5
);
let err = expand(vec![item(1, 6)], budgeted(5 * each), &mut None, "fold").unwrap_err();
assert!(matches!(
*err,
TraverserError::ResourceLimitExceeded { ref limit, max: Some(max), observed: Some(o) }
if limit == "memory.limit" && max == 5 * each && o == 6 * each
));
let text = TraverserValue::<TraversalGraph>::Computed(
ElementProperty::String("x".repeat(1000).into()),
None,
);
assert!(
budgeted(10_000)
.check_copies(&text, 10, "aggregate")
.is_err()
);
}
#[test]
fn expand_checks_the_deadline_while_it_copies() {
let mut deadline =
crate::traversal::context::Deadline::new(std::time::Duration::from_nanos(1));
std::thread::sleep(std::time::Duration::from_millis(2));
let err = expand(
vec![item(1, 5000)],
ExpansionLimits::UNBOUNDED,
&mut deadline,
"fold",
)
.unwrap_err();
assert!(matches!(*err, TraverserError::Timeout { .. }));
}
#[test]
fn expand_reports_overflow_of_the_total() {
let err = expand(
vec![item(1, u64::MAX), item(2, 2)],
ExpansionLimits::UNBOUNDED,
&mut None,
"fold",
)
.unwrap_err();
assert!(matches!(*err, TraverserError::BulkOverflow { .. }));
}
#[test]
fn expand_maps_an_unreservable_total_to_the_limit_error() {
let err = expand(
vec![item(1, u64::MAX)],
ExpansionLimits::UNBOUNDED,
&mut None,
"fold",
)
.unwrap_err();
assert!(matches!(*err, TraverserError::BulkExpansionLimit { .. }));
}
mod merging {
use super::*;
use crate::graph::VertexHandle;
use crate::traversal::context::TraversalStrategy;
use std::collections::HashMap;
fn vertex(id: usize) -> T {
Traverser::from_value(TraverserValue::Vertex(VertexHandle::for_test(id as u32)))
}
fn computed(value: ElementProperty, tag: Option<usize>) -> T {
Traverser::from_value(TraverserValue::Computed(
value,
tag.map(|id| VertexHandle::for_test(id as u32)),
))
}
fn with_context<R>(
merge: bool,
run: impl FnOnce(&mut TraverserContext<'_, TraversalGraph>) -> R,
) -> R {
let mut graph = TraversalGraph::new();
let mut context =
TraverserContext::new(&mut graph, TraversalStrategy::none(), HashMap::new());
context.bulk_merge = merge;
run(&mut context)
}
fn bulks(items: &[T]) -> Vec<u64> {
items.iter().map(Traverser::bulk).collect()
}
#[test]
fn identical_vertices_merge_and_sum_in_first_occurrence_order() {
let merged = with_context(true, |context| {
let input = vec![vertex(2), vertex(1), vertex(2), vertex(2), vertex(1)];
let merged = merge_traversers(context, input, "barrier").unwrap();
assert!(context.bulk_active);
merged
});
let ids: Vec<_> = merged
.iter()
.map(|t| match t.value {
TraverserValue::Vertex(id) => id.index().index(),
_ => unreachable!(),
})
.collect();
assert_eq!(ids, vec![2, 1]);
assert_eq!(bulks(&merged), vec![3, 2]);
}
#[test]
fn different_sacks_never_merge_and_equal_sacks_do() {
let mut arena = crate::value::SackArena::new();
let one = arena.intern(ElementProperty::Number(1.into())).unwrap();
let two = arena.intern(ElementProperty::Number(2.into())).unwrap();
let with = |id: usize, sack: SackRef| {
let mut t = vertex(id);
t.sack = sack;
t
};
let merged = with_context(true, |context| {
let input = vec![
with(1, one),
with(1, two),
with(1, one),
with(1, SackRef::NONE),
with(1, two),
];
merge_traversers(context, input, "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![2, 2, 1]);
let sacks: Vec<_> = merged.iter().map(|t| t.sack).collect();
assert_eq!(sacks, vec![one, two, SackRef::NONE]);
}
#[test]
fn the_merge_key_follows_the_sack_rules() {
let mut arena = crate::value::SackArena::new();
let one = arena.intern(ElementProperty::Number(1.into())).unwrap();
let two = arena.intern(ElementProperty::Number(2.into())).unwrap();
let mut a = vertex(1);
a.sack = one;
let mut b = vertex(1);
b.sack = two;
fn default(t: &T) -> Option<MergeKey<'_, TraversalGraph>> {
key_for::<TraversalGraph, false, false>(t)
}
fn operator(t: &T) -> Option<MergeKey<'_, TraversalGraph>> {
key_for::<TraversalGraph, true, false>(t)
}
fn one_bulk(t: &T) -> Option<MergeKey<'_, TraversalGraph>> {
key_for::<TraversalGraph, false, true>(t)
}
assert!(default(&a).unwrap() != default(&b).unwrap());
assert!(default(&a).unwrap() == default(&a).unwrap());
assert!(operator(&a).unwrap() == operator(&b).unwrap());
assert!(one_bulk(&a).is_none());
assert!(one_bulk(&vertex(1)).is_some());
assert!(key_for::<TraversalGraph, true, true>(&a).is_some());
}
#[test]
fn a_merge_operator_combines_sacks_and_bulk_one_keeps_bulk_one() {
for bulk_one in [false, true] {
let mut arena = crate::value::SackArena::new();
let handles: Vec<_> = (1..=3)
.map(|n| arena.intern(ElementProperty::Number(n.into())).unwrap())
.collect();
let merged = with_context(true, |context| {
context.sacks = arena;
context.sack_merge = Some(crate::value::Operator::Sum);
context.bulk_one = bulk_one;
let input: Vec<_> = handles
.iter()
.map(|h| {
let mut t = vertex(1);
t.sack = *h;
t
})
.collect();
let merged = merge_traversers(context, input, "barrier").unwrap();
assert_eq!(context.bulk_active, !bulk_one);
let sack = context.sacks.get(merged[0].sack).clone();
assert_eq!(sack, ElementProperty::Number(6.into()));
merged
});
assert_eq!(bulks(&merged), vec![if bulk_one { 1 } else { 3 }]);
}
}
#[test]
fn existing_bulks_are_summed() {
let merged = with_context(true, |context| {
let mut a = vertex(1);
a.bulk = 5;
let mut b = vertex(1);
b.bulk = 7;
merge_traversers(context, vec![a, b], "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![12]);
}
#[test]
fn different_paths_do_not_merge() {
let merged = with_context(true, |context| {
let path = context
.paths
.push(
PathRef::NONE,
TraverserValue::Vertex(VertexHandle::for_test(0)),
None,
)
.unwrap();
let a = vertex(1);
let b = Traverser::with_parts(
TraverserValue::Vertex(VertexHandle::for_test(1)),
path,
1,
SackRef::NONE,
);
let result = merge_traversers(context, vec![a, b], "barrier").unwrap();
assert!(!context.bulk_active);
result
});
assert_eq!(bulks(&merged), vec![1, 1]);
}
#[test]
fn computed_values_of_different_number_types_do_not_merge() {
let merged = with_context(true, |context| {
let input = vec![
computed(ElementProperty::Number(1i64.into()), None),
computed(ElementProperty::Number(1.0f64.into()), None),
computed(ElementProperty::Number(1i64.into()), None),
];
merge_traversers(context, input, "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![2, 1]);
}
#[test]
fn computed_values_with_different_tags_do_not_merge() {
let merged = with_context(true, |context| {
let input = vec![
computed(ElementProperty::String("x".into()), Some(1)),
computed(ElementProperty::String("x".into()), Some(2)),
computed(ElementProperty::String("x".into()), None),
computed(ElementProperty::String("x".into()), Some(1)),
];
merge_traversers(context, input, "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![2, 1, 1]);
}
#[test]
fn containers_pass_through() {
let merged = with_context(true, |context| {
let array = || Traverser::from_value(TraverserValue::Array(vec![]));
let result = merge_traversers(context, vec![array(), array()], "barrier").unwrap();
assert!(!context.bulk_active);
result
});
assert_eq!(bulks(&merged), vec![1, 1]);
}
#[test]
fn nothing_merges_when_the_switch_is_off() {
let merged = with_context(false, |context| {
merge_traversers(context, vec![vertex(1), vertex(1)], "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![1, 1]);
}
#[test]
fn full_path_mode_skips_merging() {
let merged = with_context(true, |context| {
context.path_mode = crate::traversal::optimizer::path_analysis::PathMode::Full;
merge_traversers(context, vec![vertex(1), vertex(1)], "barrier").unwrap()
});
assert_eq!(bulks(&merged), vec![1, 1]);
}
#[test]
fn overflowing_bulks_fail_with_bulk_overflow() {
let err = with_context(true, |context| {
let mut a = vertex(1);
a.bulk = u64::MAX;
merge_traversers(context, vec![a, vertex(1)], "barrier").unwrap_err()
});
assert!(matches!(*err, TraverserError::BulkOverflow { ref step } if step == "barrier"));
}
}
#[test]
fn slicing_counts_units_and_cuts_straddling_traversers() {
let stream = || vec![item(1, 3), item(2, 1), item(3, 4)];
let sliced = slice_units(stream(), 2, 4);
let shape: Vec<_> = sliced
.iter()
.map(|t| (number(t).unwrap(), t.bulk()))
.collect();
assert_eq!(shape, vec![(1, 1), (2, 1), (3, 2)]);
assert!(slice_units(stream(), 8, 5).is_empty());
assert_eq!(slice_units(stream(), 0, u64::MAX).len(), 3);
let tail = tail_units(stream(), 5);
let shape: Vec<_> = tail
.iter()
.map(|t| (number(t).unwrap(), t.bulk()))
.collect();
assert_eq!(shape, vec![(2, 1), (3, 4)]);
}
}