gemini_adk_rs/session/
sqlite.rs

1//! SQLite session service — lightweight persistent session storage.
2//!
3//! Mirrors ADK-Python's `sqlite_session_service`. Provides session
4//! persistence using a local SQLite database file.
5//!
6//! When the `database-sessions` feature is enabled this delegates to the real
7//! `sqlx`-backed [`DatabaseSessionService`](super::DatabaseSessionService).
8//! Without that feature it falls back to an in-memory stub so the default
9//! build stays dependency-free.
10
11use std::path::PathBuf;
12
13use async_trait::async_trait;
14
15use super::{Session, SessionError, SessionId, SessionService};
16use crate::events::Event;
17
18/// Configuration for the SQLite session service.
19#[derive(Debug, Clone)]
20pub struct SqliteSessionConfig {
21    /// Path to the SQLite database file.
22    pub db_path: PathBuf,
23}
24
25impl SqliteSessionConfig {
26    /// Create a config for an in-memory SQLite database.
27    pub fn in_memory() -> Self {
28        Self {
29            db_path: PathBuf::from(":memory:"),
30        }
31    }
32}
33
34/// Backend used by [`SqliteSessionService`].
35///
36/// With `database-sessions` this is the real `sqlx`-backed service; otherwise
37/// it is an in-memory stub.
38#[cfg(feature = "database-sessions")]
39type Backend = super::DatabaseSessionService;
40#[cfg(not(feature = "database-sessions"))]
41type Backend = super::InMemorySessionService;
42
43/// Session service backed by SQLite.
44///
45/// Provides lightweight, file-based session persistence suitable for
46/// single-process deployments and development environments.
47///
48/// The database schema is automatically created on first use.
49pub struct SqliteSessionService {
50    config: SqliteSessionConfig,
51    inner: Backend,
52}
53
54impl SqliteSessionService {
55    /// Create a new SQLite session service.
56    ///
57    /// With the `database-sessions` feature this builds a real `sqlx` pool
58    /// (opened lazily on first use). Without it, an in-memory stub is used.
59    pub fn new(config: SqliteSessionConfig) -> Self {
60        let inner = Self::build_backend(&config);
61        Self { config, inner }
62    }
63
64    #[cfg(feature = "database-sessions")]
65    fn build_backend(config: &SqliteSessionConfig) -> Backend {
66        let path = config.db_path.to_string_lossy();
67        // `:memory:` maps to sqlx's in-memory URL; file paths map to
68        // `sqlite://<path>`.
69        let url = if path == ":memory:" {
70            "sqlite::memory:".to_string()
71        } else {
72            format!("sqlite://{path}")
73        };
74        super::DatabaseSessionService::new(url)
75    }
76
77    #[cfg(not(feature = "database-sessions"))]
78    fn build_backend(config: &SqliteSessionConfig) -> Backend {
79        // Without the feature there is no SQLite backend: fall back to in-memory,
80        // but LOUDLY — silently dropping a configured DB path would lose every
81        // session on restart with no signal.
82        static WARN_ONCE: std::sync::Once = std::sync::Once::new();
83        WARN_ONCE.call_once(|| {
84            let msg = format!(
85                "SqliteSessionService: the `database-sessions` feature is not                  enabled — ignoring db_path '{}' and using in-memory storage                  (sessions are lost on restart)",
86                config.db_path.to_string_lossy()
87            );
88            tracing::warn!(target: "gemini_adk_rs::session", "{msg}");
89        });
90        super::InMemorySessionService::new()
91    }
92
93    /// Returns the configured database path.
94    pub fn db_path(&self) -> &std::path::Path {
95        &self.config.db_path
96    }
97}
98
99#[async_trait]
100impl SessionService for SqliteSessionService {
101    async fn create_session(&self, app_name: &str, user_id: &str) -> Result<Session, SessionError> {
102        self.inner.create_session(app_name, user_id).await
103    }
104
105    async fn get_session(&self, id: &SessionId) -> Result<Option<Session>, SessionError> {
106        self.inner.get_session(id).await
107    }
108
109    async fn list_sessions(
110        &self,
111        app_name: &str,
112        user_id: &str,
113    ) -> Result<Vec<Session>, SessionError> {
114        self.inner.list_sessions(app_name, user_id).await
115    }
116
117    async fn delete_session(&self, id: &SessionId) -> Result<(), SessionError> {
118        self.inner.delete_session(id).await
119    }
120
121    async fn append_event(&self, id: &SessionId, event: Event) -> Result<(), SessionError> {
122        self.inner.append_event(id, event).await
123    }
124
125    async fn get_events(&self, id: &SessionId) -> Result<Vec<Event>, SessionError> {
126        self.inner.get_events(id).await
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[tokio::test]
135    async fn create_and_get() {
136        let svc = SqliteSessionService::new(SqliteSessionConfig::in_memory());
137        let session = svc.create_session("app", "user").await.unwrap();
138        let fetched = svc.get_session(&session.id).await.unwrap();
139        assert!(fetched.is_some());
140    }
141
142    #[test]
143    fn db_path() {
144        let svc = SqliteSessionService::new(SqliteSessionConfig {
145            db_path: PathBuf::from("/tmp/test.db"),
146        });
147        assert_eq!(svc.db_path(), std::path::Path::new("/tmp/test.db"));
148    }
149
150    #[test]
151    fn in_memory_config() {
152        let config = SqliteSessionConfig::in_memory();
153        assert_eq!(config.db_path, PathBuf::from(":memory:"));
154    }
155}