use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use fs2::FileExt;
use kuzu::{Connection, Database, SystemConfig};
use super::schema::{CREATE_SCHEMA, MIGRATIONS};
use super::store_util::escape;
pub struct WriteLock {
_file: std::fs::File,
}
impl WriteLock {
fn acquire(lock_path: &Path) -> Result<Self> {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(lock_path)?;
file.lock_exclusive()
.map_err(|e| anyhow::anyhow!("failed to acquire write lock: {e}"))?;
Ok(Self { _file: file })
}
fn try_acquire(lock_path: &Path) -> Result<Option<Self>> {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(lock_path)?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(Self { _file: file })),
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock || e.raw_os_error() == Some(33) =>
{
Ok(None)
}
Err(e) => Err(anyhow::anyhow!("lock error: {e}")),
}
}
}
pub struct GraphStore {
db: Database,
lock_path: PathBuf,
}
impl GraphStore {
pub fn open(path: &Path) -> Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let lock_path = path.with_extension("lock");
let db = Database::new(path, SystemConfig::default())
.map_err(|e| anyhow::anyhow!("failed to open kuzu db: {e}"))?;
let store = Self { db, lock_path };
store.init_schema()?;
Ok(store)
}
pub fn write_lock(&self) -> Result<WriteLock> {
WriteLock::acquire(&self.lock_path)
}
pub fn try_write_lock(&self) -> Result<Option<WriteLock>> {
WriteLock::try_acquire(&self.lock_path)
}
fn init_schema(&self) -> Result<()> {
let conn = self.connection()?;
for ddl in CREATE_SCHEMA {
conn.query(ddl)
.map_err(|e| anyhow::anyhow!("schema error: {e}\n DDL: {ddl}"))?;
}
for migration in MIGRATIONS {
let _ = conn.query(migration);
}
Ok(())
}
pub fn connection(&self) -> Result<Connection<'_>> {
Connection::new(&self.db).map_err(|e| anyhow::anyhow!("failed to create connection: {e}"))
}
pub fn remove_file(&self, file: &str) -> Result<()> {
let _lock = self.write_lock()?;
let conn = self.connection()?;
self.remove_file_conn(&conn, file)
}
pub fn remove_file_conn(&self, conn: &Connection<'_>, file: &str) -> Result<()> {
let _ = conn.query(&format!(
"MATCH (f:File)-[:DEFINES]->(s:Symbol)-[:HAS_STATEMENT]->(st:Statement) WHERE f.id = '{}' DETACH DELETE st",
escape(file)
));
let _ = conn.query(&format!(
"MATCH (s:Symbol) WHERE s.file = '{}' DETACH DELETE s",
escape(file)
));
let _ = conn.query(&format!(
"MATCH (m:Module) WHERE m.file = '{}' DETACH DELETE m",
escape(file)
));
let _ = conn.query(&format!(
"MATCH (f:File) WHERE f.id = '{}' DETACH DELETE f",
escape(file)
));
Ok(())
}
pub fn get_file_hashes(&self) -> Result<HashMap<String, String>> {
let conn = self.connection()?;
let result = conn
.query("MATCH (m:Module) RETURN m.file, m.content_hash")
.map_err(|e| anyhow::anyhow!("get_file_hashes failed: {e}"))?;
let mut map = HashMap::new();
for row in result {
if row.len() >= 2 {
map.insert(row[0].to_string(), row[1].to_string());
}
}
Ok(map)
}
pub fn get_all_symbols(&self) -> Result<Vec<(String, String, String, String)>> {
let conn = self.connection()?;
let result = conn
.query("MATCH (s:Symbol) RETURN s.name, s.id, s.file, s.kind")
.map_err(|e| anyhow::anyhow!("get_all_symbols failed: {e}"))?;
let mut symbols = Vec::new();
for row in result {
if row.len() >= 4 {
symbols.push((
row[0].to_string(),
row[1].to_string(),
row[2].to_string(),
row[3].to_string(),
));
}
}
Ok(symbols)
}
pub fn derive_tested_by_edges(&self) -> Result<usize> {
let _lock = self.write_lock()?;
let conn = self.connection()?;
let q = super::queries::GraphQuery::new(&conn);
q.derive_tested_by_edges()
}
pub fn stats(&self) -> Result<GraphStats> {
let conn = self.connection()?;
let symbol_count = count_query(&conn, "MATCH (s:Symbol) RETURN count(s)")?;
let module_count = count_query(&conn, "MATCH (m:Module) RETURN count(m)")?;
let file_count = count_query(&conn, "MATCH (f:File) RETURN count(f)")?;
let folder_count = count_query(&conn, "MATCH (d:Folder) RETURN count(d)")?;
let calls_count = count_query(&conn, "MATCH ()-[r:CALLS]->() RETURN count(r)")?;
let inherits_count = count_query(&conn, "MATCH ()-[r:INHERITS]->() RETURN count(r)")?;
let contains_count = count_query(&conn, "MATCH ()-[r:CONTAINS]->() RETURN count(r)")?;
Ok(GraphStats {
symbols: symbol_count,
modules: module_count,
files: file_count,
folders: folder_count,
calls: calls_count,
inherits: inherits_count,
contains: contains_count,
})
}
}
#[derive(Debug)]
pub struct GraphStats {
pub symbols: u64,
pub modules: u64,
pub files: u64,
pub folders: u64,
pub calls: u64,
pub inherits: u64,
pub contains: u64,
}
impl std::fmt::Display for GraphStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Graph Statistics:")?;
writeln!(f, " Symbols: {}", self.symbols)?;
writeln!(f, " Modules: {}", self.modules)?;
writeln!(f, " Files: {}", self.files)?;
writeln!(f, " Folders: {}", self.folders)?;
writeln!(f, " Calls edges: {}", self.calls)?;
writeln!(f, " Inherits: {}", self.inherits)?;
writeln!(f, " Contains: {}", self.contains)
}
}
fn count_query(conn: &Connection, query: &str) -> Result<u64> {
let mut result = conn
.query(query)
.map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
if let Some(row) = result.next() {
if let Some(val) = row.first() {
return Ok(val.to_string().parse().unwrap_or(0));
}
}
Ok(0)
}