Skip to main content

valence_backend_postgres/
backend.rs

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