use super::csr_snapshot::CsrSnapshot;
use super::label_table::{LabelId, LabelTable};
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>>,
#[serde(skip)]
pub(super) by_label: HashMap<LabelId, Vec<u64>>,
#[serde(skip)]
pub(super) outgoing_by_label: HashMap<(u64, LabelId), Vec<u64>>,
#[serde(skip)]
pub(super) incoming_by_label: HashMap<(u64, LabelId), Vec<u64>>,
#[serde(skip)]
pub(super) label_table: LabelTable,
#[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),
incoming_by_label: HashMap::with_capacity(outgoing_by_label_cap),
label_table: LabelTable::with_capacity(expected_labels),
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));
}
let label_id = self.intern_label(edge.label())?;
if index_outgoing {
let source = edge.source();
self.outgoing.entry(source).or_default().push(id);
self.by_label.entry(label_id).or_default().push(id);
self.outgoing_by_label
.entry((source, label_id))
.or_default()
.push(id);
}
if index_incoming {
let target = edge.target();
self.incoming.entry(target).or_default().push(id);
self.incoming_by_label
.entry((target, label_id))
.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_bounded(&self, node_id: u64, cap: usize) -> Vec<&GraphEdge> {
self.resolve_edge_ids_bounded(self.outgoing.get(&node_id), cap)
}
#[must_use]
pub fn get_incoming_bounded(&self, node_id: u64, cap: usize) -> Vec<&GraphEdge> {
self.resolve_edge_ids_bounded(self.incoming.get(&node_id), cap)
}
#[inline]
fn resolve_edge_ids_bounded(&self, ids: Option<&Vec<u64>>, cap: usize) -> Vec<&GraphEdge> {
ids.map(|ids| {
ids.iter()
.take(cap)
.filter_map(|id| self.edges.get(id))
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn get_outgoing_by_label(&self, node_id: u64, label: &str) -> Vec<&GraphEdge> {
let Some(label_id) = self.label_table.get_id(label) else {
return Vec::new();
};
self.resolve_edge_ids(self.outgoing_by_label.get(&(node_id, label_id)))
}
#[must_use]
pub fn get_edges_by_label(&self, label: &str) -> Vec<&GraphEdge> {
let Some(label_id) = self.label_table.get_id(label) else {
return Vec::new();
};
self.resolve_edge_ids(self.by_label.get(&label_id))
}
#[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> {
let Some(label_id) = self.label_table.get_id(label) else {
return Vec::new();
};
self.resolve_edge_ids(self.incoming_by_label.get(&(node_id, label_id)))
}
fn intern_label(&mut self, label: &str) -> Result<LabelId> {
intern_or_overflow(&mut self.label_table, label)
}
pub(super) fn rebuild_label_indexes(&mut self) -> Result<()> {
let Self {
edges,
outgoing,
incoming,
by_label,
outgoing_by_label,
incoming_by_label,
label_table,
..
} = self;
by_label.clear();
outgoing_by_label.clear();
incoming_by_label.clear();
for &node in &sorted_keys(outgoing) {
for &id in &outgoing[&node] {
if let Some(edge) = edges.get(&id) {
let label_id = intern_or_overflow(label_table, edge.label())?;
by_label.entry(label_id).or_default().push(id);
outgoing_by_label
.entry((edge.source(), label_id))
.or_default()
.push(id);
}
}
}
rebuild_incoming_labels(edges, incoming, incoming_by_label, label_table)
}
#[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);
}
}
}
}
}
fn intern_or_overflow(table: &mut LabelTable, label: &str) -> Result<LabelId> {
table
.intern(label)
.map_err(|e| Error::Overflow(e.to_string()))
}
fn rebuild_incoming_labels(
edges: &HashMap<u64, GraphEdge>,
incoming: &HashMap<u64, Vec<u64>>,
incoming_by_label: &mut HashMap<(u64, LabelId), Vec<u64>>,
label_table: &mut LabelTable,
) -> Result<()> {
for &node in &sorted_keys(incoming) {
for &id in &incoming[&node] {
if let Some(edge) = edges.get(&id) {
let label_id = intern_or_overflow(label_table, edge.label())?;
incoming_by_label
.entry((edge.target(), label_id))
.or_default()
.push(id);
}
}
}
Ok(())
}
fn sorted_keys<V>(map: &HashMap<u64, V>) -> Vec<u64> {
let mut keys: Vec<u64> = map.keys().copied().collect();
keys.sort_unstable();
keys
}