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