Skip to main content

rusticx_postgres/
adapter.rs

1use async_trait::async_trait;
2use rusticx_core::{
3    adapter::DatabaseAdapter,
4    error::{Result, RusticxError},
5    model::TableSchema,
6    query::QueryBuilder,
7    value::{Row, Value},
8};
9use rusticx_sql::{compiler::SqlCompiler, dialect::PostgresDialect};
10use sqlx::{postgres::PgPoolOptions, PgPool, Row as SqlxRow};
11use tracing::debug;
12
13use crate::convert::pg_row_to_row;
14
15/// Async PostgreSQL adapter backed by an `sqlx` connection pool.
16///
17/// Obtain one via [`PostgresAdapter::connect_url`] (quick) or
18/// [`PostgresAdapter::connect`] (full [`PostgresConfig`] options).
19pub struct PostgresAdapter {
20    pool: PgPool,
21    dialect: PostgresDialect,
22}
23
24/// Configuration for the PostgreSQL connection pool.
25///
26/// ```rust,ignore
27/// let adapter = PostgresAdapter::connect(
28///     PostgresConfig::new("postgres://user:pass@localhost/mydb")
29///         .max_connections(20)
30///         .min_connections(2)
31/// ).await?;
32/// ```
33#[derive(Debug, Clone)]
34pub struct PostgresConfig {
35    pub url: String,
36    pub max_connections: u32,
37    pub min_connections: u32,
38}
39
40impl PostgresConfig {
41    pub fn new(url: impl Into<String>) -> Self {
42        Self {
43            url: url.into(),
44            max_connections: 10,
45            min_connections: 1,
46        }
47    }
48
49    pub fn max_connections(mut self, n: u32) -> Self {
50        self.max_connections = n;
51        self
52    }
53
54    pub fn min_connections(mut self, n: u32) -> Self {
55        self.min_connections = n;
56        self
57    }
58}
59
60impl PostgresAdapter {
61    pub async fn connect(config: PostgresConfig) -> Result<Self> {
62        let pool = PgPoolOptions::new()
63            .max_connections(config.max_connections)
64            .min_connections(config.min_connections)
65            .connect(&config.url)
66            .await
67            .map_err(|e| RusticxError::Connection(e.to_string()))?;
68
69        Ok(Self { pool, dialect: PostgresDialect })
70    }
71
72    pub async fn connect_url(url: impl Into<String>) -> Result<Self> {
73        Self::connect(PostgresConfig::new(url)).await
74    }
75
76    fn compiler(&self) -> SqlCompiler<'_, PostgresDialect> {
77        SqlCompiler::new(&self.dialect)
78    }
79
80    /// Bind rusticx Values onto a sqlx PgQuery.
81    async fn execute_with_bindings(&self, sql: &str, bindings: Vec<Value>) -> Result<u64> {
82        let mut q = sqlx::query(sql);
83        for val in bindings {
84            q = bind_value(q, val);
85        }
86        let res = q.execute(&self.pool).await.map_err(|e| RusticxError::Query(e.to_string()))?;
87        Ok(res.rows_affected())
88    }
89
90    async fn fetch_rows_with_bindings(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>> {
91        let mut q = sqlx::query(sql);
92        for val in bindings {
93            q = bind_value(q, val);
94        }
95        let rows = q.fetch_all(&self.pool).await.map_err(|e| RusticxError::Query(e.to_string()))?;
96        Ok(rows.into_iter().map(pg_row_to_row).collect())
97    }
98}
99
100fn bind_value<'q>(
101    q: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
102    val: Value,
103) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
104    match val {
105        Value::Null => q.bind(Option::<String>::None),
106        Value::Bool(b) => q.bind(b),
107        Value::Int(i) => q.bind(i),
108        Value::Float(f) => q.bind(f),
109        Value::Text(s) => q.bind(s),
110        Value::Bytes(b) => q.bind(b),
111        Value::Uuid(u) => q.bind(u),
112        Value::DateTime(dt) => q.bind(dt),
113        Value::Json(j) => q.bind(j),
114        Value::Array(arr) => {
115            let json = serde_json::to_value(&arr).unwrap_or(serde_json::Value::Null);
116            q.bind(json)
117        }
118        Value::Map(m) => {
119            let json = serde_json::to_value(&m).unwrap_or(serde_json::Value::Null);
120            q.bind(json)
121        }
122    }
123}
124
125#[async_trait]
126impl DatabaseAdapter for PostgresAdapter {
127    fn name(&self) -> &'static str {
128        "postgres"
129    }
130
131    async fn ping(&self) -> Result<()> {
132        sqlx::query("SELECT 1")
133            .execute(&self.pool)
134            .await
135            .map_err(|e| RusticxError::Connection(e.to_string()))?;
136        Ok(())
137    }
138
139    async fn close(&self) -> Result<()> {
140        self.pool.close().await;
141        Ok(())
142    }
143
144    async fn create_table(&self, schema: &TableSchema) -> Result<()> {
145        let sql = self.compiler().create_table(schema);
146        debug!(sql = %sql, "create_table");
147        // May contain multiple statements (table + indexes), execute each
148        for stmt in sql.split(';').map(str::trim).filter(|s| !s.is_empty()) {
149            sqlx::query(stmt)
150                .execute(&self.pool)
151                .await
152                .map_err(|e| RusticxError::Schema(e.to_string()))?;
153        }
154        Ok(())
155    }
156
157    async fn drop_table(&self, table: &str) -> Result<()> {
158        let sql = self.compiler().drop_table(table);
159        sqlx::query(&sql)
160            .execute(&self.pool)
161            .await
162            .map_err(|e| RusticxError::Schema(e.to_string()))?;
163        Ok(())
164    }
165
166    async fn table_exists(&self, table: &str) -> Result<bool> {
167        let row = sqlx::query(
168            "SELECT COUNT(*) as cnt FROM information_schema.tables WHERE table_name = $1",
169        )
170        .bind(table)
171        .fetch_one(&self.pool)
172        .await
173        .map_err(|e| RusticxError::Query(e.to_string()))?;
174        let cnt: i64 = row.try_get("cnt").unwrap_or(0);
175        Ok(cnt > 0)
176    }
177
178    async fn insert(&self, table: &str, row: Row) -> Result<Row> {
179        let pairs: Vec<(String, Value)> = row.into_iter().collect();
180        let (sql, bindings) = self.compiler().insert(table, &pairs);
181        debug!(sql = %sql, "insert");
182
183        let mut q = sqlx::query(&sql);
184        for val in bindings {
185            q = bind_value(q, val);
186        }
187
188        let pg_row = q
189            .fetch_one(&self.pool)
190            .await
191            .map_err(|e| RusticxError::Query(e.to_string()))?;
192
193        Ok(pg_row_to_row(pg_row))
194    }
195
196    async fn insert_many(&self, table: &str, rows: Vec<Row>) -> Result<u64> {
197        let mut tx = self.pool.begin().await.map_err(|e| RusticxError::Transaction(e.to_string()))?;
198        let mut count = 0u64;
199        for row in rows {
200            let pairs: Vec<(String, Value)> = row.into_iter().collect();
201            let (sql, bindings) = self.compiler().insert(table, &pairs);
202            let mut q = sqlx::query(&sql);
203            for val in bindings {
204                q = bind_value(q, val);
205            }
206            q.execute(&mut *tx).await.map_err(|e| RusticxError::Query(e.to_string()))?;
207            count += 1;
208        }
209        tx.commit().await.map_err(|e| RusticxError::Transaction(e.to_string()))?;
210        Ok(count)
211    }
212
213    async fn find(&self, query: &QueryBuilder) -> Result<Vec<Row>> {
214        let (sql, bindings) = self.compiler().select(query);
215        debug!(sql = %sql, "find");
216        self.fetch_rows_with_bindings(&sql, bindings).await
217    }
218
219    async fn find_one(&self, query: &QueryBuilder) -> Result<Option<Row>> {
220        let mut qb = query.clone();
221        qb.limit = Some(1);
222        let (sql, bindings) = self.compiler().select(&qb);
223        debug!(sql = %sql, "find_one");
224        let rows = self.fetch_rows_with_bindings(&sql, bindings).await?;
225        Ok(rows.into_iter().next())
226    }
227
228    async fn update(&self, query: &QueryBuilder) -> Result<u64> {
229        let (sql, bindings) = self.compiler().update(query);
230        debug!(sql = %sql, "update");
231        self.execute_with_bindings(&sql, bindings).await
232    }
233
234    async fn delete(&self, query: &QueryBuilder) -> Result<u64> {
235        let (sql, bindings) = self.compiler().delete(query);
236        debug!(sql = %sql, "delete");
237        self.execute_with_bindings(&sql, bindings).await
238    }
239
240    async fn count(&self, query: &QueryBuilder) -> Result<u64> {
241        let (sql, bindings) = self.compiler().count(query);
242        debug!(sql = %sql, "count");
243        let mut q = sqlx::query(&sql);
244        for val in bindings {
245            q = bind_value(q, val);
246        }
247        let row = q.fetch_one(&self.pool).await.map_err(|e| RusticxError::Query(e.to_string()))?;
248        let cnt: i64 = row.try_get("count").unwrap_or(0);
249        Ok(cnt as u64)
250    }
251
252    async fn execute_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<u64> {
253        self.execute_with_bindings(sql, bindings).await
254    }
255
256    async fn query_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>> {
257        self.fetch_rows_with_bindings(sql, bindings).await
258    }
259}