gemini_adk_rs/session/
postgres.rs

1//! PostgreSQL session service — scalable persistent session storage.
2//!
3//! Provides session persistence using a PostgreSQL database. Suitable for
4//! multi-process and distributed deployments.
5//!
6//! Feature-gated behind `postgres-sessions`. Delegates to the real
7//! `sqlx`-backed [`DatabaseSessionService`](super::DatabaseSessionService).
8
9use async_trait::async_trait;
10
11use super::{DatabaseSessionService, Session, SessionError, SessionId, SessionService};
12use crate::events::Event;
13
14/// Configuration for the PostgreSQL session service.
15#[derive(Debug, Clone)]
16pub struct PostgresSessionConfig {
17    /// PostgreSQL connection string (e.g., `postgres://user:pass@host/db`).
18    pub connection_string: String,
19    /// Maximum number of connections in the pool.
20    pub max_connections: u32,
21}
22
23impl PostgresSessionConfig {
24    /// Create a new config with the given connection string and default pool size.
25    pub fn new(connection_string: impl Into<String>) -> Self {
26        Self {
27            connection_string: connection_string.into(),
28            max_connections: 10,
29        }
30    }
31
32    /// Set the maximum number of connections in the pool.
33    pub fn max_connections(mut self, max: u32) -> Self {
34        self.max_connections = max;
35        self
36    }
37}
38
39/// Session service backed by PostgreSQL.
40///
41/// Provides scalable, multi-process session persistence using PostgreSQL.
42/// Suitable for production deployments requiring horizontal scaling.
43///
44/// Delegates all storage operations to a [`DatabaseSessionService`] built from
45/// the configured connection string. The connection pool is opened lazily on
46/// first use; call [`initialize`](Self::initialize) to open it eagerly and run
47/// the schema migration.
48pub struct PostgresSessionService {
49    config: PostgresSessionConfig,
50    inner: DatabaseSessionService,
51}
52
53impl PostgresSessionService {
54    /// Create a new PostgreSQL session service.
55    ///
56    /// This only creates the service struct. The pool connects lazily on first
57    /// use, or eagerly via [`initialize`](Self::initialize).
58    pub fn new(config: PostgresSessionConfig) -> Self {
59        let inner = DatabaseSessionService::new(config.connection_string.clone())
60            .with_max_connections(config.max_connections);
61        Self { config, inner }
62    }
63
64    /// Open the pool and run the schema migration.
65    ///
66    /// Creates the `sessions` and `events` tables if they don't exist.
67    /// Safe to call multiple times.
68    pub async fn initialize(&self) -> Result<(), SessionError> {
69        self.inner.initialize().await
70    }
71
72    /// Returns the configured connection string.
73    pub fn connection_string(&self) -> &str {
74        &self.config.connection_string
75    }
76
77    /// Returns the configured maximum number of pool connections.
78    pub fn max_connections(&self) -> u32 {
79        self.config.max_connections
80    }
81}
82
83#[async_trait]
84impl SessionService for PostgresSessionService {
85    async fn create_session(&self, app_name: &str, user_id: &str) -> Result<Session, SessionError> {
86        self.inner.create_session(app_name, user_id).await
87    }
88
89    async fn get_session(&self, id: &SessionId) -> Result<Option<Session>, SessionError> {
90        self.inner.get_session(id).await
91    }
92
93    async fn list_sessions(
94        &self,
95        app_name: &str,
96        user_id: &str,
97    ) -> Result<Vec<Session>, SessionError> {
98        self.inner.list_sessions(app_name, user_id).await
99    }
100
101    async fn delete_session(&self, id: &SessionId) -> Result<(), SessionError> {
102        self.inner.delete_session(id).await
103    }
104
105    async fn append_event(&self, id: &SessionId, event: Event) -> Result<(), SessionError> {
106        self.inner.append_event(id, event).await
107    }
108
109    async fn get_events(&self, id: &SessionId) -> Result<Vec<Event>, SessionError> {
110        self.inner.get_events(id).await
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn config_new() {
120        let config = PostgresSessionConfig::new("postgres://localhost/test");
121        assert_eq!(config.connection_string, "postgres://localhost/test");
122        assert_eq!(config.max_connections, 10);
123    }
124
125    #[test]
126    fn config_max_connections() {
127        let config = PostgresSessionConfig::new("postgres://localhost/test").max_connections(20);
128        assert_eq!(config.max_connections, 20);
129    }
130
131    #[test]
132    fn service_accessors() {
133        let svc = PostgresSessionService::new(
134            PostgresSessionConfig::new("postgres://user:pass@host/db").max_connections(5),
135        );
136        assert_eq!(svc.connection_string(), "postgres://user:pass@host/db");
137        assert_eq!(svc.max_connections(), 5);
138    }
139}