use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
#[derive(Debug, Clone, uniffi::Record, serde::Serialize, serde::Deserialize)]
pub struct MobileGraphNode {
pub id: u64,
pub label: String,
pub properties_json: Option<String>,
pub vector: Option<Vec<f32>>,
}
#[derive(Debug, Clone, uniffi::Record, serde::Serialize, serde::Deserialize)]
pub struct MobileGraphEdge {
pub id: u64,
pub source: u64,
pub target: u64,
pub label: String,
pub properties_json: Option<String>,
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct TraversalResult {
pub node_id: u64,
pub path: Vec<u64>,
pub depth: u32,
}
fn properties_to_json(
properties: &std::collections::HashMap<String, serde_json::Value>,
) -> Option<String> {
if properties.is_empty() {
return None;
}
serde_json::to_string(properties).ok()
}
impl From<velesdb_core::GraphNode> for MobileGraphNode {
fn from(node: velesdb_core::GraphNode) -> Self {
Self {
id: node.id(),
label: node.label().to_string(),
properties_json: properties_to_json(node.properties()),
vector: node.vector().cloned(),
}
}
}
impl From<velesdb_core::GraphEdge> for MobileGraphEdge {
fn from(edge: velesdb_core::GraphEdge) -> Self {
Self {
id: edge.id(),
source: edge.source(),
target: edge.target(),
label: edge.label().to_string(),
properties_json: properties_to_json(edge.properties()),
}
}
}
impl From<velesdb_core::TraversalResult> for TraversalResult {
fn from(result: velesdb_core::TraversalResult) -> Self {
Self {
node_id: result.target_id,
path: result.path,
depth: result.depth,
}
}
}
#[derive(uniffi::Object)]
pub struct MobileGraphStore {
nodes: RwLock<HashMap<u64, MobileGraphNode>>,
edges: RwLock<HashMap<u64, MobileGraphEdge>>,
outgoing: RwLock<HashMap<u64, Vec<u64>>>,
incoming: RwLock<HashMap<u64, Vec<u64>>>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct GraphSnapshot {
nodes: Vec<MobileGraphNode>,
edges: Vec<MobileGraphEdge>,
}
#[uniffi::export]
impl MobileGraphStore {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
nodes: RwLock::new(HashMap::new()),
edges: RwLock::new(HashMap::new()),
outgoing: RwLock::new(HashMap::new()),
incoming: RwLock::new(HashMap::new()),
})
}
pub fn save(&self, path: String) -> Result<(), crate::VelesError> {
let edges: Vec<MobileGraphEdge> = self.edges.read().values().cloned().collect();
let nodes: Vec<MobileGraphNode> = self.nodes.read().values().cloned().collect();
let snapshot = GraphSnapshot { nodes, edges };
let bytes = serde_json::to_vec(&snapshot)
.map_err(|e| crate::VelesError::database(format!("Graph serialize failed: {e}")))?;
std::fs::write(&path, bytes)
.map_err(|e| crate::VelesError::database(format!("Graph save to '{path}' failed: {e}")))
}
#[uniffi::constructor]
pub fn load(path: String) -> Result<Arc<Self>, crate::VelesError> {
let bytes = std::fs::read(&path).map_err(|e| {
crate::VelesError::database(format!("Graph load from '{path}' failed: {e}"))
})?;
let snapshot: GraphSnapshot = serde_json::from_slice(&bytes)
.map_err(|e| crate::VelesError::database(format!("Graph deserialize failed: {e}")))?;
let store = Self::new();
for node in snapshot.nodes {
store.add_node(node);
}
for edge in snapshot.edges {
store.add_edge(edge)?;
}
Ok(store)
}
pub fn add_node(&self, node: MobileGraphNode) {
let mut nodes = self.nodes.write();
nodes.insert(node.id, node);
}
pub fn add_edge(&self, edge: MobileGraphEdge) -> Result<(), crate::VelesError> {
let mut edges = self.edges.write();
let mut outgoing = self.outgoing.write();
let mut incoming = self.incoming.write();
if edges.contains_key(&edge.id) {
return Err(crate::VelesError::database(format!(
"Edge with ID {} already exists",
edge.id
)));
}
let source = edge.source;
let target = edge.target;
let id = edge.id;
edges.insert(id, edge);
outgoing.entry(source).or_default().push(id);
incoming.entry(target).or_default().push(id);
Ok(())
}
pub fn get_node(&self, id: u64) -> Option<MobileGraphNode> {
let nodes = self.nodes.read();
nodes.get(&id).cloned()
}
pub fn get_edge(&self, id: u64) -> Option<MobileGraphEdge> {
let edges = self.edges.read();
edges.get(&id).cloned()
}
pub fn node_count(&self) -> u64 {
let nodes = self.nodes.read();
nodes.len() as u64
}
pub fn edge_count(&self) -> u64 {
let edges = self.edges.read();
edges.len() as u64
}
pub fn get_outgoing(&self, node_id: u64) -> Vec<MobileGraphEdge> {
self.get_edges_from_index(node_id, &self.outgoing)
}
pub fn get_incoming(&self, node_id: u64) -> Vec<MobileGraphEdge> {
self.get_edges_from_index(node_id, &self.incoming)
}
pub fn get_outgoing_by_label(&self, node_id: u64, label: String) -> Vec<MobileGraphEdge> {
self.get_outgoing(node_id)
.into_iter()
.filter(|e| e.label == label)
.collect()
}
pub fn get_neighbors(&self, node_id: u64) -> Vec<u64> {
self.get_outgoing(node_id)
.into_iter()
.map(|e| e.target)
.collect()
}
pub fn bfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
self.bfs_traverse_parallel(vec![source_id], max_depth, limit)
}
pub fn bfs_traverse_parallel(
&self,
source_ids: Vec<u64>,
max_depth: u32,
limit: u32,
) -> Vec<TraversalResult> {
use std::collections::{HashSet, VecDeque};
let mut results: Vec<TraversalResult> = Vec::new();
let mut visited: HashSet<u64> = HashSet::new();
let mut queue: VecDeque<(u64, u32, Vec<u64>)> = VecDeque::new();
for &source_id in &source_ids {
if visited.insert(source_id) {
queue.push_back((source_id, 0, Vec::new()));
}
}
while let Some((node_id, depth, path)) = queue.pop_front() {
if results.len() >= limit as usize {
break;
}
if depth > 0 {
results.push(TraversalResult {
node_id,
path: path.clone(),
depth,
});
}
self.enqueue_neighbors(node_id, depth, max_depth, &path, &mut visited, &mut queue);
}
results
}
pub fn remove_node(&self, node_id: u64) {
let mut edges = self.edges.write();
let mut outgoing = self.outgoing.write();
let mut incoming = self.incoming.write();
let mut nodes = self.nodes.write();
nodes.remove(&node_id);
let outgoing_ids: Vec<u64> = outgoing.remove(&node_id).unwrap_or_default();
for edge_id in outgoing_ids {
if let Some(edge) = edges.remove(&edge_id) {
if let Some(ids) = incoming.get_mut(&edge.target) {
ids.retain(|&id| id != edge_id);
}
}
}
let incoming_ids: Vec<u64> = incoming.remove(&node_id).unwrap_or_default();
for edge_id in incoming_ids {
if let Some(edge) = edges.remove(&edge_id) {
if let Some(ids) = outgoing.get_mut(&edge.source) {
ids.retain(|&id| id != edge_id);
}
}
}
}
pub fn remove_edge(&self, edge_id: u64) {
let mut edges = self.edges.write();
let mut outgoing = self.outgoing.write();
let mut incoming = self.incoming.write();
if let Some(edge) = edges.remove(&edge_id) {
if let Some(ids) = outgoing.get_mut(&edge.source) {
ids.retain(|&id| id != edge_id);
}
if let Some(ids) = incoming.get_mut(&edge.target) {
ids.retain(|&id| id != edge_id);
}
}
}
pub fn clear(&self) {
let mut edges = self.edges.write();
let mut outgoing = self.outgoing.write();
let mut incoming = self.incoming.write();
let mut nodes = self.nodes.write();
edges.clear();
outgoing.clear();
incoming.clear();
nodes.clear();
}
pub fn dfs_traverse(&self, source_id: u64, max_depth: u32, limit: u32) -> Vec<TraversalResult> {
use std::collections::HashSet;
let mut results: Vec<TraversalResult> = Vec::new();
let mut visited: HashSet<u64> = HashSet::new();
let mut stack: Vec<(u64, u32, Vec<u64>)> = vec![(source_id, 0, Vec::new())];
while let Some((node_id, depth, path)) = stack.pop() {
if results.len() >= limit as usize {
break;
}
if visited.contains(&node_id) {
continue;
}
visited.insert(node_id);
if depth > 0 {
results.push(TraversalResult {
node_id,
path: path.clone(),
depth,
});
}
if depth < max_depth {
let neighbors: Vec<_> = self
.get_outgoing(node_id)
.into_iter()
.filter(|e| !visited.contains(&e.target))
.collect();
for edge in neighbors.into_iter().rev() {
let mut next_path = path.clone();
next_path.push(edge.id);
stack.push((edge.target, depth + 1, next_path));
}
}
}
results
}
pub fn has_node(&self, id: u64) -> bool {
let nodes = self.nodes.read();
nodes.contains_key(&id)
}
pub fn has_edge(&self, id: u64) -> bool {
let edges = self.edges.read();
edges.contains_key(&id)
}
#[allow(clippy::cast_possible_truncation)]
pub fn out_degree(&self, node_id: u64) -> u32 {
let outgoing = self.outgoing.read();
outgoing.get(&node_id).map_or(0, |v| v.len() as u32)
}
#[allow(clippy::cast_possible_truncation)]
pub fn in_degree(&self, node_id: u64) -> u32 {
let incoming = self.incoming.read();
incoming.get(&node_id).map_or(0, |v| v.len() as u32)
}
pub fn get_nodes_by_label(&self, label: String) -> Vec<MobileGraphNode> {
let nodes = self.nodes.read();
nodes
.values()
.filter(|n| n.label == label)
.cloned()
.collect()
}
pub fn get_edges_by_label(&self, label: String) -> Vec<MobileGraphEdge> {
let edges = self.edges.read();
edges
.values()
.filter(|e| e.label == label)
.cloned()
.collect()
}
}
impl MobileGraphStore {
fn get_edges_from_index(
&self,
node_id: u64,
index: &RwLock<HashMap<u64, Vec<u64>>>,
) -> Vec<MobileGraphEdge> {
let edges = self.edges.read();
let idx = index.read();
idx.get(&node_id)
.map(|ids| ids.iter().filter_map(|id| edges.get(id).cloned()).collect())
.unwrap_or_default()
}
fn enqueue_neighbors(
&self,
node_id: u64,
depth: u32,
max_depth: u32,
path: &[u64],
visited: &mut std::collections::HashSet<u64>,
queue: &mut std::collections::VecDeque<(u64, u32, Vec<u64>)>,
) {
if depth >= max_depth {
return;
}
for edge in self.get_outgoing(node_id) {
if visited.insert(edge.target) {
let mut next_path = path.to_vec();
next_path.push(edge.id);
queue.push_back((edge.target, depth + 1, next_path));
}
}
}
}
impl Default for MobileGraphStore {
fn default() -> Self {
Self {
nodes: RwLock::new(HashMap::new()),
edges: RwLock::new(HashMap::new()),
outgoing: RwLock::new(HashMap::new()),
incoming: RwLock::new(HashMap::new()),
}
}
}
#[cfg(test)]
#[path = "graph_tests.rs"]
mod tests;