Skip to main content

agentic_core/storage/
schema.rs

1//! Database schema management and migrations.
2
3use 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
20/// Database pool with per-pool schema readiness tracking.
21///
22/// Wraps `DbPool` and adds an `AtomicBool` flag to track schema initialization
23/// per pool instance. This eliminates the issue of global state interfering
24/// when multiple pools point to different databases.
25pub struct PoolWithSchema {
26    pool: Arc<DbPool>,
27    schema_ready: AtomicBool,
28}
29
30impl PoolWithSchema {
31    /// Creates a new pool with schema tracking.
32    #[must_use]
33    pub fn new(pool: Arc<DbPool>) -> Self {
34        Self {
35            pool,
36            schema_ready: AtomicBool::new(false),
37        }
38    }
39
40    /// Returns a reference to the underlying database pool.
41    pub fn pool(&self) -> &Arc<DbPool> {
42        &self.pool
43    }
44
45    /// Ensures database schema is ready by running pending migrations.
46    ///
47    /// Checks if migrations have already been applied via one of:
48    /// 1. Per-pool flag (`schema_ready`)
49    /// 2. `AGENTIC_API_SCHEMA_READY` environment variable
50    ///
51    /// If none of the above, runs all pending migrations from the `migrations/` directory.
52    ///
53    /// # Errors
54    ///
55    /// Returns a [`sqlx::Error`] if migrations fail.
56    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
78/// Manages database schema initialization and migrations (deprecated).
79///
80/// This struct is kept for backward compatibility. New code should use
81/// [`PoolWithSchema::ensure_schema_ready`] instead.
82pub struct SchemaManager<'a> {
83    pool: &'a DbPool,
84}
85
86impl<'a> SchemaManager<'a> {
87    /// Creates a new schema manager for the given database pool (deprecated).
88    #[must_use]
89    pub fn new(pool: &'a DbPool) -> Self {
90        Self { pool }
91    }
92
93    /// Runs migrations without checking any flag.
94    ///
95    /// # Errors
96    ///
97    /// Returns a [`sqlx::Error`] if migrations fail.
98    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        // First call should run migrations
149        let result = pool_with_schema.ensure_schema_ready().await;
150        assert!(result.is_ok(), "ensure_schema_ready failed: {result:?}");
151
152        // Flag should now be set
153        assert!(pool_with_schema.schema_ready.load(Ordering::SeqCst));
154
155        // Second call should return immediately without doing work
156        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        // Create two in-memory pools
163        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        // Initialize both
175        pwc1.ensure_schema_ready().await.expect("pool1 failed");
176        pwc2.ensure_schema_ready().await.expect("pool2 failed");
177
178        // Both should be marked ready independently
179        assert!(pwc1.schema_ready.load(Ordering::SeqCst));
180        assert!(pwc2.schema_ready.load(Ordering::SeqCst));
181
182        // Subsequent calls should succeed without re-running migrations
183        pwc1.ensure_schema_ready().await.expect("pool1 repeat failed");
184        pwc2.ensure_schema_ready().await.expect("pool2 repeat failed");
185    }
186}