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