plexus_substrate/activations/
storage.rs1use sqlx::{sqlite::{SqliteConnectOptions, SqlitePool}, ConnectOptions};
8use std::path::PathBuf;
9
10pub fn activation_db_path(activation_name: &str, db_filename: &str) -> PathBuf {
24 let home = std::env::var("HOME")
25 .or_else(|_| std::env::var("USERPROFILE"))
26 .unwrap_or_else(|_| ".".to_string());
27
28 PathBuf::from(home)
29 .join(".plexus")
30 .join("substrate")
31 .join("activations")
32 .join(activation_name)
33 .join(db_filename)
34}
35
36pub fn extract_activation_name(module_path: &str) -> &str {
52 module_path
55 .split("::")
56 .skip_while(|&s| s != "activations")
57 .nth(1)
58 .unwrap_or("unknown")
59}
60
61#[macro_export]
73macro_rules! activation_db_path_from_module {
74 ($db_filename:expr) => {
75 $crate::activations::storage::activation_db_path(
76 $crate::activations::storage::extract_activation_name(module_path!()),
77 $db_filename
78 )
79 };
80}
81
82pub async fn init_sqlite_pool(db_path: PathBuf) -> Result<SqlitePool, String> {
96 if let Some(parent) = db_path.parent() {
98 std::fs::create_dir_all(parent)
99 .map_err(|e| format!("Failed to create database directory: {}", e))?;
100 }
101
102 let db_url = format!("sqlite://{}", db_path.display());
104 let options = db_url
105 .parse::<SqliteConnectOptions>()
106 .map_err(|e| format!("Failed to parse DB URL: {}", e))?;
107
108 let options = options
110 .disable_statement_logging()
111 .create_if_missing(true);
112
113 SqlitePool::connect_with(options)
115 .await
116 .map_err(|e| format!("Failed to connect to database: {}", e))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_activation_db_path() {
125 let path = activation_db_path("orcha", "orcha.db");
126 let path_str = path.to_string_lossy();
127
128 assert!(path_str.contains(".plexus"));
129 assert!(path_str.contains("substrate"));
130 assert!(path_str.contains("activations"));
131 assert!(path_str.contains("orcha"));
132 assert!(path_str.ends_with("orcha.db"));
133 }
134
135 #[test]
136 fn test_activation_db_path_different_names() {
137 let path1 = activation_db_path("claudecode", "sessions.db");
138 let path2 = activation_db_path("cone", "cones.db");
139
140 assert!(path1.to_string_lossy().contains("claudecode/sessions.db"));
141 assert!(path2.to_string_lossy().contains("cone/cones.db"));
142 assert_ne!(path1, path2);
143 }
144
145 #[test]
146 fn test_extract_activation_name() {
147 assert_eq!(
148 extract_activation_name("plexus_substrate::activations::orcha::storage"),
149 "orcha"
150 );
151 assert_eq!(
152 extract_activation_name("plexus_substrate::activations::claudecode_loopback::storage"),
153 "claudecode_loopback"
154 );
155 assert_eq!(
156 extract_activation_name("crate::activations::cone::storage"),
157 "cone"
158 );
159 }
160
161 #[tokio::test]
162 async fn test_init_sqlite_pool() {
163 use std::time::{SystemTime, UNIX_EPOCH};
164
165 let timestamp = SystemTime::now()
166 .duration_since(UNIX_EPOCH)
167 .unwrap()
168 .as_nanos();
169
170 let test_db = PathBuf::from(format!("/tmp/test_storage_{}.db", timestamp));
171
172 let pool = init_sqlite_pool(test_db.clone()).await;
173 assert!(pool.is_ok(), "Failed to initialize SQLite pool");
174
175 let _ = std::fs::remove_file(test_db);
177 }
178}