Skip to main content

zeph_memory/sqlite/
mod.rs

1mod history;
2mod messages;
3mod skills;
4mod summaries;
5mod trust;
6
7use sqlx::SqlitePool;
8use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
9use std::str::FromStr;
10
11use crate::error::MemoryError;
12
13pub use messages::role_str;
14pub use skills::{SkillMetricsRow, SkillUsageRow, SkillVersionRow};
15pub use trust::SkillTrustRow;
16
17#[derive(Debug, Clone)]
18pub struct SqliteStore {
19    pool: SqlitePool,
20}
21
22impl SqliteStore {
23    /// Open (or create) the `SQLite` database and run migrations.
24    ///
25    /// Enables foreign key constraints at connection level so that
26    /// `ON DELETE CASCADE` and other FK rules are enforced.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the database cannot be opened or migrations fail.
31    pub async fn new(path: &str) -> Result<Self, MemoryError> {
32        let url = if path == ":memory:" {
33            "sqlite::memory:".to_string()
34        } else {
35            if let Some(parent) = std::path::Path::new(path).parent()
36                && !parent.as_os_str().is_empty()
37            {
38                std::fs::create_dir_all(parent)?;
39            }
40            format!("sqlite:{path}?mode=rwc")
41        };
42
43        let opts = SqliteConnectOptions::from_str(&url)?
44            .create_if_missing(true)
45            .foreign_keys(true)
46            .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
47            .synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
48
49        let pool = SqlitePoolOptions::new()
50            .max_connections(5)
51            .connect_with(opts)
52            .await?;
53
54        sqlx::migrate!("./migrations").run(&pool).await?;
55
56        Ok(Self { pool })
57    }
58
59    /// Expose the underlying pool for shared access by other stores.
60    #[must_use]
61    pub fn pool(&self) -> &SqlitePool {
62        &self.pool
63    }
64
65    /// Run all migrations on the given pool.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if any migration fails.
70    pub async fn run_migrations(pool: &SqlitePool) -> Result<(), MemoryError> {
71        sqlx::migrate!("./migrations").run(pool).await?;
72        Ok(())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use tempfile::NamedTempFile;
80
81    #[tokio::test]
82    async fn wal_journal_mode_enabled_on_file_db() {
83        let file = NamedTempFile::new().expect("tempfile");
84        let path = file.path().to_str().expect("valid path");
85
86        let store = SqliteStore::new(path).await.expect("SqliteStore::new");
87
88        let mode: String = sqlx::query_scalar("PRAGMA journal_mode")
89            .fetch_one(store.pool())
90            .await
91            .expect("PRAGMA query");
92
93        assert_eq!(mode, "wal", "expected WAL journal mode, got: {mode}");
94    }
95
96    #[tokio::test]
97    async fn creates_parent_dirs() {
98        let dir = tempfile::tempdir().expect("tempdir");
99        let deep = dir.path().join("a/b/c/zeph.db");
100        let path = deep.to_str().expect("valid path");
101        let _store = SqliteStore::new(path).await.expect("SqliteStore::new");
102        assert!(deep.exists(), "database file should exist");
103    }
104}