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::connection(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::query(e.to_string()))
48            }
49            #[allow(unreachable_patterns)]
50            _ => Err(DataError::config("execute(): not a SQLx connection")),
51        }
52    }
53
54    /// Execute a parameterized statement; returns affected rows.
55    /// Placeholder syntax follows the backend (`?` sqlite/mysql, `$1…` pg).
56    pub async fn execute_bind(&mut self, sql: &str, binds: &[&str]) -> Result<u64, DataError> {
57        match self {
58            crate::data::db::OwnedDbConn::Sqlx(conn) => {
59                let mut q = sqlx::query(sql);
60                for b in binds {
61                    q = q.bind(*b);
62                }
63                q.execute(&mut **conn)
64                    .await
65                    .map(|r| r.rows_affected())
66                    .map_err(|e| DataError::query(e.to_string()))
67            }
68            #[allow(unreachable_patterns)]
69            _ => Err(DataError::config("execute_bind(): not a SQLx connection")),
70        }
71    }
72
73    /// Fetch a single scalar `i64` (e.g. `SELECT COUNT(*) …`).
74    pub async fn fetch_one_i64(&mut self, sql: &str) -> Result<i64, DataError> {
75        self.fetch_one_i64_bind(sql, &[]).await
76    }
77
78    /// Fetch a single scalar `i64` with positional binds.
79    pub async fn fetch_one_i64_bind(
80        &mut self,
81        sql: &str,
82        binds: &[&str],
83    ) -> Result<i64, DataError> {
84        match self {
85            crate::data::db::OwnedDbConn::Sqlx(conn) => {
86                let mut q = sqlx::query_scalar::<_, i64>(sql);
87                for b in binds {
88                    q = q.bind(*b);
89                }
90                q.fetch_one(&mut **conn)
91                    .await
92                    .map_err(|e| DataError::query(e.to_string()))
93            }
94            #[allow(unreachable_patterns)]
95            _ => Err(DataError::config("fetch_one_i64(): not a SQLx connection")),
96        }
97    }
98
99    /// Fetch a single scalar string with positional binds.
100    pub async fn fetch_one_string(
101        &mut self,
102        sql: &str,
103        binds: &[&str],
104    ) -> Result<String, DataError> {
105        match self {
106            crate::data::db::OwnedDbConn::Sqlx(conn) => {
107                let mut q = sqlx::query_scalar::<_, String>(sql);
108                for b in binds {
109                    q = q.bind(*b);
110                }
111                q.fetch_one(&mut **conn)
112                    .await
113                    .map_err(|e| DataError::query(e.to_string()))
114            }
115            #[allow(unreachable_patterns)]
116            _ => Err(DataError::config(
117                "fetch_one_string(): not a SQLx connection",
118            )),
119        }
120    }
121}
122
123impl crate::data::tx::ArclyTransaction {
124    /// Execute a parameterized statement inside this transaction.
125    /// Bind values positionally with the backend's placeholder syntax
126    /// (`?` for sqlite/mysql, `$1…` for postgres via the Any driver).
127    pub async fn execute_bind(&mut self, sql: &str, binds: &[&str]) -> Result<u64, DataError> {
128        match self {
129            crate::data::tx::ArclyTransaction::Sqlx(tx) => {
130                let mut q = sqlx::query(sql);
131                for b in binds {
132                    q = q.bind(*b);
133                }
134                q.execute(&mut **tx)
135                    .await
136                    .map(|r| r.rows_affected())
137                    .map_err(|e| DataError::query(e.to_string()))
138            }
139            #[allow(unreachable_patterns)]
140            _ => Err(DataError::config("execute_bind(): not a SQLx transaction")),
141        }
142    }
143
144    /// Fetch a single scalar `i64` inside this transaction.
145    pub async fn fetch_one_i64(&mut self, sql: &str) -> Result<i64, DataError> {
146        match self {
147            crate::data::tx::ArclyTransaction::Sqlx(tx) => sqlx::query_scalar::<_, i64>(sql)
148                .fetch_one(&mut **tx)
149                .await
150                .map_err(|e| DataError::query(e.to_string())),
151            #[allow(unreachable_patterns)]
152            _ => Err(DataError::config("fetch_one_i64(): not a SQLx transaction")),
153        }
154    }
155}