use crate::graph::subgraph::copy_edges_into;
use crate::graph::{Graph, GraphStorage};
use crate::traversal::side_effect::SideEffectValue;
use crate::*;
use indexmap::IndexSet;
use std::sync::Arc;
#[derive(Clone)]
struct StepSubgraph {
label: String,
}
impl fmt::Display for StepSubgraph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"subgraph({})",
crate::value::literal::rhai_string(&self.label)
)
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn subgraph(self, label: &str) -> Self {
self.push_step(GraphStep::SideEffect(
"subgraph",
Box::new(StepSubgraph {
label: label.to_string(),
}),
))
}
}
impl __ {
pub fn subgraph<'g, S: GraphStorage + 'g>(label: &'static str) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().subgraph(label)
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn subgraph(self, label: &str) -> Self {
let step = GraphStep::SideEffect(
"subgraph",
Box::new(StepSubgraph {
label: label.to_string(),
}),
);
self.push_step(step)
}
}
impl<'g, S: GraphStorage + 'g> GraphStepSideEffect<'g, S> for StepSubgraph {
fn declares_side_effect_key(&self) -> Option<&str> {
Some(self.label.as_str())
}
fn declare_side_effect(
&self,
side_effect: &mut std::collections::HashMap<CompactString, SideEffectValue<S>>,
) {
if !side_effect.contains_key(self.label.as_str()) {
side_effect.insert(
CompactString::new(&self.label),
SideEffectValue::Subgraph {
edges: IndexSet::default(),
target: None,
},
);
}
}
#[inline]
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: &Traverser<S>,
) -> TraverserResult<()> {
match &source.value {
TraverserValue::Edge(edge) | TraverserValue::EdgeProperty(_, edge, _) => {
match context.side_effect.get_mut(self.label.as_str()) {
Some(SideEffectValue::Subgraph { edges, .. }) => {
edges.insert(*edge);
}
Some(_) => {
return Err(Box::new(TraverserError::QueryExecution {
query: "subgraph".to_string(),
reason: format!(
"the side effect '{}' already holds a value that is not a subgraph",
self.label
),
}));
}
None => {}
}
}
TraverserValue::Schema(schema) => {
context.side_effect.insert(
CompactString::new(&self.label),
SideEffectValue::Value(TraverserValue::Computed(
ElementProperty::from(schema.as_ref()),
None,
)),
);
}
other => {
return Err(Box::new(TraverserError::QueryExecution {
query: "subgraph".to_string(),
reason: format!(
"subgraph(\"{}\") collects edges, but the traversal yields a {}",
self.label,
other.type_name()
),
}));
}
}
Ok(())
}
fn finish_batch(&self, context: &mut TraverserContext<'_, S>) -> TraverserResult<()> {
let Some(SideEffectValue::Subgraph {
edges,
target: Some(target),
}) = context.side_effect.get_mut(self.label.as_str())
else {
return Ok(());
};
if edges.is_empty() {
return Ok(());
}
let Some(mut guard) = target.try_write() else {
return Err(Box::new(TraverserError::QueryExecution {
query: "subgraph".to_string(),
reason: format!(
"the target graph of with_side_effect(\"{}\", graph) is locked: it is the graph being traversed, or in use elsewhere",
self.label
),
}));
};
let storage: &S = &*context.storage;
copy_edges_into(storage, edges.drain(..), &mut *guard, true)
}
fn clone_box(&self) -> Box<dyn GraphStepSideEffect<'g, S> + 'g> {
Box::new(self.clone())
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn to_graph(mut self) -> TraverserResult<Arc<Graph>> {
self.atomically(Self::collect_graph)
}
fn collect_graph(&mut self) -> TraverserResult<Arc<Graph>> {
let (traversers, _) = self.internal_to_list_with_context()?;
let mut edges: IndexSet<S::EdgeId> = IndexSet::default();
let mut graph_value: Option<Arc<Graph>> = None;
for traverser in &traversers {
match &traverser.value {
TraverserValue::Edge(edge) => {
edges.insert(*edge);
}
TraverserValue::Graph(graph) if graph_value.is_none() => {
graph_value = Some(graph.clone())
}
other => {
return Err(Box::new(TraverserError::QueryExecution {
query: "to_graph".to_string(),
reason: format!(
"to_graph() needs edges, but the traversal yields a {}",
other.type_name()
),
}));
}
}
}
match graph_value {
Some(graph) if edges.is_empty() && traversers.len() == 1 => Ok(graph),
Some(_) => Err(Box::new(TraverserError::QueryExecution {
query: "to_graph".to_string(),
reason: "to_graph() needs either edges or exactly one graph value, not a mix"
.to_string(),
})),
None => {
let mut graph = TraversalGraph::new();
copy_edges_into(&self.storage, edges.iter().copied(), &mut graph, true)?;
Ok(Arc::new(Graph::new(graph)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v("1").out_e(None).subgraph("S").cap("S").to_list(),
rhai: [
r#"g.V("1").outE().subgraph("S").cap("S")"#,
r#"g.v("1").out_e().subgraph("S").cap("S")"#,
],
assert: |res| {
match &res[0] {
ElementProperty::Object(map) => {
assert!(matches!(map.get("edges"), Some(ElementProperty::Array(e)) if e.len() == 3));
assert!(matches!(map.get("vertices"), Some(ElementProperty::Array(v)) if v.len() == 4));
},
other => panic!("expected the graph listing, got {other:?}"),
}
},
);
}
#[test]
fn vertices_are_an_error() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
assert!(g.v(None).subgraph("S").to_graph().is_err());
assert!(
g_lock
.traversal()
.v(None)
.subgraph("S")
.cap("S")
.to_graph()
.is_err()
);
assert!(g_lock.traversal().v(None).cap("S").next().is_err());
}
}