Skip to main content

valence_backend_postgres/
backend.rs

1//! Postgres storage engine.
2
3use sqlx::postgres::PgPool;
4
5use valence_backend_sql::{
6    apply_ttl_policy_postgres, create_record_postgres, define_unique_index_postgres,
7    delete_record_postgres, ensure_edges_postgres, ensure_table_postgres, execute_select_postgres,
8    get_edge_sources_postgres, get_edge_targets_postgres, get_record_postgres,
9    merge_record_postgres, relate_edge_postgres, sql_capabilities, ttl_deferred,
10    unrelate_edge_postgres, update_record_postgres,
11};
12use valence_core::backend::DatabaseBackend;
13use valence_core::compiled_query::CompiledQuery;
14use valence_core::error::{Error, Result};
15use valence_core::record_id::RecordId;
16use valence_core::ttl::SchemaTtlPolicy;
17use valence_core::{Database, DatabaseFromEngine, KnownEngines};
18
19/// Stable engine slug for router keys (`postgres:logical_name`).
20pub const ENGINE_ID: &str = KnownEngines::POSTGRES;
21
22/// Schema evaluator const for `database:` routing.
23pub const PRIMARY: DatabaseFromEngine = Database::from_engine("primary", ENGINE_ID);
24
25/// Postgres-backed [`DatabaseBackend`] using JSONB document rows.
26///
27/// # Examples
28///
29/// ```ignore
30/// use std::sync::Arc;
31/// use valence::{
32///     valence_schema, Database, DatabaseFromEngine, FieldType, PostgresBackend, Valence,
33///     POSTGRES_ENGINE_ID,
34/// };
35///
36/// const COUNTER_DB: DatabaseFromEngine =
37///     Database::from_engine("default", POSTGRES_ENGINE_ID);
38///
39/// valence_schema! {
40///     Counter {
41///         table: "counter",
42///         version: "0.1.0",
43///         database: COUNTER_DB,
44///         fields: [
45///             id: { r#type: FieldType::String, primary_key: true, required: true },
46///             value: { r#type: FieldType::Integer, required: true },
47///         ],
48///     }
49/// }
50///
51/// // Reads DATABASE_URL.
52/// let backend = PostgresBackend::from_env().await?;
53/// let valence = Valence::builder()
54///     .add_backend("default", Arc::new(backend))
55///     .build()?;
56/// assert_eq!(
57///     valence.backend_for_table("counter")?.engine_id(),
58///     POSTGRES_ENGINE_ID
59/// );
60/// # Ok::<(), valence::Error>(())
61/// ```
62#[derive(Debug, Clone)]
63pub struct PostgresBackend {
64    pool: PgPool,
65}
66
67impl PostgresBackend {
68    /// Start a builder for explicit host wiring.
69    pub fn builder() -> crate::config::PostgresBackendBuilder {
70        crate::config::PostgresBackendBuilder::new()
71    }
72
73    /// Connect using env defaults via builder (shorthand).
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if `DATABASE_URL` is missing or the connection fails.
78    pub async fn from_env() -> Result<Self> {
79        Self::builder().from_env_defaults().build().await
80    }
81
82    /// Connect using a Postgres connection URL.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`Error::Database`] if connecting or ensuring the edges schema fails.
87    pub async fn connect(url: &str) -> Result<Self> {
88        let pool = PgPool::connect(url)
89            .await
90            .map_err(|e| Error::database(e.to_string()))?;
91        ensure_edges_postgres(&pool).await?;
92        Ok(Self { pool })
93    }
94
95    /// Borrow the underlying pool.
96    pub fn pool(&self) -> &PgPool {
97        &self.pool
98    }
99}
100
101#[async_trait::async_trait]
102impl DatabaseBackend for PostgresBackend {
103    fn engine_id(&self) -> &'static str {
104        ENGINE_ID
105    }
106
107    fn capabilities(&self) -> valence_core::BackendCapabilities {
108        sql_capabilities("postgres")
109    }
110
111    async fn execute_compiled_query(
112        &self,
113        compiled: &CompiledQuery,
114    ) -> Result<Vec<serde_json::Value>> {
115        execute_select_postgres(&self.pool, compiled, "").await
116    }
117
118    async fn ensure_schemaless_table(&self, table: &str) -> Result<()> {
119        ensure_table_postgres(&self.pool, table).await
120    }
121
122    async fn get_record(&self, table: &str, id: &str) -> Result<Option<serde_json::Value>> {
123        get_record_postgres(&self.pool, table, id).await
124    }
125
126    async fn create_record(
127        &self,
128        table: &str,
129        content: serde_json::Value,
130    ) -> Result<serde_json::Value> {
131        create_record_postgres(&self.pool, table, content).await
132    }
133
134    async fn update_record(
135        &self,
136        table: &str,
137        id: &str,
138        content: serde_json::Value,
139    ) -> Result<serde_json::Value> {
140        update_record_postgres(&self.pool, table, id, content).await
141    }
142
143    async fn merge_record(
144        &self,
145        table: &str,
146        id: &str,
147        patch: serde_json::Value,
148    ) -> Result<serde_json::Value> {
149        merge_record_postgres(&self.pool, table, id, patch).await
150    }
151
152    async fn upsert_record(
153        &self,
154        table: &str,
155        id: &str,
156        content: serde_json::Value,
157    ) -> Result<serde_json::Value> {
158        if self.get_record(table, id).await?.is_some() {
159            self.update_record(table, id, content).await
160        } else {
161            let mut c = content;
162            if let Some(obj) = c.as_object_mut() {
163                obj.insert("id".into(), serde_json::json!({"table": table, "id": id}));
164            }
165            self.create_record(table, c).await
166        }
167    }
168
169    async fn delete_record(&self, table: &str, id: &str) -> Result<()> {
170        delete_record_postgres(&self.pool, table, id).await
171    }
172
173    async fn relate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
174        relate_edge_postgres(&self.pool, from, edge_table, to).await
175    }
176
177    async fn unrelate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
178        unrelate_edge_postgres(&self.pool, from, edge_table, to).await
179    }
180
181    async fn get_edge_targets(&self, from: &RecordId, edge_table: &str) -> Result<Vec<RecordId>> {
182        get_edge_targets_postgres(&self.pool, from, edge_table).await
183    }
184
185    async fn get_edge_sources(&self, to: &RecordId, edge_table: &str) -> Result<Vec<RecordId>> {
186        get_edge_sources_postgres(&self.pool, to, edge_table).await
187    }
188
189    async fn define_unique_index(&self, table: &str, field: &str) -> Result<()> {
190        define_unique_index_postgres(&self.pool, table, field).await
191    }
192
193    fn ttl_capability(&self) -> valence_core::ttl::BackendTtlCapability {
194        ttl_deferred()
195    }
196
197    async fn apply_ttl_policy(&self, table: &str, policy: &SchemaTtlPolicy) -> Result<()> {
198        apply_ttl_policy_postgres(&self.pool, table, policy).await
199    }
200}