Skip to main content

systemprompt_database/services/postgres/
mod.rs

1//! `PostgreSQL` implementation of [`crate::services::DatabaseProvider`].
2//!
3//! This module is part of the documented sqlx allowlist: every `sqlx::query(_)`
4//! call here either binds a [`crate::models::QuerySelector`] string supplied
5//! at runtime (extension-defined SQL, dynamic admin queries) or executes
6//! `SELECT 1` for connection probing. Static SQL goes through the verified
7//! macros elsewhere.
8
9pub mod connection;
10pub mod conversion;
11mod ext;
12mod introspection;
13pub mod transaction;
14
15use async_trait::async_trait;
16use sqlx::Executor;
17use sqlx::postgres::{PgConnectOptions, PgPool, PgSslMode};
18use std::str::FromStr;
19use std::sync::Arc;
20
21use super::provider::DatabaseProvider;
22use crate::error::{DatabaseResult, RepositoryError};
23use crate::models::{
24    DatabaseInfo, DatabaseTransaction, DbValue, JsonRow, QueryResult, QuerySelector, ToDbValue,
25};
26use conversion::{bind_params, row_to_json, rows_to_result};
27use transaction::PostgresTransaction;
28
29#[derive(Debug)]
30pub struct PostgresProvider {
31    pool: Arc<PgPool>,
32}
33
34impl PostgresProvider {
35    pub async fn new(database_url: &str) -> DatabaseResult<Self> {
36        Self::new_with_pool(database_url, &connection::PoolConfig::default()).await
37    }
38
39    pub async fn new_with_pool(
40        database_url: &str,
41        pool_config: &connection::PoolConfig,
42    ) -> DatabaseResult<Self> {
43        let mut connect_options = PgConnectOptions::from_str(database_url)?;
44
45        let ssl_mode = if database_url.contains("sslmode=require") {
46            PgSslMode::Require
47        } else if database_url.contains("sslmode=disable") {
48            PgSslMode::Disable
49        } else {
50            PgSslMode::Prefer
51        };
52
53        connect_options = connect_options
54            .application_name("systemprompt")
55            .statement_cache_capacity(0)
56            .ssl_mode(ssl_mode)
57            .options([("client_min_messages", "warning")]);
58
59        if let Some(ca_cert_path) = Self::get_cert_path() {
60            connect_options = connect_options.ssl_root_cert(&ca_cert_path);
61        }
62
63        let pool = connection::connect_with_retry(
64            connection::build_pool_options(pool_config),
65            connect_options,
66        )
67        .await?;
68
69        Ok(Self {
70            pool: Arc::new(pool),
71        })
72    }
73
74    #[must_use]
75    pub const fn from_pool(pool: Arc<PgPool>) -> Self {
76        Self { pool }
77    }
78
79    fn get_cert_path() -> Option<std::path::PathBuf> {
80        std::env::var("PGCA_CERT_PATH")
81            .ok()
82            .map(std::path::PathBuf::from)
83    }
84
85    #[must_use]
86    pub fn pool(&self) -> &PgPool {
87        &self.pool
88    }
89}
90
91#[async_trait]
92impl DatabaseProvider for PostgresProvider {
93    fn get_postgres_pool(&self) -> Option<Arc<PgPool>> {
94        Some(Arc::clone(&self.pool))
95    }
96
97    async fn execute(
98        &self,
99        query: &dyn QuerySelector,
100        params: &[&dyn ToDbValue],
101    ) -> DatabaseResult<u64> {
102        let sql = query.select_query();
103        let query_obj = sqlx::query(sqlx::AssertSqlSafe(sql));
104        let query_obj = bind_params(query_obj, params);
105
106        let result = query_obj.execute(&*self.pool).await?;
107
108        Ok(result.rows_affected())
109    }
110
111    async fn execute_raw(&self, sql: &str) -> DatabaseResult<()> {
112        let mut conn = self.pool.acquire().await?;
113
114        conn.execute(sqlx::AssertSqlSafe(sql.to_owned())).await?;
115
116        Ok(())
117    }
118
119    async fn fetch_all(
120        &self,
121        query: &dyn QuerySelector,
122        params: &[&dyn ToDbValue],
123    ) -> DatabaseResult<Vec<JsonRow>> {
124        let sql = query.select_query();
125        let query_obj = sqlx::query(sqlx::AssertSqlSafe(sql));
126        let query_obj = bind_params(query_obj, params);
127
128        let rows = query_obj.fetch_all(&*self.pool).await?;
129
130        Ok(rows.iter().map(row_to_json).collect())
131    }
132
133    async fn fetch_one(
134        &self,
135        query: &dyn QuerySelector,
136        params: &[&dyn ToDbValue],
137    ) -> DatabaseResult<JsonRow> {
138        let sql = query.select_query();
139        let query_obj = sqlx::query(sqlx::AssertSqlSafe(sql));
140        let query_obj = bind_params(query_obj, params);
141
142        let row = query_obj.fetch_one(&*self.pool).await?;
143
144        Ok(row_to_json(&row))
145    }
146
147    async fn fetch_optional(
148        &self,
149        query: &dyn QuerySelector,
150        params: &[&dyn ToDbValue],
151    ) -> DatabaseResult<Option<JsonRow>> {
152        let sql = query.select_query();
153        let query_obj = sqlx::query(sqlx::AssertSqlSafe(sql));
154        let query_obj = bind_params(query_obj, params);
155
156        let row = query_obj.fetch_optional(&*self.pool).await?;
157
158        Ok(row.map(|r| row_to_json(&r)))
159    }
160
161    async fn fetch_scalar_value(
162        &self,
163        query: &dyn QuerySelector,
164        params: &[&dyn ToDbValue],
165    ) -> DatabaseResult<DbValue> {
166        let row = self.fetch_one(query, params).await?;
167
168        let first_value = row
169            .values()
170            .next()
171            .ok_or_else(|| RepositoryError::invalid_state("No columns in result"))?;
172
173        let db_value = match first_value {
174            serde_json::Value::String(s) => DbValue::String(s.clone()),
175            serde_json::Value::Number(n) => n
176                .as_i64()
177                .map(DbValue::Int)
178                .or_else(|| n.as_f64().map(DbValue::Float))
179                .unwrap_or(DbValue::NullFloat),
180            serde_json::Value::Bool(b) => DbValue::Bool(*b),
181            serde_json::Value::Null => DbValue::NullString,
182            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
183                return Err(RepositoryError::invalid_state("Unsupported value type"));
184            },
185        };
186
187        Ok(db_value)
188    }
189
190    async fn begin_transaction(&self) -> DatabaseResult<Box<dyn DatabaseTransaction>> {
191        let tx = self.pool.begin().await?;
192
193        Ok(Box::new(PostgresTransaction::new(tx)))
194    }
195
196    async fn get_database_info(&self) -> DatabaseResult<DatabaseInfo> {
197        introspection::get_database_info(&self.pool).await
198    }
199
200    async fn test_connection(&self) -> DatabaseResult<()> {
201        sqlx::query("SELECT 1").fetch_one(&*self.pool).await?;
202        Ok(())
203    }
204
205    async fn execute_batch(&self, sql: &str) -> DatabaseResult<()> {
206        let statements = crate::services::SqlExecutor::parse_sql_statements(sql)?;
207        for statement in statements {
208            sqlx::query(sqlx::AssertSqlSafe(statement))
209                .execute(&*self.pool)
210                .await?;
211        }
212        Ok(())
213    }
214
215    async fn query_raw(&self, query: &dyn QuerySelector) -> DatabaseResult<QueryResult> {
216        let sql = query.select_query();
217        let start = std::time::Instant::now();
218
219        let rows = sqlx::query(sqlx::AssertSqlSafe(sql))
220            .fetch_all(&*self.pool)
221            .await?;
222
223        Ok(rows_to_result(rows, start))
224    }
225
226    async fn query_raw_with(
227        &self,
228        query: &dyn QuerySelector,
229        params: &[&dyn ToDbValue],
230    ) -> DatabaseResult<QueryResult> {
231        let sql = query.select_query();
232        let start = std::time::Instant::now();
233
234        let query_obj = bind_params(sqlx::query(sqlx::AssertSqlSafe(sql)), params);
235        let rows = query_obj.fetch_all(&*self.pool).await?;
236
237        Ok(rows_to_result(rows, start))
238    }
239}