Skip to main content

zeph_memory/store/
mod.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4mod acp_sessions;
5pub mod admission_training;
6pub mod compression_guidelines;
7pub mod compression_predictor;
8pub mod corrections;
9pub mod experiments;
10pub mod graph_store;
11mod history;
12mod mem_scenes;
13pub(crate) mod messages;
14pub mod overflow;
15pub mod persona;
16pub mod preferences;
17pub mod session_digest;
18mod skills;
19mod summaries;
20mod trust;
21
22#[allow(unused_imports)]
23use zeph_db::sql;
24use zeph_db::{DbConfig, DbPool};
25
26use crate::error::MemoryError;
27
28pub use acp_sessions::{AcpSessionEvent, AcpSessionInfo};
29pub use messages::role_str;
30pub use persona::PersonaFactRow;
31pub use skills::{SkillMetricsRow, SkillUsageRow, SkillVersionRow};
32pub use trust::{SkillTrustRow, SourceKind};
33
34/// Backward-compatible type alias. Prefer [`DbStore`] in new code.
35pub type SqliteStore = DbStore;
36
37#[derive(Debug, Clone)]
38pub struct DbStore {
39    pool: DbPool,
40}
41
42impl DbStore {
43    /// Open (or create) the database and run migrations.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the database cannot be opened or migrations fail.
48    pub async fn new(path: &str) -> Result<Self, MemoryError> {
49        Self::with_pool_size(path, 5).await
50    }
51
52    /// Open (or create) the database with a configurable connection pool size.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if the database cannot be opened or migrations fail.
57    pub async fn with_pool_size(path: &str, pool_size: u32) -> Result<Self, MemoryError> {
58        let pool = DbConfig {
59            url: path.to_string(),
60            max_connections: pool_size,
61            pool_size,
62        }
63        .connect()
64        .await?;
65
66        Ok(Self { pool })
67    }
68
69    /// Expose the underlying pool for shared access by other stores.
70    #[must_use]
71    pub fn pool(&self) -> &DbPool {
72        &self.pool
73    }
74
75    /// Run all migrations on the given pool.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if any migration fails.
80    pub async fn run_migrations(pool: &DbPool) -> Result<(), MemoryError> {
81        zeph_db::run_migrations(pool).await?;
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use tempfile::NamedTempFile;
90
91    // Matches `DbConfig` busy_timeout default (5 seconds in ms)
92    const DEFAULT_BUSY_TIMEOUT_MS: i64 = 5_000;
93
94    #[tokio::test]
95    async fn wal_journal_mode_enabled_on_file_db() {
96        let file = NamedTempFile::new().expect("tempfile");
97        let path = file.path().to_str().expect("valid path");
98
99        let store = DbStore::new(path).await.expect("DbStore::new");
100
101        let mode: String = zeph_db::query_scalar(sql!("PRAGMA journal_mode"))
102            .fetch_one(store.pool())
103            .await
104            .expect("PRAGMA query");
105
106        assert_eq!(mode, "wal", "expected WAL journal mode, got: {mode}");
107    }
108
109    #[tokio::test]
110    async fn busy_timeout_enabled_on_file_db() {
111        let file = NamedTempFile::new().expect("tempfile");
112        let path = file.path().to_str().expect("valid path");
113
114        let store = DbStore::new(path).await.expect("DbStore::new");
115
116        let timeout_ms: i64 = zeph_db::query_scalar(sql!("PRAGMA busy_timeout"))
117            .fetch_one(store.pool())
118            .await
119            .expect("PRAGMA query");
120
121        assert_eq!(
122            timeout_ms, DEFAULT_BUSY_TIMEOUT_MS,
123            "expected busy_timeout pragma to match configured timeout"
124        );
125    }
126
127    #[tokio::test]
128    async fn creates_parent_dirs() {
129        let dir = tempfile::tempdir().expect("tempdir");
130        let deep = dir.path().join("a/b/c/zeph.db");
131        let path = deep.to_str().expect("valid path");
132        let _store = DbStore::new(path).await.expect("DbStore::new");
133        assert!(deep.exists(), "database file should exist");
134    }
135
136    #[tokio::test]
137    async fn with_pool_size_accepts_custom_size() {
138        let store = DbStore::with_pool_size(":memory:", 2)
139            .await
140            .expect("with_pool_size");
141        // Verify the store is operational with the custom pool size.
142        let _cid = store
143            .create_conversation()
144            .await
145            .expect("create_conversation");
146    }
147}