use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::SystemTime;
use uni_algo::algo::GraphProjection;
use uni_store::storage::manager::StorageManager;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProjectionSourceKind {
Native,
Cypher,
}
impl ProjectionSourceKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Native => "Native",
Self::Cypher => "Cypher",
}
}
}
#[derive(Clone)]
pub struct ProjectionEntry {
pub projection: Arc<GraphProjection>,
pub node_count: usize,
pub edge_count: usize,
pub bytes: usize,
pub created_at: SystemTime,
pub source_kind: ProjectionSourceKind,
}
impl std::fmt::Debug for ProjectionEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProjectionEntry")
.field("node_count", &self.node_count)
.field("edge_count", &self.edge_count)
.field("bytes", &self.bytes)
.field("source_kind", &self.source_kind)
.finish_non_exhaustive()
}
}
#[derive(Default)]
pub struct ProjectionStore {
entries: RwLock<HashMap<String, ProjectionEntry>>,
}
impl std::fmt::Debug for ProjectionStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let len = self.entries.read().map(|g| g.len()).unwrap_or(0);
f.debug_struct("ProjectionStore")
.field("entries", &len)
.finish()
}
}
impl ProjectionStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, name: String, entry: ProjectionEntry) -> Result<(), String> {
let mut g = self
.entries
.write()
.map_err(|_| "store lock poisoned".to_owned())?;
if g.contains_key(&name) {
return Err(name);
}
g.insert(name, entry);
Ok(())
}
#[must_use]
pub fn get(&self, name: &str) -> Option<ProjectionEntry> {
self.entries.read().ok()?.get(name).cloned()
}
pub fn drop_by_name(&self, name: &str) -> bool {
self.entries
.write()
.map(|mut g| g.remove(name).is_some())
.unwrap_or(false)
}
#[must_use]
pub fn list(&self) -> Vec<(String, ProjectionEntry)> {
self.entries
.read()
.map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.entries
.read()
.map(|g| g.contains_key(name))
.unwrap_or(false)
}
}
pub fn for_storage(storage: &Arc<StorageManager>) -> Arc<ProjectionStore> {
static REGISTRY: OnceLock<Mutex<HashMap<usize, Arc<ProjectionStore>>>> = OnceLock::new();
let key = Arc::as_ptr(storage) as usize;
let reg = REGISTRY.get_or_init(|| Mutex::new(HashMap::new()));
let mut g = match reg.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
g.entry(key)
.or_insert_with(|| Arc::new(ProjectionStore::new()))
.clone()
}
#[must_use]
pub fn estimate_bytes(p: &GraphProjection) -> usize {
use std::mem::size_of;
let v = p.vertex_count();
v * 32 + size_of::<GraphProjection>()
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::SystemTime;
fn empty_entry() -> ProjectionEntry {
ProjectionEntry {
projection: Arc::new(GraphProjection::from_rows(&[], &[], None, false).unwrap()),
node_count: 0,
edge_count: 0,
bytes: 0,
created_at: SystemTime::now(),
source_kind: ProjectionSourceKind::Native,
}
}
#[test]
fn insert_get_drop_round_trip() {
let s = ProjectionStore::new();
s.insert("g".to_owned(), empty_entry()).unwrap();
assert!(s.contains("g"));
assert!(s.get("g").is_some());
assert!(s.drop_by_name("g"));
assert!(!s.contains("g"));
assert!(!s.drop_by_name("g"));
}
#[test]
fn duplicate_insert_rejected() {
let s = ProjectionStore::new();
s.insert("g".to_owned(), empty_entry()).unwrap();
let err = s.insert("g".to_owned(), empty_entry()).unwrap_err();
assert_eq!(err, "g");
}
#[test]
fn list_returns_all_entries() {
let s = ProjectionStore::new();
s.insert("a".to_owned(), empty_entry()).unwrap();
s.insert("b".to_owned(), empty_entry()).unwrap();
let l = s.list();
assert_eq!(l.len(), 2);
let names: Vec<&str> = l.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"a"));
assert!(names.contains(&"b"));
}
}