Skip to main content

systemprompt_database/services/
database.rs

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