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