Skip to main content

systemprompt_database/services/
database.rs

1//! Top-level [`Database`] handle that owns one or two
2//! [`DatabaseProvider`] instances (read + optional write) and exposes the
3//! query and transaction surface.
4//!
5//! Copyright (c) systemprompt.io — Business Source License 1.1.
6//! See <https://systemprompt.io> for licensing details.
7
8use super::postgres::PostgresProvider;
9use super::postgres::connection::PoolConfig;
10use super::provider::DatabaseProvider;
11use crate::error::{DatabaseResult, RepositoryError};
12use crate::models::{DatabaseInfo, QueryResult};
13use std::sync::Arc;
14
15pub struct Database {
16    provider: Arc<dyn DatabaseProvider>,
17    write_provider: Option<Arc<dyn DatabaseProvider>>,
18}
19
20impl std::fmt::Debug for Database {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("Database")
23            .field("backend", &"PostgreSQL")
24            .finish()
25    }
26}
27
28impl Database {
29    pub async fn new_postgres(url: &str) -> DatabaseResult<Self> {
30        let provider = PostgresProvider::new(url).await?;
31        Ok(Self {
32            provider: Arc::new(provider),
33            write_provider: None,
34        })
35    }
36
37    pub async fn from_config(db_type: &str, url: &str) -> DatabaseResult<Self> {
38        match db_type.to_lowercase().as_str() {
39            "postgres" | "postgresql" | "" => Self::new_postgres(url).await,
40            other => Err(RepositoryError::invalid_argument(format!(
41                "Unsupported database type: {other}. Only PostgreSQL is supported."
42            ))),
43        }
44    }
45
46    pub async fn from_config_with_write(
47        db_type: &str,
48        read_url: &str,
49        write_url: Option<&str>,
50        pool: &PoolConfig,
51    ) -> DatabaseResult<Self> {
52        let provider: Arc<dyn DatabaseProvider> = match db_type.to_lowercase().as_str() {
53            "postgres" | "postgresql" | "" => {
54                Arc::new(PostgresProvider::new_with_pool(read_url, pool).await?)
55            },
56            other => {
57                return Err(RepositoryError::invalid_argument(format!(
58                    "Unsupported database type: {other}. Only PostgreSQL is supported."
59                )));
60            },
61        };
62
63        let write_provider: Option<Arc<dyn DatabaseProvider>> = match write_url {
64            Some(url) => Some(Arc::new(PostgresProvider::new_with_pool(url, pool).await?)),
65            None => None,
66        };
67
68        Ok(Self {
69            provider,
70            write_provider,
71        })
72    }
73
74    #[must_use]
75    pub fn from_pools(read: Arc<sqlx::PgPool>, write: Option<Arc<sqlx::PgPool>>) -> Self {
76        let write_provider = write.map(|pool| -> Arc<dyn DatabaseProvider> {
77            Arc::new(PostgresProvider::from_pool(pool))
78        });
79        Self {
80            provider: Arc::new(PostgresProvider::from_pool(read)),
81            write_provider,
82        }
83    }
84
85    fn require_postgres(pool: Option<Arc<sqlx::PgPool>>) -> DatabaseResult<Arc<sqlx::PgPool>> {
86        pool.ok_or_else(|| RepositoryError::invalid_state("Database is not PostgreSQL"))
87    }
88
89    #[must_use]
90    pub fn read(&self) -> &dyn DatabaseProvider {
91        self.provider.as_ref()
92    }
93
94    #[must_use]
95    pub fn write(&self) -> &dyn DatabaseProvider {
96        self.write_provider
97            .as_deref()
98            .unwrap_or_else(|| self.provider.as_ref())
99    }
100
101    #[must_use]
102    pub fn pool(&self) -> Option<Arc<sqlx::PgPool>> {
103        self.read().get_postgres_pool()
104    }
105
106    pub fn pool_arc(&self) -> DatabaseResult<Arc<sqlx::PgPool>> {
107        Self::require_postgres(self.read().get_postgres_pool())
108    }
109
110    #[must_use]
111    pub fn write_pool(&self) -> Option<Arc<sqlx::PgPool>> {
112        self.write().get_postgres_pool()
113    }
114
115    pub fn write_pool_arc(&self) -> DatabaseResult<Arc<sqlx::PgPool>> {
116        Self::require_postgres(self.write().get_postgres_pool())
117    }
118
119    #[must_use]
120    pub fn has_write_pool(&self) -> bool {
121        self.write_provider.is_some()
122    }
123
124    pub async fn execute_batch(&self, sql: &str) -> DatabaseResult<()> {
125        self.write().execute_batch(sql).await
126    }
127
128    pub async fn get_info(&self) -> DatabaseResult<DatabaseInfo> {
129        self.read().get_database_info().await
130    }
131
132    pub async fn test_connection(&self) -> DatabaseResult<()> {
133        self.provider.test_connection().await?;
134        if let Some(wp) = &self.write_provider {
135            wp.test_connection().await?;
136        }
137        Ok(())
138    }
139
140    pub async fn begin(&self) -> DatabaseResult<sqlx::Transaction<'_, sqlx::Postgres>> {
141        let pool = self.write_pool_arc()?;
142        pool.begin().await.map_err(Into::into)
143    }
144}
145
146pub type DbPool = Arc<Database>;
147
148pub trait DatabaseExt {
149    fn database(&self) -> Arc<Database>;
150}
151
152impl DatabaseExt for Arc<Database> {
153    fn database(&self) -> Arc<Database> {
154        Self::clone(self)
155    }
156}
157
158#[async_trait::async_trait]
159impl DatabaseProvider for Database {
160    fn get_postgres_pool(&self) -> Option<Arc<sqlx::PgPool>> {
161        self.read().get_postgres_pool()
162    }
163
164    async fn execute(
165        &self,
166        query: &dyn crate::models::QuerySelector,
167        params: &[&dyn crate::models::ToDbValue],
168    ) -> DatabaseResult<u64> {
169        self.write().execute(query, params).await
170    }
171
172    async fn execute_raw(&self, sql: &str) -> DatabaseResult<()> {
173        self.write().execute_raw(sql).await
174    }
175
176    async fn fetch_all(
177        &self,
178        query: &dyn crate::models::QuerySelector,
179        params: &[&dyn crate::models::ToDbValue],
180    ) -> DatabaseResult<Vec<crate::models::JsonRow>> {
181        self.read().fetch_all(query, params).await
182    }
183
184    async fn fetch_one(
185        &self,
186        query: &dyn crate::models::QuerySelector,
187        params: &[&dyn crate::models::ToDbValue],
188    ) -> DatabaseResult<crate::models::JsonRow> {
189        self.read().fetch_one(query, params).await
190    }
191
192    async fn fetch_optional(
193        &self,
194        query: &dyn crate::models::QuerySelector,
195        params: &[&dyn crate::models::ToDbValue],
196    ) -> DatabaseResult<Option<crate::models::JsonRow>> {
197        self.read().fetch_optional(query, params).await
198    }
199
200    async fn fetch_scalar_value(
201        &self,
202        query: &dyn crate::models::QuerySelector,
203        params: &[&dyn crate::models::ToDbValue],
204    ) -> DatabaseResult<crate::models::DbValue> {
205        self.read().fetch_scalar_value(query, params).await
206    }
207
208    async fn begin_transaction(
209        &self,
210    ) -> DatabaseResult<Box<dyn crate::models::DatabaseTransaction>> {
211        self.write().begin_transaction().await
212    }
213
214    async fn get_database_info(&self) -> DatabaseResult<DatabaseInfo> {
215        self.read().get_database_info().await
216    }
217
218    async fn test_connection(&self) -> DatabaseResult<()> {
219        self.read().test_connection().await
220    }
221
222    async fn execute_batch(&self, sql: &str) -> DatabaseResult<()> {
223        self.write().execute_batch(sql).await
224    }
225
226    async fn query_raw(
227        &self,
228        query: &dyn crate::models::QuerySelector,
229    ) -> DatabaseResult<QueryResult> {
230        self.read().query_raw(query).await
231    }
232
233    async fn query_raw_with(
234        &self,
235        query: &dyn crate::models::QuerySelector,
236        params: &[&dyn crate::models::ToDbValue],
237    ) -> DatabaseResult<QueryResult> {
238        self.read().query_raw_with(query, params).await
239    }
240}