graph_flow/
storage_postgres.rs1use async_trait::async_trait;
2use serde_json;
3use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
4
5use crate::{Session, error::{Result, GraphError}, storage::SessionStorage};
6
7pub struct PostgresSessionStorage {
16 pool: Pool<Postgres>,
17}
18
19impl PostgresSessionStorage {
20 pub async fn connect(database_url: &str) -> Result<Self> {
24 let pool = PgPoolOptions::new()
25 .max_connections(5)
26 .connect(database_url)
27 .await
28 .map_err(|e| GraphError::StorageError(format!("Failed to connect to Postgres: {e}")))?;
29
30 Self::with_pool(pool).await
31 }
32
33 pub async fn with_pool(pool: Pool<Postgres>) -> Result<Self> {
36 Self::migrate(&pool).await?;
37 Ok(Self { pool })
38 }
39
40 async fn migrate(pool: &Pool<Postgres>) -> Result<()> {
41 sqlx::query(
42 r#"
43 CREATE TABLE IF NOT EXISTS sessions (
44 id UUID PRIMARY KEY,
45 graph_id TEXT NOT NULL,
46 current_task_id TEXT NOT NULL,
47 status_message TEXT,
48 context JSONB NOT NULL,
49 created_at TIMESTAMPTZ DEFAULT NOW(),
50 updated_at TIMESTAMPTZ DEFAULT NOW()
51 );
52 "#,
53 )
54 .execute(pool)
55 .await
56 .map_err(|e| GraphError::StorageError(format!("Migration failed: {e}")))?;
57 Ok(())
58 }
59}
60
61#[async_trait]
62impl SessionStorage for PostgresSessionStorage {
63 async fn save(&self, session: Session) -> Result<()> {
64 let context_json = serde_json::to_value(&session.context)
65 .map_err(|e| GraphError::StorageError(format!("Context serialization failed: {e}")))?;
66
67 sqlx::query(
69 r#"
70 INSERT INTO sessions (id, graph_id, current_task_id, status_message, context, updated_at)
71 VALUES ($1::uuid, $2, $3, $4, $5, NOW())
72 ON CONFLICT (id) DO UPDATE
73 SET graph_id = EXCLUDED.graph_id,
74 current_task_id = EXCLUDED.current_task_id,
75 status_message = EXCLUDED.status_message,
76 context = EXCLUDED.context,
77 updated_at = NOW()
78 "#,
79 )
80 .bind(&session.id)
81 .bind(&session.graph_id)
82 .bind(&session.current_task_id)
83 .bind(&session.status_message)
84 .bind(&context_json)
85 .execute(&self.pool)
86 .await
87 .map_err(|e| GraphError::StorageError(format!("Failed to save session: {e}")))?;
88
89 Ok(())
90 }
91
92 async fn get(&self, id: &str) -> Result<Option<Session>> {
93 let row = sqlx::query_as::<_, (String, String, String, Option<String>, serde_json::Value)>(
94 r#"
95 SELECT id::text, graph_id, current_task_id, status_message, context
96 FROM sessions
97 WHERE id = $1::uuid
98 "#,
99 )
100 .bind(id)
101 .fetch_optional(&self.pool)
102 .await
103 .map_err(|e| GraphError::StorageError(format!("Failed to fetch session: {e}")))?;
104
105 if let Some((session_id, graph_id, current_task_id, status_message, context_json)) = row {
106 let context: crate::Context = serde_json::from_value(context_json)
107 .map_err(|e| GraphError::StorageError(format!("Context deserialization failed: {e}")))?;
108 Ok(Some(Session {
109 id: session_id,
110 graph_id,
111 current_task_id,
112 status_message,
113 context,
114 }))
115 } else {
116 Ok(None)
117 }
118 }
119
120 async fn delete(&self, id: &str) -> Result<()> {
121 sqlx::query(
122 r#"
123 DELETE FROM sessions WHERE id = $1::uuid
124 "#,
125 )
126 .bind(id)
127 .execute(&self.pool)
128 .await
129 .map_err(|e| GraphError::StorageError(format!("Failed to delete session: {e}")))?;
130 Ok(())
131 }
132}