use crate::sdbql::ast::EdgeDirection;
use crate::storage::{Collection, Document};
use serde_json::Value;
use std::collections::{HashMap, HashSet, VecDeque};
pub(crate) struct Reached {
pub id: String,
pub depth: usize,
}
pub(crate) struct EdgeExpander<'e> {
edge: &'e Collection,
direction: EdgeDirection,
adjacency: Option<HashMap<String, Vec<Document>>>,
}
impl<'e> EdgeExpander<'e> {
pub(crate) fn new(edge: &'e Collection, direction: EdgeDirection, auto_index: bool) -> Self {
let probe = Value::String(String::new());
let want_from = matches!(direction, EdgeDirection::Outbound | EdgeDirection::Any);
let want_to = matches!(direction, EdgeDirection::Inbound | EdgeDirection::Any);
let mut has_from_index = edge.index_lookup_eq("_from", &probe).is_some();
let mut has_to_index = edge.index_lookup_eq("_to", &probe).is_some();
if auto_index && edge.get_type() == "edge" {
if want_from && !has_from_index {
let _ = edge.create_index(
"_edge_from_idx".to_string(),
vec!["_from".to_string()],
crate::storage::IndexType::Persistent,
false,
);
has_from_index = edge.index_lookup_eq("_from", &probe).is_some();
}
if want_to && !has_to_index {
let _ = edge.create_index(
"_edge_to_idx".to_string(),
vec!["_to".to_string()],
crate::storage::IndexType::Persistent,
false,
);
has_to_index = edge.index_lookup_eq("_to", &probe).is_some();
}
}
let needs_adjacency = match direction {
EdgeDirection::Outbound => !has_from_index,
EdgeDirection::Inbound => !has_to_index,
EdgeDirection::Any => !(has_from_index && has_to_index),
};
let adjacency = if needs_adjacency {
let mut map: HashMap<String, Vec<Document>> = HashMap::new();
for doc in edge.scan(None) {
let from = match doc.get("_from") {
Some(Value::String(s)) => Some(s.clone()),
_ => None,
};
let to = match doc.get("_to") {
Some(Value::String(s)) => Some(s.clone()),
_ => None,
};
if want_from {
if let Some(ref f) = from {
map.entry(f.clone()).or_default().push(doc.clone());
}
}
if want_to {
if let Some(ref t) = to {
if !(want_from && from.as_deref() == Some(t.as_str())) {
map.entry(t.clone()).or_default().push(doc.clone());
}
}
}
}
Some(map)
} else {
None
};
Self {
edge,
direction,
adjacency,
}
}
fn adjacency_edges(&self, key: &str) -> Vec<Document> {
self.adjacency
.as_ref()
.and_then(|m| m.get(key).cloned())
.unwrap_or_default()
}
pub(crate) fn edges_for(&self, current_id: &str) -> Vec<Document> {
let current_value = Value::String(current_id.to_string());
match self.direction {
EdgeDirection::Outbound => self
.edge
.index_lookup_eq("_from", ¤t_value)
.unwrap_or_else(|| self.adjacency_edges(current_id)),
EdgeDirection::Inbound => self
.edge
.index_lookup_eq("_to", ¤t_value)
.unwrap_or_else(|| self.adjacency_edges(current_id)),
EdgeDirection::Any => match (
self.edge.index_lookup_eq("_from", ¤t_value),
self.edge.index_lookup_eq("_to", ¤t_value),
) {
(Some(from_edges), Some(to_edges)) => {
let mut seen: HashSet<String> = HashSet::new();
from_edges
.into_iter()
.chain(to_edges)
.filter(|e| seen.insert(e.key.clone()))
.collect()
}
_ => self.adjacency_edges(current_id),
},
}
}
fn resolve_next(
&self,
from: Option<&str>,
to: Option<&str>,
current_id: &str,
) -> Option<String> {
match self.direction {
EdgeDirection::Outbound => to.map(|s| s.to_string()),
EdgeDirection::Inbound => from.map(|s| s.to_string()),
EdgeDirection::Any => {
if from == Some(current_id) {
to.map(|s| s.to_string())
} else if to == Some(current_id) {
from.map(|s| s.to_string())
} else {
None
}
}
}
}
pub(crate) fn next_id(&self, edge_val: &Value, current_id: &str) -> Option<String> {
self.resolve_next(
edge_val.get("_from").and_then(|v| v.as_str()),
edge_val.get("_to").and_then(|v| v.as_str()),
current_id,
)
}
fn next_id_from_doc(&self, edge_doc: &Document, current_id: &str) -> Option<String> {
let from = edge_doc.get("_from");
let to = edge_doc.get("_to");
self.resolve_next(
from.as_ref().and_then(|v| v.as_str()),
to.as_ref().and_then(|v| v.as_str()),
current_id,
)
}
pub(crate) fn bfs_from(
&self,
seed: &str,
max_hops: usize,
max_frontier: usize,
) -> Vec<Reached> {
let mut visited: HashSet<String> = HashSet::new();
let mut queue: VecDeque<(String, usize)> = VecDeque::new();
let mut out: Vec<Reached> = Vec::new();
visited.insert(seed.to_string());
queue.push_back((seed.to_string(), 0));
while let Some((current_id, depth)) = queue.pop_front() {
if depth > 0 {
out.push(Reached {
id: current_id.clone(),
depth,
});
}
if depth >= max_hops {
continue;
}
for edge_doc in self.edges_for(¤t_id) {
if let Some(next) = self.next_id_from_doc(&edge_doc, ¤t_id) {
if visited.len() >= max_frontier {
break;
}
if visited.insert(next.clone()) {
queue.push_back((next, depth + 1));
}
}
}
}
out
}
}