#[cfg(any(feature = "neo4j", feature = "nebulagraph", feature = "arangodb"))]
mod http;
#[cfg(feature = "arangodb")]
pub mod arangodb;
#[cfg(feature = "nebulagraph")]
pub mod nebulagraph;
#[cfg(feature = "neo4j")]
pub mod neo4j;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type VertexId = String;
pub type EdgeId = String;
pub type Properties = serde_json::Map<String, serde_json::Value>;
pub type Params = serde_json::Map<String, serde_json::Value>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vertex {
pub id: VertexId,
pub label: String,
pub properties: Properties,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub id: EdgeId,
pub label: String,
pub from: VertexId,
pub to: VertexId,
pub properties: Properties,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Traversal {
pub start: VertexId,
pub edge_label: Option<String>,
pub max_depth: u32,
pub direction: TraversalDirection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TraversalDirection {
Outgoing,
Incoming,
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathResult {
pub vertices: Vec<Vertex>,
pub edges: Vec<Edge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<serde_json::Value>,
}
#[derive(Error, Debug)]
pub enum GraphError {
#[error("vertex not found: {0}")]
VertexNotFound(VertexId),
#[error("connection error: {0}")]
ConnectionError(String),
#[error("query error: {0}")]
QueryError(String),
}
#[async_trait]
pub trait GraphDB: Send + Sync {
async fn add_vertex(&self, vertex: Vertex) -> Result<Vertex, GraphError>;
async fn get_vertex(&self, id: &VertexId) -> Result<Option<Vertex>, GraphError>;
async fn update_vertex(
&self,
id: &VertexId,
properties: Properties,
) -> Result<Vertex, GraphError>;
async fn delete_vertex(&self, id: &VertexId) -> Result<(), GraphError>;
async fn add_edge(&self, edge: Edge) -> Result<Edge, GraphError>;
async fn traverse(&self, traversal: Traversal) -> Result<Vec<PathResult>, GraphError>;
async fn query(&self, query: &str, params: Option<Params>) -> Result<QueryResult, GraphError>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
struct StubGraphDB {
vertices: Mutex<HashMap<VertexId, Vertex>>,
edges: Mutex<HashMap<EdgeId, Edge>>,
}
impl StubGraphDB {
fn new() -> Self {
Self { vertices: Mutex::new(HashMap::new()), edges: Mutex::new(HashMap::new()) }
}
}
#[allow(clippy::needless_lifetimes)]
#[async_trait]
impl GraphDB for StubGraphDB {
async fn add_vertex(&self, mut vertex: Vertex) -> Result<Vertex, GraphError> {
let mut map = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
if vertex.id.is_empty() {
vertex.id = format!("v{}", map.len());
}
map.insert(vertex.id.clone(), vertex.clone());
Ok(vertex)
}
async fn get_vertex(&self, id: &VertexId) -> Result<Option<Vertex>, GraphError> {
let map = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
Ok(map.get(id).cloned())
}
async fn update_vertex(
&self,
id: &VertexId,
properties: Properties,
) -> Result<Vertex, GraphError> {
let mut map = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
let vertex = map.get_mut(id).ok_or_else(|| GraphError::VertexNotFound(id.clone()))?;
for (k, v) in properties {
vertex.properties.insert(k, v);
}
Ok(vertex.clone())
}
async fn delete_vertex(&self, id: &VertexId) -> Result<(), GraphError> {
let mut vmap = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
vmap.remove(id);
let mut emap = self.edges.lock().unwrap_or_else(|e| e.into_inner());
emap.retain(|_, e| e.from != *id && e.to != *id);
Ok(())
}
async fn add_edge(&self, mut edge: Edge) -> Result<Edge, GraphError> {
let vmap = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
if !vmap.contains_key(&edge.from) {
return Err(GraphError::VertexNotFound(edge.from.clone()));
}
if !vmap.contains_key(&edge.to) {
return Err(GraphError::VertexNotFound(edge.to.clone()));
}
drop(vmap);
let mut map = self.edges.lock().unwrap_or_else(|e| e.into_inner());
if edge.id.is_empty() {
edge.id = format!("e{}", map.len());
}
map.insert(edge.id.clone(), edge.clone());
Ok(edge)
}
async fn traverse(&self, traversal: Traversal) -> Result<Vec<PathResult>, GraphError> {
let vmap = self.vertices.lock().unwrap_or_else(|e| e.into_inner());
if !vmap.contains_key(&traversal.start) {
return Err(GraphError::VertexNotFound(traversal.start.clone()));
}
let emap = self.edges.lock().unwrap_or_else(|e| e.into_inner());
let mut results = Vec::new();
let mut frontier: Vec<(VertexId, Vec<Vertex>, Vec<Edge>)> = Vec::new();
if let Some(v) = vmap.get(&traversal.start).cloned() {
frontier.push((traversal.start.clone(), vec![v], Vec::new()));
}
for _ in 0..traversal.max_depth {
let mut next = Vec::new();
for (cur, vs, es) in &frontier {
for edge in emap.values() {
let follows = match &traversal.direction {
TraversalDirection::Outgoing => edge.from == *cur,
TraversalDirection::Incoming => edge.to == *cur,
TraversalDirection::Both => edge.from == *cur || edge.to == *cur,
};
if !follows {
continue;
}
if let Some(label) = &traversal.edge_label
&& *label != edge.label
{
continue;
}
let next_id = match &traversal.direction {
TraversalDirection::Outgoing => edge.to.clone(),
TraversalDirection::Incoming => edge.from.clone(),
TraversalDirection::Both => {
if edge.from == *cur {
edge.to.clone()
} else {
edge.from.clone()
}
}
};
if let Some(nv) = vmap.get(&next_id).cloned() {
let mut nvs = vs.clone();
let mut nes = es.clone();
nvs.push(nv);
nes.push(edge.clone());
results.push(PathResult { vertices: nvs.clone(), edges: nes.clone() });
next.push((next_id, nvs, nes));
}
}
if results.len() >= 10 {
break;
}
}
if results.len() >= 10 {
break;
}
frontier = next;
if frontier.is_empty() {
break;
}
}
Ok(results)
}
async fn query(
&self,
_query: &str,
_params: Option<Params>,
) -> Result<QueryResult, GraphError> {
Ok(QueryResult { columns: Vec::new(), rows: Vec::new() })
}
}
#[tokio::test]
async fn test_add_and_get_vertex() {
let db = StubGraphDB::new();
let v = Vertex { id: "v1".into(), label: "Person".into(), properties: Properties::new() };
let created = db.add_vertex(v).await.unwrap();
assert_eq!(created.id, "v1");
let found = db.get_vertex(&"v1".into()).await.unwrap();
assert!(found.is_some());
}
#[tokio::test]
async fn test_update_vertex() {
let db = StubGraphDB::new();
let v = Vertex {
id: "v1".into(),
label: "Person".into(),
properties: {
let mut m = Properties::new();
m.insert("name".into(), serde_json::json!("Alice"));
m
},
};
db.add_vertex(v).await.unwrap();
let mut props = Properties::new();
props.insert("age".into(), serde_json::json!(30));
let updated = db.update_vertex(&"v1".into(), props).await.unwrap();
assert_eq!(updated.properties.get("name").unwrap(), &serde_json::json!("Alice"));
assert_eq!(updated.properties.get("age").unwrap(), &serde_json::json!(30));
}
#[tokio::test]
async fn test_add_edge_and_traverse() {
let db = StubGraphDB::new();
let a = Vertex { id: "a".into(), label: "Person".into(), properties: Properties::new() };
let b = Vertex { id: "b".into(), label: "Person".into(), properties: Properties::new() };
db.add_vertex(a).await.unwrap();
db.add_vertex(b).await.unwrap();
let edge = Edge {
id: String::new(),
label: "KNOWS".into(),
from: "a".into(),
to: "b".into(),
properties: Properties::new(),
};
let created = db.add_edge(edge).await.unwrap();
assert!(!created.id.is_empty());
let paths = db
.traverse(Traversal {
start: "a".into(),
edge_label: Some("KNOWS".into()),
max_depth: 1,
direction: TraversalDirection::Outgoing,
})
.await
.unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].vertices.len(), 2);
}
#[tokio::test]
async fn test_delete_vertex_cascades_edges() {
let db = StubGraphDB::new();
let a = Vertex { id: "a".into(), label: "N".into(), properties: Properties::new() };
let b = Vertex { id: "b".into(), label: "N".into(), properties: Properties::new() };
db.add_vertex(a).await.unwrap();
db.add_vertex(b).await.unwrap();
db.add_edge(Edge {
id: "e1".into(),
label: "L".into(),
from: "a".into(),
to: "b".into(),
properties: Properties::new(),
})
.await
.unwrap();
db.delete_vertex(&"a".into()).await.unwrap();
assert!(db.get_vertex(&"a".into()).await.unwrap().is_none());
let result = db
.traverse(Traversal {
start: "a".into(),
edge_label: None,
max_depth: 1,
direction: TraversalDirection::Both,
})
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_query() {
let db = StubGraphDB::new();
let res = db.query("MATCH (n) RETURN n", None).await.unwrap();
assert!(res.columns.is_empty());
assert!(res.rows.is_empty());
}
}