agentic_core/storage/
schema.rs1use std::env;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use tracing::{debug, info};
8
9use super::pool::DbPool;
10
11type DbResult<T> = Result<T, sqlx::Error>;
12
13fn is_marked_ready() -> bool {
14 matches!(
15 env::var("AGENTIC_API_SCHEMA_READY").as_deref(),
16 Ok("1" | "true" | "t" | "yes" | "y" | "on")
17 )
18}
19
20pub struct PoolWithSchema {
26 pool: Arc<DbPool>,
27 schema_ready: AtomicBool,
28}
29
30impl PoolWithSchema {
31 #[must_use]
33 pub fn new(pool: Arc<DbPool>) -> Self {
34 Self {
35 pool,
36 schema_ready: AtomicBool::new(false),
37 }
38 }
39
40 pub fn pool(&self) -> &Arc<DbPool> {
42 &self.pool
43 }
44
45 pub async fn ensure_schema_ready(&self) -> DbResult<()> {
57 if self.schema_ready.load(Ordering::SeqCst) {
58 return Ok(());
59 }
60
61 if is_marked_ready() {
62 debug!("[schema] DDL skipped — marked ready by supervisor.");
63 self.schema_ready.store(true, Ordering::SeqCst);
64 return Ok(());
65 }
66
67 debug!("[schema] Running migrations...");
68 sqlx::migrate!("./migrations")
69 .run(self.pool.as_ref())
70 .await
71 .map_err(|e| sqlx::Error::Configuration(e.to_string().into()))?;
72 info!("[schema] DB schema ready.");
73 self.schema_ready.store(true, Ordering::SeqCst);
74 Ok(())
75 }
76}
77
78pub struct SchemaManager<'a> {
83 pool: &'a DbPool,
84}
85
86impl<'a> SchemaManager<'a> {
87 #[must_use]
89 pub fn new(pool: &'a DbPool) -> Self {
90 Self { pool }
91 }
92
93 pub async fn run_migrations(&self) -> DbResult<()> {
99 debug!("[schema] Running migrations...");
100 sqlx::migrate!("./migrations")
101 .run(self.pool)
102 .await
103 .map_err(|e| sqlx::Error::Configuration(e.to_string().into()))?;
104 info!("[schema] DB schema ready.");
105 Ok(())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_env_var_pattern() {
115 let test_values = vec![
116 ("1", true),
117 ("true", true),
118 ("t", true),
119 ("yes", true),
120 ("y", true),
121 ("on", true),
122 ("0", false),
123 ("false", false),
124 ("f", false),
125 ("no", false),
126 ("n", false),
127 ("off", false),
128 ("", false),
129 ];
130
131 for (val, expected) in test_values {
132 let matches = matches!(
133 Ok::<&str, String>(val).as_deref(),
134 Ok("1" | "true" | "t" | "yes" | "y" | "on")
135 );
136 assert_eq!(matches, expected, "Mismatch for value '{val}'");
137 }
138 }
139
140 #[tokio::test]
141 async fn test_pool_with_schema_ready() {
142 let pool = crate::storage::pool::create_pool(Some("sqlite://?mode=memory"))
143 .await
144 .expect("failed to create pool");
145
146 let pool_with_schema = PoolWithSchema::new(pool);
147
148 let result = pool_with_schema.ensure_schema_ready().await;
150 assert!(result.is_ok(), "ensure_schema_ready failed: {result:?}");
151
152 assert!(pool_with_schema.schema_ready.load(Ordering::SeqCst));
154
155 let result = pool_with_schema.ensure_schema_ready().await;
157 assert!(result.is_ok());
158 }
159
160 #[tokio::test]
161 async fn test_multiple_pools_independent() {
162 let pool1 = crate::storage::pool::create_pool(Some("sqlite://?mode=memory"))
164 .await
165 .expect("failed to create pool1");
166
167 let pool2 = crate::storage::pool::create_pool(Some("sqlite://?mode=memory"))
168 .await
169 .expect("failed to create pool2");
170
171 let pwc1 = PoolWithSchema::new(pool1);
172 let pwc2 = PoolWithSchema::new(pool2);
173
174 pwc1.ensure_schema_ready().await.expect("pool1 failed");
176 pwc2.ensure_schema_ready().await.expect("pool2 failed");
177
178 assert!(pwc1.schema_ready.load(Ordering::SeqCst));
180 assert!(pwc2.schema_ready.load(Ordering::SeqCst));
181
182 pwc1.ensure_schema_ready().await.expect("pool1 repeat failed");
184 pwc2.ensure_schema_ready().await.expect("pool2 repeat failed");
185 }
186}