mod file;
mod memory;
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
mod postgres;
pub use file::{
FileStorage, StorageFormat, StorageMetadata, StorageStats, WalEntry, WalOperation,
};
pub use memory::{InMemoryStorage, IndexedInMemoryStorage, StorageEvent, StorageEventType};
#[cfg(feature = "postgres")]
pub use postgres::{
AsyncGraphStorageAdapter, EdgeRow, EventLogEntry, LineageRecordRow, NodeStateRow,
PolicyBundleRow, PostgresConfig, PostgresStats, PostgresStorage, WitnessRecordRow,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub postgres_url: Option<String>,
pub graph_path: String,
pub event_log_path: String,
pub enable_wal: bool,
pub cache_size_mb: usize,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
postgres_url: None,
graph_path: "./data/graph".to_string(),
event_log_path: "./data/events".to_string(),
enable_wal: true,
cache_size_mb: 256,
}
}
}
impl StorageConfig {
#[must_use]
pub fn in_memory() -> Self {
Self {
postgres_url: None,
graph_path: String::new(),
event_log_path: String::new(),
enable_wal: false,
cache_size_mb: 256,
}
}
#[must_use]
pub fn file_based(path: impl Into<String>) -> Self {
let path = path.into();
Self {
postgres_url: None,
graph_path: path.clone(),
event_log_path: format!("{}/events", path),
enable_wal: true,
cache_size_mb: 256,
}
}
#[must_use]
pub fn postgres(url: impl Into<String>) -> Self {
Self {
postgres_url: Some(url.into()),
graph_path: "./data/graph".to_string(),
event_log_path: "./data/events".to_string(),
enable_wal: false,
cache_size_mb: 256,
}
}
#[must_use]
pub const fn with_cache_size(mut self, size_mb: usize) -> Self {
self.cache_size_mb = size_mb;
self
}
#[must_use]
pub const fn with_wal(mut self, enable: bool) -> Self {
self.enable_wal = enable;
self
}
}
pub trait GraphStorage: Send + Sync {
fn store_node(&self, node_id: &str, state: &[f32]) -> Result<(), StorageError>;
fn get_node(&self, node_id: &str) -> Result<Option<Vec<f32>>, StorageError>;
fn store_edge(&self, source: &str, target: &str, weight: f32) -> Result<(), StorageError>;
fn delete_edge(&self, source: &str, target: &str) -> Result<(), StorageError>;
fn find_similar(&self, query: &[f32], k: usize) -> Result<Vec<(String, f32)>, StorageError>;
}
pub trait GovernanceStorage: Send + Sync {
fn store_policy(&self, bundle: &[u8]) -> Result<String, StorageError>;
fn get_policy(&self, id: &str) -> Result<Option<Vec<u8>>, StorageError>;
fn store_witness(&self, witness: &[u8]) -> Result<String, StorageError>;
fn get_witnesses_for_action(&self, action_id: &str) -> Result<Vec<Vec<u8>>, StorageError>;
fn store_lineage(&self, lineage: &[u8]) -> Result<String, StorageError>;
}
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("Connection error: {0}")]
Connection(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Transaction failed: {0}")]
Transaction(String),
#[error("Integrity violation: {0}")]
IntegrityViolation(String),
#[error("Resource exhausted: {0}")]
ResourceExhausted(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
}
#[derive(Debug)]
pub struct HybridStorage {
file_storage: FileStorage,
config: StorageConfig,
}
impl HybridStorage {
pub fn new(config: StorageConfig) -> Result<Self, StorageError> {
let file_storage = FileStorage::from_config(&config)?;
Ok(Self {
file_storage,
config,
})
}
#[must_use]
pub fn file_storage(&self) -> &FileStorage {
&self.file_storage
}
#[must_use]
pub fn config(&self) -> &StorageConfig {
&self.config
}
#[must_use]
pub fn has_postgres(&self) -> bool {
self.config.postgres_url.is_some()
}
pub fn sync(&self) -> Result<(), StorageError> {
self.file_storage.sync()
}
}
impl GraphStorage for HybridStorage {
fn store_node(&self, node_id: &str, state: &[f32]) -> Result<(), StorageError> {
self.file_storage.store_node(node_id, state)
}
fn get_node(&self, node_id: &str) -> Result<Option<Vec<f32>>, StorageError> {
self.file_storage.get_node(node_id)
}
fn store_edge(&self, source: &str, target: &str, weight: f32) -> Result<(), StorageError> {
self.file_storage.store_edge(source, target, weight)
}
fn delete_edge(&self, source: &str, target: &str) -> Result<(), StorageError> {
self.file_storage.delete_edge(source, target)
}
fn find_similar(&self, query: &[f32], k: usize) -> Result<Vec<(String, f32)>, StorageError> {
self.file_storage.find_similar(query, k)
}
}
impl GovernanceStorage for HybridStorage {
fn store_policy(&self, bundle: &[u8]) -> Result<String, StorageError> {
self.file_storage.store_policy(bundle)
}
fn get_policy(&self, id: &str) -> Result<Option<Vec<u8>>, StorageError> {
self.file_storage.get_policy(id)
}
fn store_witness(&self, witness: &[u8]) -> Result<String, StorageError> {
self.file_storage.store_witness(witness)
}
fn get_witnesses_for_action(&self, action_id: &str) -> Result<Vec<Vec<u8>>, StorageError> {
self.file_storage.get_witnesses_for_action(action_id)
}
fn store_lineage(&self, lineage: &[u8]) -> Result<String, StorageError> {
self.file_storage.store_lineage(lineage)
}
}
pub struct StorageFactory;
impl StorageFactory {
pub fn create_graph_storage(config: &StorageConfig) -> Result<Box<dyn GraphStorage>, StorageError> {
if config.graph_path.is_empty() {
Ok(Box::new(InMemoryStorage::new()))
} else {
Ok(Box::new(FileStorage::from_config(config)?))
}
}
pub fn create_governance_storage(config: &StorageConfig) -> Result<Box<dyn GovernanceStorage>, StorageError> {
if config.graph_path.is_empty() {
Ok(Box::new(InMemoryStorage::new()))
} else {
Ok(Box::new(FileStorage::from_config(config)?))
}
}
#[must_use]
pub fn in_memory() -> InMemoryStorage {
InMemoryStorage::new()
}
pub fn file(path: impl AsRef<std::path::Path>) -> Result<FileStorage, StorageError> {
FileStorage::new(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_storage_config_builders() {
let config = StorageConfig::in_memory();
assert!(config.graph_path.is_empty());
assert!(!config.enable_wal);
let config = StorageConfig::file_based("/tmp/test");
assert_eq!(config.graph_path, "/tmp/test");
assert!(config.enable_wal);
let config = StorageConfig::postgres("postgresql://localhost/db");
assert!(config.postgres_url.is_some());
}
#[test]
fn test_storage_factory_in_memory() {
let config = StorageConfig::in_memory();
let storage = StorageFactory::create_graph_storage(&config).unwrap();
storage.store_node("test", &[1.0, 2.0]).unwrap();
let state = storage.get_node("test").unwrap();
assert!(state.is_some());
}
#[test]
fn test_storage_factory_file() {
let temp_dir = TempDir::new().unwrap();
let config = StorageConfig::file_based(temp_dir.path().to_str().unwrap());
let storage = StorageFactory::create_graph_storage(&config).unwrap();
storage.store_node("test", &[1.0, 2.0]).unwrap();
let state = storage.get_node("test").unwrap();
assert!(state.is_some());
}
#[test]
fn test_hybrid_storage() {
let temp_dir = TempDir::new().unwrap();
let config = StorageConfig::file_based(temp_dir.path().to_str().unwrap());
let storage = HybridStorage::new(config).unwrap();
storage.store_node("node-1", &[1.0, 0.0, 0.0]).unwrap();
let state = storage.get_node("node-1").unwrap();
assert!(state.is_some());
let policy_id = storage.store_policy(b"test policy").unwrap();
let policy = storage.get_policy(&policy_id).unwrap();
assert!(policy.is_some());
storage.sync().unwrap();
}
#[test]
fn test_trait_object_usage() {
let memory: Box<dyn GraphStorage> = Box::new(InMemoryStorage::new());
memory.store_node("test", &[1.0]).unwrap();
let memory: Box<dyn GovernanceStorage> = Box::new(InMemoryStorage::new());
let _ = memory.store_policy(b"test").unwrap();
}
}