Skip to main content

arcly_http/data/drivers/
sqlx.rs

1//! SQLx adapter — `AnyPool` over postgres / mysql / sqlite.
2//!
3//! Uses SQLx's `Any` driver so one facade variant covers every backend the
4//! enabled `db-sqlx-*` features compile in; the concrete backend is chosen
5//! by the connection URL at boot.
6
7use crate::data::db::DbDriver;
8use crate::data::DataError;
9
10/// Build a SQLx-backed driver for [`ArclyDbPool`](crate::data::db::ArclyDbPool).
11///
12/// ```ignore
13/// let primary = sqlx_driver("postgres://app@db-primary/orders", 16).await?;
14/// let replica = sqlx_driver("postgres://app@db-replica/orders", 16).await?;
15/// ctx.provide(DataSourceRegistry::new(
16///     ArclyDbPool::new("default", primary).with_replica(replica),
17/// ));
18/// ```
19pub async fn sqlx_driver(url: &str, max_connections: u32) -> Result<DbDriver, DataError> {
20    // Register the compiled-in backends with the Any driver (idempotent).
21    sqlx::any::install_default_drivers();
22
23    let pool = sqlx::any::AnyPoolOptions::new()
24        .max_connections(max_connections)
25        .connect(url)
26        .await
27        .map_err(|e| DataError(format!("sqlx connect failed: {e}")))?;
28
29    Ok(DbDriver::Sqlx(pool))
30}
31
32// ─── Query facade ─────────────────────────────────────────────────────────────
33//
34// A deliberately small surface: enough for services to run parameterized SQL
35// through the unified handle. Apps wanting the full SQLx API can match on
36// the enum variants directly — the facade never hides the native types.
37
38impl crate::data::db::OwnedDbConn {
39    /// Execute a statement (INSERT/UPDATE/DELETE/DDL); returns affected rows.
40    pub async fn execute(&mut self, sql: &str) -> Result<u64, DataError> {
41        match self {
42            crate::data::db::OwnedDbConn::Sqlx(conn) => {
43                use sqlx::Executor;
44                conn.execute(sql)
45                    .await
46                    .map(|r| r.rows_affected())
47                    .map_err(|e| DataError(e.to_string()))
48            }
49            #[allow(unreachable_patterns)]
50            _ => Err(DataError("execute(): not a SQLx connection".into())),
51        }
52    }
53
54    /// Fetch a single scalar `i64` (e.g. `SELECT COUNT(*) …`).
55    pub async fn fetch_one_i64(&mut self, sql: &str) -> Result<i64, DataError> {
56        match self {
57            crate::data::db::OwnedDbConn::Sqlx(conn) => sqlx::query_scalar::<_, i64>(sql)
58                .fetch_one(&mut **conn)
59                .await
60                .map_err(|e| DataError(e.to_string())),
61            #[allow(unreachable_patterns)]
62            _ => Err(DataError("fetch_one_i64(): not a SQLx connection".into())),
63        }
64    }
65
66    /// Fetch a single scalar string with positional binds.
67    pub async fn fetch_one_string(
68        &mut self,
69        sql: &str,
70        binds: &[&str],
71    ) -> Result<String, DataError> {
72        match self {
73            crate::data::db::OwnedDbConn::Sqlx(conn) => {
74                let mut q = sqlx::query_scalar::<_, String>(sql);
75                for b in binds {
76                    q = q.bind(*b);
77                }
78                q.fetch_one(&mut **conn)
79                    .await
80                    .map_err(|e| DataError(e.to_string()))
81            }
82            #[allow(unreachable_patterns)]
83            _ => Err(DataError(
84                "fetch_one_string(): not a SQLx connection".into(),
85            )),
86        }
87    }
88}
89
90impl crate::data::tx::ArclyTransaction {
91    /// Execute a parameterized statement inside this transaction.
92    /// Bind values positionally with the backend's placeholder syntax
93    /// (`?` for sqlite/mysql, `$1…` for postgres via the Any driver).
94    pub async fn execute_bind(&mut self, sql: &str, binds: &[&str]) -> Result<u64, DataError> {
95        match self {
96            crate::data::tx::ArclyTransaction::Sqlx(tx) => {
97                let mut q = sqlx::query(sql);
98                for b in binds {
99                    q = q.bind(*b);
100                }
101                q.execute(&mut **tx)
102                    .await
103                    .map(|r| r.rows_affected())
104                    .map_err(|e| DataError(e.to_string()))
105            }
106            #[allow(unreachable_patterns)]
107            _ => Err(DataError("execute_bind(): not a SQLx transaction".into())),
108        }
109    }
110
111    /// Fetch a single scalar `i64` inside this transaction.
112    pub async fn fetch_one_i64(&mut self, sql: &str) -> Result<i64, DataError> {
113        match self {
114            crate::data::tx::ArclyTransaction::Sqlx(tx) => sqlx::query_scalar::<_, i64>(sql)
115                .fetch_one(&mut **tx)
116                .await
117                .map_err(|e| DataError(e.to_string())),
118            #[allow(unreachable_patterns)]
119            _ => Err(DataError("fetch_one_i64(): not a SQLx transaction".into())),
120        }
121    }
122}