use std::sync::Arc;
use compact_str::CompactString;
use crate::error::{TraverserError, TraverserResult};
use crate::graph::GraphStorage;
use crate::value::path::{TraverserPath, TraverserPathStep};
use crate::value::{ElementProperty, Pop, PropertyHandleKind, TraverserValue};
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct PathRef(u32);
impl PathRef {
pub(crate) const NONE: PathRef = PathRef(u32::MAX);
#[inline]
pub(crate) fn is_none(self) -> bool {
self == Self::NONE
}
}
impl Default for PathRef {
fn default() -> Self {
Self::NONE
}
}
pub(crate) struct PathNode<S: GraphStorage> {
pub(crate) parent: PathRef,
pub(crate) value: TraverserValue<S>,
pub(crate) labels: Option<Arc<[CompactString]>>,
}
pub(crate) struct PathArena<S: GraphStorage> {
nodes: Vec<PathNode<S>>,
capacity: u32,
value_bytes: u64,
}
impl<S: GraphStorage> Default for PathArena<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: GraphStorage> PathArena<S> {
pub(crate) fn new() -> Self {
Self {
nodes: Vec::new(),
capacity: u32::MAX - 1,
value_bytes: 0,
}
}
#[cfg(test)]
pub(crate) fn with_capacity_limit(capacity: u32) -> Self {
Self {
nodes: Vec::new(),
capacity: capacity.min(u32::MAX - 1),
value_bytes: 0,
}
}
#[inline]
pub(crate) fn value_bytes(&self) -> u64 {
self.value_bytes
}
#[allow(dead_code)]
pub(crate) fn node_count(&self) -> usize {
self.nodes.len()
}
#[inline]
fn node(&self, path: PathRef) -> Option<&PathNode<S>> {
if path.is_none() {
None
} else {
self.nodes.get(path.0 as usize)
}
}
#[inline]
pub(crate) fn push(
&mut self,
parent: PathRef,
value: TraverserValue<S>,
labels: Option<Arc<[CompactString]>>,
) -> TraverserResult<PathRef> {
let index = self.nodes.len();
if index >= self.capacity as usize {
return Err(Box::new(TraverserError::PathArenaExhausted {
capacity: u64::from(self.capacity),
}));
}
self.value_bytes = self.value_bytes.saturating_add(
crate::traversal::step::profile::estimate_traverser_value_bytes(&value),
);
self.nodes.push(PathNode {
parent,
value,
labels,
});
Ok(PathRef(index as u32))
}
pub(crate) fn relabel_last(
&mut self,
path: PathRef,
value: &TraverserValue<S>,
labels: &Arc<[CompactString]>,
) -> TraverserResult<PathRef> {
let (parent, node_value, merged) = match self.node(path) {
None => return self.push(PathRef::NONE, value.clone(), Some(labels.clone())),
Some(node) => {
let merged = match &node.labels {
None => labels.clone(),
Some(existing) => {
let mut all: Vec<CompactString> = existing.iter().cloned().collect();
for label in labels.iter() {
if !all.contains(label) {
all.push(label.clone());
}
}
Arc::from(all)
}
};
(node.parent, node.value.clone(), merged)
}
};
self.push(parent, node_value, Some(merged))
}
pub(crate) fn iter_rev(&self, path: PathRef) -> PathIter<'_, S> {
PathIter {
arena: self,
current: path,
}
}
#[allow(dead_code)]
pub(crate) fn len(&self, path: PathRef) -> usize {
self.iter_rev(path).count()
}
pub(crate) fn by_label(&self, path: PathRef, label: &str) -> Option<TraverserValue<S>> {
self.iter_rev(path)
.find(|node| {
node.labels
.as_ref()
.is_some_and(|labels| labels.iter().any(|l| l == label))
})
.map(|node| node.value.clone())
}
pub(crate) fn labelled(
&self,
storage: &S,
path: PathRef,
label: &str,
pop: Pop,
) -> Option<TraverserValue<S>> {
let has = |node: &&PathNode<S>| {
node.labels
.as_ref()
.is_some_and(|labels| labels.iter().any(|l| l == label))
};
match pop {
Pop::Last => self.iter_rev(path).find(has).map(|node| node.value.clone()),
Pop::First => self
.iter_rev(path)
.filter(has)
.last()
.map(|node| node.value.clone()),
Pop::All => {
let mut found: Vec<TraverserValue<S>> = self
.iter_rev(path)
.filter(has)
.map(|node| node.value.clone())
.collect();
if found.is_empty() {
return None;
}
found.reverse();
Some(TraverserValue::Array(found))
}
Pop::Mixed => {
let mut found: Vec<TraverserValue<S>> = self
.iter_rev(path)
.filter(has)
.map(|node| node.value.clone())
.collect();
found.reverse();
match found.len() {
0 => None,
1 => found.pop(),
_ => {
if let Some(mut items) = list_items(storage, &found[0]) {
items.extend(found.drain(1..));
Some(TraverserValue::Array(items))
} else {
Some(TraverserValue::Array(found))
}
}
}
}
}
}
pub(crate) fn collect_values_into(&self, path: PathRef, buf: &mut Vec<TraverserValue<S>>) {
buf.clear();
let mut current = path;
while let Some(node) = self.node(current) {
buf.push(node.value.clone());
current = node.parent;
}
}
pub(crate) fn materialize(&self, path: PathRef) -> TraverserPath<S> {
let mut steps: Vec<TraverserPathStep<S>> = self
.iter_rev(path)
.map(|node| TraverserPathStep {
value: node.value.clone(),
label: node.labels.as_ref().map(|labels| labels.to_vec()),
})
.collect();
steps.reverse();
TraverserPath { steps }
}
pub(crate) fn is_simple(
&self,
storage: &S,
path: PathRef,
scratch: &mut Vec<(u64, PathRef)>,
) -> bool {
use std::hash::{Hash, Hasher};
scratch.clear();
let mut current = path;
while let Some(node) = self.node(current) {
let mut hasher = fnv::FnvHasher::default();
PathObject::resolve(storage, &node.value).hash(&mut hasher);
scratch.push((hasher.finish(), current));
current = node.parent;
}
if scratch.len() < 2 {
return true;
}
scratch.sort_unstable_by_key(|(hash, _)| *hash);
let mut start = 0;
while start < scratch.len() {
let mut end = start + 1;
while end < scratch.len() && scratch[end].0 == scratch[start].0 {
end += 1;
}
for i in start..end {
for j in (i + 1)..end {
let (Some(a), Some(b)) = (self.node(scratch[i].1), self.node(scratch[j].1))
else {
continue;
};
if PathObject::resolve(storage, &a.value)
== PathObject::resolve(storage, &b.value)
{
return false;
}
}
}
start = end;
}
true
}
}
fn list_items<S: GraphStorage>(
storage: &S,
value: &TraverserValue<S>,
) -> Option<Vec<TraverserValue<S>>> {
let from_property = |property: Option<&ElementProperty>| match property {
Some(ElementProperty::Array(items)) => {
Some(items.iter().map(TraverserValue::from).collect())
}
_ => None,
};
match value {
TraverserValue::Array(items) => Some(items.clone()),
TraverserValue::Computed(property, _) => from_property(Some(property)),
TraverserValue::VertexProperty(key, vertex, PropertyHandleKind::Value) => from_property(
storage
.vertex_properties(*vertex)
.and_then(|p| p.get(key.as_str())),
),
TraverserValue::EdgeProperty(key, edge, PropertyHandleKind::Value) => from_property(
storage
.edge_properties(*edge)
.and_then(|p| p.get(key.as_str())),
),
_ => None,
}
}
pub(crate) fn is_simple_values<S: GraphStorage>(
storage: &S,
values: &[&TraverserValue<S>],
) -> bool {
use std::hash::{Hash, Hasher};
if values.len() < 2 {
return true;
}
let mut hashed: Vec<(u64, usize)> = values
.iter()
.enumerate()
.map(|(index, value)| {
let mut hasher = fnv::FnvHasher::default();
PathObject::resolve(storage, value).hash(&mut hasher);
(hasher.finish(), index)
})
.collect();
hashed.sort_unstable_by_key(|(hash, _)| *hash);
let mut start = 0;
while start < hashed.len() {
let mut end = start + 1;
while end < hashed.len() && hashed[end].0 == hashed[start].0 {
end += 1;
}
for i in start..end {
for j in (i + 1)..end {
if PathObject::resolve(storage, values[hashed[i].1])
== PathObject::resolve(storage, values[hashed[j].1])
{
return false;
}
}
}
start = end;
}
true
}
pub(crate) struct PathIter<'a, S: GraphStorage> {
arena: &'a PathArena<S>,
current: PathRef,
}
impl<'a, S: GraphStorage> Iterator for PathIter<'a, S> {
type Item = &'a PathNode<S>;
fn next(&mut self) -> Option<Self::Item> {
let node = self.arena.node(self.current)?;
self.current = node.parent;
Some(node)
}
}
enum PathObject<'a, S: GraphStorage> {
Value(&'a ElementProperty),
Str(&'a str),
Raw(&'a TraverserValue<S>),
}
impl<'a, S: GraphStorage> PathObject<'a, S> {
fn resolve(storage: &'a S, value: &'a TraverserValue<S>) -> Self {
match value {
TraverserValue::Computed(property, _) => PathObject::Value(property),
TraverserValue::VertexProperty(key, vertex, _) if key != "id" => {
if key == "label" {
return match storage.vertex_label(*vertex) {
Some(label) => PathObject::Str(label),
None => PathObject::Raw(value),
};
}
match storage
.vertex_properties(*vertex)
.and_then(|p| p.get(key.as_str()))
{
Some(property) => PathObject::Value(property),
None => PathObject::Raw(value),
}
}
TraverserValue::EdgeProperty(key, edge, _) if key != "id" => {
if key == "label" {
return match storage.edge_label(*edge) {
Some(label) => PathObject::Str(label),
None => PathObject::Raw(value),
};
}
match storage
.edge_properties(*edge)
.and_then(|p| p.get(key.as_str()))
{
Some(property) => PathObject::Value(property),
None => PathObject::Raw(value),
}
}
other => PathObject::Raw(other),
}
}
}
impl<S: GraphStorage> std::hash::Hash for PathObject<'_, S> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
PathObject::Value(ElementProperty::String(s)) => s.as_str().hash(state),
PathObject::Str(s) => s.hash(state),
PathObject::Value(property) => property.hash(state),
PathObject::Raw(value) => value.hash(state),
}
}
}
impl<S: GraphStorage> PartialEq for PathObject<'_, S> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(PathObject::Value(a), PathObject::Value(b)) => a == b,
(PathObject::Str(a), PathObject::Str(b)) => a == b,
(PathObject::Value(ElementProperty::String(a)), PathObject::Str(b))
| (PathObject::Str(b), PathObject::Value(ElementProperty::String(a))) => {
a.as_str() == *b
}
(PathObject::Raw(a), PathObject::Raw(b)) => a == b,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::DiagnosticError;
use crate::graph::{TraversalGraph, VertexHandle};
fn vertex(index: u32) -> TraverserValue<TraversalGraph> {
TraverserValue::Vertex(VertexHandle::for_test(index))
}
#[test]
fn empty_arena_allocates_nothing() {
let arena: PathArena<TraversalGraph> = PathArena::new();
assert_eq!(arena.node_count(), 0);
assert_eq!(arena.len(PathRef::NONE), 0);
assert!(arena.materialize(PathRef::NONE).steps.is_empty());
}
#[test]
fn siblings_share_their_prefix() {
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let root = arena.push(PathRef::NONE, vertex(0), None).unwrap();
let a = arena.push(root, vertex(1), None).unwrap();
let b = arena.push(root, vertex(2), None).unwrap();
assert_eq!(arena.node_count(), 3);
assert_eq!(arena.len(a), 2);
assert_eq!(arena.len(b), 2);
let path = arena.materialize(b);
assert_eq!(path.steps[0].value, vertex(0));
assert_eq!(path.steps[1].value, vertex(2));
}
#[test]
fn by_label_returns_the_last_match() {
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let labels: Arc<[CompactString]> = Arc::from(vec![CompactString::new("a")]);
let first = arena
.push(PathRef::NONE, vertex(0), Some(labels.clone()))
.unwrap();
let second = arena.push(first, vertex(1), Some(labels.clone())).unwrap();
assert_eq!(arena.by_label(second, "a"), Some(vertex(1)));
assert_eq!(arena.by_label(first, "a"), Some(vertex(0)));
assert_eq!(arena.by_label(second, "b"), None);
}
#[test]
fn labelled_reads_first_last_all_and_mixed_in_path_order() {
let storage = TraversalGraph::new();
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let a: Arc<[CompactString]> = Arc::from(vec![CompactString::new("a")]);
let ab: Arc<[CompactString]> =
Arc::from(vec![CompactString::new("a"), CompactString::new("b")]);
let p0 = arena
.push(PathRef::NONE, vertex(0), Some(a.clone()))
.unwrap();
let p1 = arena.push(p0, vertex(1), None).unwrap();
let p2 = arena.push(p1, vertex(2), Some(ab)).unwrap();
let p3 = arena.push(p2, vertex(3), Some(a)).unwrap();
let all = |path, label, pop| arena.labelled(&storage, path, label, pop);
assert_eq!(all(p3, "a", Pop::First), Some(vertex(0)));
assert_eq!(all(p3, "a", Pop::Last), Some(vertex(3)));
assert_eq!(
all(p3, "a", Pop::All),
Some(TraverserValue::Array(vec![vertex(0), vertex(2), vertex(3)]))
);
assert_eq!(
all(p3, "a", Pop::Mixed),
Some(TraverserValue::Array(vec![vertex(0), vertex(2), vertex(3)]))
);
assert_eq!(all(p3, "b", Pop::Mixed), Some(vertex(2)));
assert_eq!(
all(p3, "b", Pop::All),
Some(TraverserValue::Array(vec![vertex(2)]))
);
assert_eq!(all(p3, "b", Pop::First), Some(vertex(2)));
for pop in [Pop::First, Pop::Last, Pop::All, Pop::Mixed] {
assert_eq!(all(p3, "zz", pop), None, "{pop}");
assert_eq!(all(PathRef::NONE, "a", pop), None, "{pop}");
}
assert_eq!(
all(p1, "a", Pop::All),
Some(TraverserValue::Array(vec![vertex(0)]))
);
}
#[test]
fn labelled_mixed_appends_to_a_list_valued_oldest_occurrence() {
let storage = TraversalGraph::new();
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let a: Arc<[CompactString]> = Arc::from(vec![CompactString::new("a")]);
let list = TraverserValue::Array(vec![vertex(0), vertex(1)]);
let p0 = arena
.push(PathRef::NONE, list.clone(), Some(a.clone()))
.unwrap();
let p1 = arena.push(p0, vertex(2), Some(a)).unwrap();
assert_eq!(
arena.labelled(&storage, p1, "a", Pop::Mixed),
Some(TraverserValue::Array(vec![vertex(0), vertex(1), vertex(2)]))
);
assert_eq!(
arena.labelled(&storage, p1, "a", Pop::All),
Some(TraverserValue::Array(vec![list.clone(), vertex(2)]))
);
assert_eq!(
arena.labelled(&storage, p1, "a", Pop::First),
Some(list.clone())
);
assert_eq!(arena.labelled(&storage, p0, "a", Pop::Mixed), Some(list));
}
#[test]
fn relabel_last_does_not_mutate_shared_nodes() {
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let root = arena.push(PathRef::NONE, vertex(0), None).unwrap();
let a: Arc<[CompactString]> = Arc::from(vec![CompactString::new("a")]);
let b: Arc<[CompactString]> = Arc::from(vec![CompactString::new("b")]);
let with_a = arena.relabel_last(root, &vertex(0), &a).unwrap();
let with_b = arena.relabel_last(root, &vertex(0), &b).unwrap();
assert_eq!(arena.by_label(with_a, "a"), Some(vertex(0)));
assert_eq!(arena.by_label(with_a, "b"), None);
assert_eq!(arena.by_label(with_b, "b"), Some(vertex(0)));
assert_eq!(arena.len(with_a), 1);
let both = arena.relabel_last(with_a, &vertex(0), &b).unwrap();
assert_eq!(arena.len(both), 1);
assert_eq!(arena.by_label(both, "a"), Some(vertex(0)));
assert_eq!(arena.by_label(both, "b"), Some(vertex(0)));
}
#[test]
fn exhausted_arena_returns_an_error() {
let mut arena: PathArena<TraversalGraph> = PathArena::with_capacity_limit(2);
let root = arena.push(PathRef::NONE, vertex(0), None).unwrap();
let next = arena.push(root, vertex(1), None).unwrap();
let err = arena.push(next, vertex(2), None).unwrap_err();
assert!(matches!(
*err,
TraverserError::PathArenaExhausted { capacity: 2 }
));
assert!(err.help().unwrap_or_default().contains("limit("));
}
#[test]
fn simple_path_detects_repeats() {
let graph = TraversalGraph::tinkerpop_modern();
let mut arena: PathArena<TraversalGraph> = PathArena::new();
let mut scratch = Vec::new();
let a = arena.push(PathRef::NONE, vertex(0), None).unwrap();
let b = arena.push(a, vertex(1), None).unwrap();
assert!(arena.is_simple(&graph, b, &mut scratch));
let c = arena.push(b, vertex(0), None).unwrap();
assert!(!arena.is_simple(&graph, c, &mut scratch));
let text: TraverserValue<TraversalGraph> =
TraverserValue::Computed(ElementProperty::String("x".into()), None);
let d = arena.push(b, text.clone(), None).unwrap();
let e = arena.push(d, text, None).unwrap();
assert!(arena.is_simple(&graph, d, &mut scratch));
assert!(!arena.is_simple(&graph, e, &mut scratch));
}
fn recorded(query: impl FnOnce(&TraversalGraph) -> usize) -> usize {
let graph = TraversalGraph::tinkerpop_modern();
query(&graph)
}
#[test]
fn a_query_without_path_consumer_pushes_no_node() {
let count = recorded(|g| {
g.traversal()
.v(None)
.out(None)
.out(None)
.values("name")
.dedup()
.internal_path_node_count()
.unwrap()
});
assert_eq!(count, 0);
let count = recorded(|g| {
g.traversal()
.v(None)
.as_("a")
.out(None)
.path()
.count()
.internal_path_node_count()
.unwrap()
});
assert_eq!(
count, 0,
"neither the unread label nor the skipped path() record"
);
}
#[test]
fn only_read_labels_push_nodes() {
let count = recorded(|g| {
g.traversal()
.v(None)
.as_("a")
.out(None)
.as_("b")
.select("a")
.internal_path_node_count()
.unwrap()
});
assert_eq!(count, 6);
}
#[test]
fn a_path_consumer_records_every_position() {
let count = recorded(|g| {
g.traversal()
.v("1")
.out(None)
.path()
.internal_path_node_count()
.unwrap()
});
assert_eq!(count, 4);
}
}