pipedash_core/infrastructure/storage/
postgres.rs1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use chrono::{
5 DateTime,
6 Utc,
7};
8use sqlx::postgres::PgPool;
9use sqlx::Row;
10
11use super::{
12 ObjectMetadata,
13 StorageBackend,
14};
15use crate::domain::{
16 DomainError,
17 DomainResult,
18};
19
20#[derive(Debug, Clone)]
21pub struct PostgresCacheConfig {
22 pub connection_string: String,
23}
24
25pub struct PostgresStorage {
26 pool: PgPool,
27}
28
29impl PostgresStorage {
30 pub fn new(pool: PgPool) -> Self {
31 Self { pool }
32 }
33
34 pub async fn from_connection_string(connection_string: &str) -> DomainResult<Self> {
35 use sqlx::postgres::PgPoolOptions;
36
37 let pool = PgPoolOptions::new()
38 .max_connections(20)
39 .min_connections(5)
40 .acquire_timeout(std::time::Duration::from_secs(30))
41 .connect(connection_string)
42 .await
43 .map_err(|e| {
44 DomainError::DatabaseError(format!("Failed to connect to PostgreSQL: {}", e))
45 })?;
46
47 Self::init_cache_table(&pool).await?;
48
49 Ok(Self { pool })
50 }
51
52 async fn init_cache_table(pool: &PgPool) -> DomainResult<()> {
53 sqlx::query(
54 r#"
55 CREATE TABLE IF NOT EXISTS cache_objects (
56 key TEXT PRIMARY KEY,
57 data BYTEA NOT NULL,
58 size BIGINT NOT NULL,
59 content_type TEXT,
60 etag TEXT,
61 metadata JSONB DEFAULT '{}',
62 created_at TIMESTAMPTZ DEFAULT NOW(),
63 updated_at TIMESTAMPTZ DEFAULT NOW()
64 )
65 "#,
66 )
67 .execute(pool)
68 .await
69 .map_err(|e| DomainError::DatabaseError(format!("Failed to create cache table: {}", e)))?;
70
71 sqlx::query(
72 r#"
73 CREATE INDEX IF NOT EXISTS idx_cache_objects_key ON cache_objects(key)
74 "#,
75 )
76 .execute(pool)
77 .await
78 .map_err(|e| DomainError::DatabaseError(format!("Failed to create cache index: {}", e)))?;
79
80 Ok(())
81 }
82
83 fn generate_etag(data: &[u8]) -> String {
84 use std::collections::hash_map::DefaultHasher;
85 use std::hash::{
86 Hash,
87 Hasher,
88 };
89
90 let mut hasher = DefaultHasher::new();
91 data.hash(&mut hasher);
92 format!("{:x}", hasher.finish())
93 }
94}
95
96#[async_trait]
97impl StorageBackend for PostgresStorage {
98 fn backend_type(&self) -> &str {
99 "postgres"
100 }
101
102 async fn is_available(&self) -> bool {
103 sqlx::query("SELECT 1").fetch_one(&self.pool).await.is_ok()
104 }
105
106 async fn list(&self, prefix: Option<&str>) -> DomainResult<Vec<ObjectMetadata>> {
107 let rows = if let Some(prefix) = prefix {
108 let pattern = format!("{}%", prefix);
109 sqlx::query(
110 r#"
111 SELECT key, size, content_type, etag, metadata, updated_at
112 FROM cache_objects
113 WHERE key LIKE $1
114 ORDER BY key
115 "#,
116 )
117 .bind(&pattern)
118 .fetch_all(&self.pool)
119 .await
120 } else {
121 sqlx::query(
122 r#"
123 SELECT key, size, content_type, etag, metadata, updated_at
124 FROM cache_objects
125 ORDER BY key
126 "#,
127 )
128 .fetch_all(&self.pool)
129 .await
130 }
131 .map_err(|e| DomainError::DatabaseError(format!("Failed to list cache objects: {}", e)))?;
132
133 let mut objects = Vec::new();
134 for row in rows {
135 let key: String = row.get("key");
136 let size: i64 = row.get("size");
137 let content_type: Option<String> = row.get("content_type");
138 let etag: Option<String> = row.get("etag");
139 let metadata_json: serde_json::Value = row.get("metadata");
140 let updated_at: DateTime<Utc> = row.get("updated_at");
141
142 let metadata: HashMap<String, String> =
143 serde_json::from_value(metadata_json).unwrap_or_default();
144
145 objects.push(ObjectMetadata {
146 key,
147 size: size as u64,
148 last_modified: updated_at,
149 etag,
150 content_type,
151 metadata,
152 });
153 }
154
155 Ok(objects)
156 }
157
158 async fn get(&self, key: &str) -> DomainResult<Vec<u8>> {
159 let row = sqlx::query(
160 r#"
161 SELECT data FROM cache_objects WHERE key = $1
162 "#,
163 )
164 .bind(key)
165 .fetch_optional(&self.pool)
166 .await
167 .map_err(|e| DomainError::DatabaseError(format!("Failed to get cache object: {}", e)))?;
168
169 match row {
170 Some(row) => {
171 let data: Vec<u8> = row.get("data");
172 Ok(data)
173 }
174 None => Err(DomainError::NotFound(format!(
175 "Cache object not found: {}",
176 key
177 ))),
178 }
179 }
180
181 async fn put(
182 &self, key: &str, data: &[u8], content_type: Option<&str>,
183 ) -> DomainResult<ObjectMetadata> {
184 let size = data.len() as i64;
185 let etag = Self::generate_etag(data);
186 let now = Utc::now();
187
188 sqlx::query(
189 r#"
190 INSERT INTO cache_objects (key, data, size, content_type, etag, updated_at)
191 VALUES ($1, $2, $3, $4, $5, $6)
192 ON CONFLICT (key) DO UPDATE SET
193 data = EXCLUDED.data,
194 size = EXCLUDED.size,
195 content_type = EXCLUDED.content_type,
196 etag = EXCLUDED.etag,
197 updated_at = EXCLUDED.updated_at
198 "#,
199 )
200 .bind(key)
201 .bind(data)
202 .bind(size)
203 .bind(content_type)
204 .bind(&etag)
205 .bind(now)
206 .execute(&self.pool)
207 .await
208 .map_err(|e| DomainError::DatabaseError(format!("Failed to put cache object: {}", e)))?;
209
210 Ok(ObjectMetadata {
211 key: key.to_string(),
212 size: size as u64,
213 last_modified: now,
214 etag: Some(etag),
215 content_type: content_type.map(String::from),
216 metadata: HashMap::new(),
217 })
218 }
219
220 async fn delete(&self, key: &str) -> DomainResult<()> {
221 sqlx::query(
222 r#"
223 DELETE FROM cache_objects WHERE key = $1
224 "#,
225 )
226 .bind(key)
227 .execute(&self.pool)
228 .await
229 .map_err(|e| DomainError::DatabaseError(format!("Failed to delete cache object: {}", e)))?;
230
231 Ok(())
232 }
233
234 async fn exists(&self, key: &str) -> DomainResult<bool> {
235 let row = sqlx::query(
236 r#"
237 SELECT 1 FROM cache_objects WHERE key = $1
238 "#,
239 )
240 .bind(key)
241 .fetch_optional(&self.pool)
242 .await
243 .map_err(|e| DomainError::DatabaseError(format!("Failed to check cache object: {}", e)))?;
244
245 Ok(row.is_some())
246 }
247
248 async fn head(&self, key: &str) -> DomainResult<Option<ObjectMetadata>> {
249 let row = sqlx::query(
250 r#"
251 SELECT key, size, content_type, etag, metadata, updated_at
252 FROM cache_objects
253 WHERE key = $1
254 "#,
255 )
256 .bind(key)
257 .fetch_optional(&self.pool)
258 .await
259 .map_err(|e| {
260 DomainError::DatabaseError(format!("Failed to get cache object metadata: {}", e))
261 })?;
262
263 match row {
264 Some(row) => {
265 let key: String = row.get("key");
266 let size: i64 = row.get("size");
267 let content_type: Option<String> = row.get("content_type");
268 let etag: Option<String> = row.get("etag");
269 let metadata_json: serde_json::Value = row.get("metadata");
270 let updated_at: DateTime<Utc> = row.get("updated_at");
271
272 let metadata: HashMap<String, String> =
273 serde_json::from_value(metadata_json).unwrap_or_default();
274
275 Ok(Some(ObjectMetadata {
276 key,
277 size: size as u64,
278 last_modified: updated_at,
279 etag,
280 content_type,
281 metadata,
282 }))
283 }
284 None => Ok(None),
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {}