use super::csr_snapshot::CsrSnapshot;
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GraphEdge {
id: u64,
source: u64,
target: u64,
label: String,
properties: HashMap<String, Value>,
}
impl GraphEdge {
pub fn new(id: u64, source: u64, target: u64, label: &str) -> Result<Self> {
let trimmed = label.trim();
if trimmed.is_empty() {
return Err(Error::InvalidEdgeLabel(
"Edge label cannot be empty or whitespace-only".to_string(),
));
}
Ok(Self {
id,
source,
target,
label: trimmed.to_string(),
properties: HashMap::new(),
})
}
#[must_use]
pub fn with_properties(mut self, properties: HashMap<String, Value>) -> Self {
self.properties = properties;
self
}
#[must_use]
pub fn id(&self) -> u64 {
self.id
}
#[must_use]
pub fn source(&self) -> u64 {
self.source
}
#[must_use]
pub fn target(&self) -> u64 {
self.target
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub fn properties(&self) -> &HashMap<String, Value> {
&self.properties
}
#[must_use]
pub fn property(&self, name: &str) -> Option<&Value> {
self.properties.get(name)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct EdgeStore {
pub(super) edges: HashMap<u64, GraphEdge>,
pub(super) outgoing: HashMap<u64, Vec<u64>>,
pub(super) incoming: HashMap<u64, Vec<u64>>,
pub(super) by_label: HashMap<String, Vec<u64>>,
pub(super) outgoing_by_label: HashMap<(u64, String), Vec<u64>>,
#[serde(skip)]
pub(super) csr_snapshot: Option<CsrSnapshot>,
}
impl EdgeStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(expected_edges: usize, expected_nodes: usize) -> Self {
let expected_labels = 10usize;
let outgoing_by_label_cap = expected_nodes
.saturating_mul(expected_labels)
.saturating_div(10);
Self {
edges: HashMap::with_capacity(expected_edges),
outgoing: HashMap::with_capacity(expected_nodes),
incoming: HashMap::with_capacity(expected_nodes),
by_label: HashMap::with_capacity(expected_labels),
outgoing_by_label: HashMap::with_capacity(outgoing_by_label_cap),
csr_snapshot: None,
}
}
pub fn add_edge(&mut self, edge: GraphEdge) -> Result<()> {
self.insert_edge(edge, true, true)
}
pub fn add_edge_outgoing_only(&mut self, edge: GraphEdge) -> Result<()> {
self.insert_edge(edge, true, false)
}
pub fn add_edge_incoming_only(&mut self, edge: GraphEdge) -> Result<()> {
self.insert_edge(edge, false, true)
}
fn insert_edge(
&mut self,
edge: GraphEdge,
index_outgoing: bool,
index_incoming: bool,
) -> Result<()> {
let id = edge.id();
if self.edges.contains_key(&id) {
return Err(Error::EdgeExists(id));
}
if index_outgoing {
let source = edge.source();
let label = edge.label().to_string();
self.outgoing.entry(source).or_default().push(id);
self.by_label.entry(label.clone()).or_default().push(id);
self.outgoing_by_label
.entry((source, label))
.or_default()
.push(id);
}
if index_incoming {
self.incoming.entry(edge.target()).or_default().push(id);
}
self.edges.insert(id, edge);
self.csr_snapshot = None;
Ok(())
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn outgoing_edge_count(&self) -> usize {
self.outgoing.values().map(Vec::len).sum()
}
#[must_use]
pub fn get_edge(&self, id: u64) -> Option<&GraphEdge> {
self.edges.get(&id)
}
#[must_use]
pub fn get_outgoing(&self, node_id: u64) -> Vec<&GraphEdge> {
self.resolve_edge_ids(self.outgoing.get(&node_id))
}
#[inline]
pub fn for_each_outgoing<F: FnMut(&GraphEdge)>(&self, node_id: u64, mut f: F) {
if let Some(ids) = self.outgoing.get(&node_id) {
for id in ids {
if let Some(edge) = self.edges.get(id) {
f(edge);
}
}
}
}
#[must_use]
#[inline]
pub fn outgoing_degree(&self, node_id: u64) -> usize {
self.outgoing.get(&node_id).map_or(0, Vec::len)
}
#[must_use]
#[inline]
pub fn incoming_degree(&self, node_id: u64) -> usize {
self.incoming.get(&node_id).map_or(0, Vec::len)
}
#[must_use]
pub fn get_incoming(&self, node_id: u64) -> Vec<&GraphEdge> {
self.resolve_edge_ids(self.incoming.get(&node_id))
}
#[must_use]
pub fn get_outgoing_by_label(&self, node_id: u64, label: &str) -> Vec<&GraphEdge> {
self.resolve_edge_ids(self.outgoing_by_label.get(&(node_id, label.to_string())))
}
#[must_use]
pub fn get_edges_by_label(&self, label: &str) -> Vec<&GraphEdge> {
self.resolve_edge_ids(self.by_label.get(label))
}
#[inline]
fn resolve_edge_ids(&self, ids: Option<&Vec<u64>>) -> Vec<&GraphEdge> {
ids.map(|ids| ids.iter().filter_map(|id| self.edges.get(id)).collect())
.unwrap_or_default()
}
#[must_use]
pub fn get_incoming_by_label(&self, node_id: u64, label: &str) -> Vec<&GraphEdge> {
self.get_incoming(node_id)
.into_iter()
.filter(|e| e.label() == label)
.collect()
}
#[must_use]
pub fn contains_edge(&self, edge_id: u64) -> bool {
self.edges.contains_key(&edge_id)
}
#[must_use]
pub fn len(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
#[must_use]
pub fn all_edges(&self) -> Vec<&GraphEdge> {
self.edges.values().collect()
}
#[must_use]
pub(crate) fn outgoing_keys(&self) -> Vec<u64> {
self.outgoing.keys().copied().collect()
}
#[must_use]
pub(crate) fn total_outgoing_edges(&self) -> usize {
self.outgoing.values().map(Vec::len).sum()
}
pub(crate) fn for_each_outgoing_edge<F: FnMut(&GraphEdge)>(&self, node_id: u64, mut f: F) {
if let Some(ids) = self.outgoing.get(&node_id) {
for id in ids {
if let Some(edge) = self.edges.get(id) {
f(edge);
}
}
}
}
}