use crate::{Connection, CypherResult, Error, Graph, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub struct GraphManager {
base_path: PathBuf,
open_graphs: HashMap<String, Graph>,
coordinator: Option<Connection>,
}
impl GraphManager {
pub fn open<P: AsRef<Path>>(base_path: P) -> Result<Self> {
let base_path = base_path.as_ref().to_path_buf();
fs::create_dir_all(&base_path)?;
Ok(GraphManager {
base_path,
open_graphs: HashMap::new(),
coordinator: None,
})
}
fn graph_path(&self, name: &str) -> PathBuf {
self.base_path.join(format!("{}.db", name))
}
fn ensure_coordinator(&mut self) -> Result<&Connection> {
if self.coordinator.is_none() {
let conn = Connection::open_in_memory()?;
self.coordinator = Some(conn);
}
Ok(self.coordinator.as_ref().unwrap())
}
pub fn list(&self) -> Result<Vec<String>> {
let mut graphs = Vec::new();
for entry in fs::read_dir(&self.base_path)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "db") {
if let Some(stem) = path.file_stem() {
if let Some(name) = stem.to_str() {
graphs.push(name.to_string());
}
}
}
}
graphs.sort();
Ok(graphs)
}
pub fn exists(&self, name: &str) -> bool {
self.graph_path(name).exists()
}
pub fn create(&mut self, name: &str) -> Result<&Graph> {
let path = self.graph_path(name);
if path.exists() {
return Err(Error::GraphExists(name.to_string()));
}
let graph = Graph::open(&path)?;
self.open_graphs.insert(name.to_string(), graph);
Ok(self.open_graphs.get(name).unwrap())
}
pub fn open_graph(&mut self, name: &str) -> Result<&Graph> {
if self.open_graphs.contains_key(name) {
return Ok(self.open_graphs.get(name).unwrap());
}
let path = self.graph_path(name);
if !path.exists() {
let available = self.list()?;
return Err(Error::GraphNotFound {
name: name.to_string(),
available,
});
}
let graph = Graph::open(&path)?;
self.open_graphs.insert(name.to_string(), graph);
Ok(self.open_graphs.get(name).unwrap())
}
pub fn open_or_create(&mut self, name: &str) -> Result<&Graph> {
if self.exists(name) {
self.open_graph(name)
} else {
self.create(name)
}
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Graph> {
self.open_graphs.get_mut(name)
}
pub fn drop(&mut self, name: &str) -> Result<()> {
let path = self.graph_path(name);
if !path.exists() {
let available = self.list()?;
return Err(Error::GraphNotFound {
name: name.to_string(),
available,
});
}
self.open_graphs.remove(name);
if let Some(ref conn) = self.coordinator {
let _ = conn.sqlite_connection().execute(&format!("DETACH DATABASE {}", name), []);
}
fs::remove_file(path)?;
Ok(())
}
pub fn query(&mut self, cypher: &str, graph_names: &[&str]) -> Result<CypherResult> {
let mut graph_paths: Vec<(String, PathBuf)> = Vec::new();
for name in graph_names {
let path = self.graph_path(name);
if !path.exists() {
let available = self.list()?;
return Err(Error::GraphNotFound {
name: name.to_string(),
available,
});
}
graph_paths.push((name.to_string(), path));
}
self.ensure_coordinator()?;
let coord = self.coordinator.as_ref().unwrap().sqlite_connection();
for (name, path) in &graph_paths {
let attach_sql = format!(
"ATTACH DATABASE '{}' AS {}",
path.display(),
name
);
if let Err(e) = coord.execute(&attach_sql, []) {
let err_str = e.to_string().to_lowercase();
if !err_str.contains("already in use") {
return Err(e.into());
}
}
}
let result: Option<String> = coord.query_row(
"SELECT cypher(?1)",
[cypher],
|row| row.get(0),
)?;
match result {
Some(json_str) => {
if json_str.starts_with("Error") || json_str.starts_with("{\"error\"") {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&json_str) {
if let Some(msg) = v.get("error").and_then(|e| e.as_str()) {
return Err(Error::Cypher(msg.to_string()));
}
}
return Err(Error::Cypher(json_str));
}
CypherResult::from_json(&json_str)
}
None => Ok(CypherResult::empty()),
}
}
pub fn query_sql(&mut self, sql: &str, graph_names: &[&str]) -> Result<Vec<Vec<rusqlite::types::Value>>> {
let mut graph_paths: Vec<(String, PathBuf)> = Vec::new();
for name in graph_names {
let path = self.graph_path(name);
if !path.exists() {
let available = self.list()?;
return Err(Error::GraphNotFound {
name: name.to_string(),
available,
});
}
graph_paths.push((name.to_string(), path));
}
self.ensure_coordinator()?;
let coord = self.coordinator.as_ref().unwrap().sqlite_connection();
for (name, path) in &graph_paths {
let attach_sql = format!(
"ATTACH DATABASE '{}' AS {}",
path.display(),
name
);
if let Err(e) = coord.execute(&attach_sql, []) {
let err_str = e.to_string().to_lowercase();
if !err_str.contains("already in use") {
return Err(e.into());
}
}
}
let mut stmt = coord.prepare(sql)?;
let column_count = stmt.column_count();
let rows = stmt.query_map([], |row| {
let mut values = Vec::with_capacity(column_count);
for i in 0..column_count {
values.push(row.get(i)?);
}
Ok(values)
})?;
let mut results = Vec::new();
for row in rows {
results.push(row?);
}
Ok(results)
}
pub fn len(&self) -> Result<usize> {
Ok(self.list()?.len())
}
pub fn is_empty(&self) -> Result<bool> {
Ok(self.len()? == 0)
}
pub fn contains(&self, name: &str) -> bool {
self.exists(name)
}
pub fn iter(&self) -> Result<impl Iterator<Item = String>> {
Ok(self.list()?.into_iter())
}
}
impl Drop for GraphManager {
fn drop(&mut self) {
self.open_graphs.clear();
}
}
pub fn graphs<P: AsRef<Path>>(base_path: P) -> Result<GraphManager> {
GraphManager::open(base_path)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_create_manager() {
let tmpdir = TempDir::new().unwrap();
let gm = GraphManager::open(tmpdir.path()).unwrap();
assert!(gm.is_empty().unwrap());
}
#[test]
fn test_list_empty() {
let tmpdir = TempDir::new().unwrap();
let gm = GraphManager::open(tmpdir.path()).unwrap();
assert_eq!(gm.list().unwrap(), Vec::<String>::new());
}
#[test]
fn test_create_graph() {
let tmpdir = TempDir::new().unwrap();
let mut gm = GraphManager::open(tmpdir.path()).unwrap();
gm.create("social").unwrap();
assert!(gm.exists("social"));
assert!(gm.list().unwrap().contains(&"social".to_string()));
}
#[test]
fn test_create_duplicate_fails() {
let tmpdir = TempDir::new().unwrap();
let mut gm = GraphManager::open(tmpdir.path()).unwrap();
gm.create("social").unwrap();
assert!(gm.create("social").is_err());
}
#[test]
fn test_open_missing_fails() {
let tmpdir = TempDir::new().unwrap();
let mut gm = GraphManager::open(tmpdir.path()).unwrap();
assert!(gm.open_graph("nonexistent").is_err());
}
#[test]
fn test_drop_graph() {
let tmpdir = TempDir::new().unwrap();
let mut gm = GraphManager::open(tmpdir.path()).unwrap();
gm.create("social").unwrap();
assert!(gm.exists("social"));
gm.drop("social").unwrap();
assert!(!gm.exists("social"));
}
#[test]
fn test_list_multiple() {
let tmpdir = TempDir::new().unwrap();
let mut gm = GraphManager::open(tmpdir.path()).unwrap();
gm.create("alpha").unwrap();
gm.create("beta").unwrap();
gm.create("gamma").unwrap();
let list = gm.list().unwrap();
assert_eq!(list, vec!["alpha", "beta", "gamma"]);
}
}