Skip to main content

alibi_sqlx/
pool.rs

1//! Engine-tagged `SQLx` pools and transactions, and statement execution.
2
3use crate::error::map_sqlx_err;
4use crate::sql::Sql;
5use alibi_core::config::AuthConfig;
6use alibi_core::error::{AuthError, AuthResult};
7#[cfg(feature = "postgres")]
8use sqlx::postgres::{PgPool, PgRow, Postgres};
9#[cfg(feature = "sqlite")]
10use sqlx::sqlite::{Sqlite, SqlitePool, SqliteRow};
11use sqlx::{AssertSqlSafe, Decode, Type};
12
13#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
14compile_error!("alibi-sqlx needs the `sqlite` or `postgres` feature");
15
16/// The database engine behind a pool.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Engine {
19    Sqlite,
20    Postgres,
21}
22
23/// An application's `SQLx` pool. Better Auth shares it with the application.
24#[derive(Clone, Debug)]
25pub enum SqlxPool {
26    #[cfg(feature = "sqlite")]
27    Sqlite(SqlitePool),
28    #[cfg(feature = "postgres")]
29    Postgres(PgPool),
30}
31
32#[cfg(feature = "sqlite")]
33impl From<SqlitePool> for SqlxPool {
34    fn from(pool: SqlitePool) -> Self {
35        Self::Sqlite(pool)
36    }
37}
38
39#[cfg(feature = "postgres")]
40impl From<PgPool> for SqlxPool {
41    fn from(pool: PgPool) -> Self {
42        Self::Postgres(pool)
43    }
44}
45
46impl SqlxPool {
47    /// Connect a SQLite (`sqlite:`) or PostgreSQL (`postgres:`/`postgresql:`) URL.
48    ///
49    /// # Errors
50    ///
51    /// Returns the driver error, or a configuration error for a scheme whose
52    /// engine feature is not enabled.
53    pub async fn connect(url: &str) -> Result<Self, sqlx::Error> {
54        #[cfg(feature = "sqlite")]
55        if url.starts_with("sqlite:") {
56            return SqlitePool::connect(url).await.map(Self::Sqlite);
57        }
58        #[cfg(feature = "postgres")]
59        if url.starts_with("postgres:") || url.starts_with("postgresql:") {
60            return PgPool::connect(url).await.map(Self::Postgres);
61        }
62        Err(sqlx::Error::Configuration(
63            "the database URL scheme is not an enabled Better Auth SQLx engine".into(),
64        ))
65    }
66
67    #[must_use]
68    pub const fn engine(&self) -> Engine {
69        match self {
70            #[cfg(feature = "sqlite")]
71            Self::Sqlite(_) => Engine::Sqlite,
72            #[cfg(feature = "postgres")]
73            Self::Postgres(_) => Engine::Postgres,
74        }
75    }
76
77    #[cfg(feature = "sqlite")]
78    #[must_use]
79    pub const fn as_sqlite(&self) -> Option<&SqlitePool> {
80        match self {
81            Self::Sqlite(pool) => Some(pool),
82            #[cfg(feature = "postgres")]
83            Self::Postgres(_) => None,
84        }
85    }
86
87    #[cfg(feature = "postgres")]
88    #[must_use]
89    pub const fn as_postgres(&self) -> Option<&PgPool> {
90        match self {
91            Self::Postgres(pool) => Some(pool),
92            #[cfg(feature = "sqlite")]
93            Self::Sqlite(_) => None,
94        }
95    }
96
97    /// Close every pooled connection.
98    pub async fn close(&self) {
99        match self {
100            #[cfg(feature = "sqlite")]
101            Self::Sqlite(pool) => pool.close().await,
102            #[cfg(feature = "postgres")]
103            Self::Postgres(pool) => pool.close().await,
104        }
105    }
106
107    /// Run `statements` in order, without bound arguments. Generated
108    /// application migrations install their tables this way.
109    ///
110    /// # Errors
111    ///
112    /// Returns the first driver error.
113    pub async fn execute_batch(&self, statements: &[&str]) -> Result<(), sqlx::Error> {
114        for statement in statements {
115            let statement = AssertSqlSafe((*statement).to_owned());
116            match self {
117                #[cfg(feature = "sqlite")]
118                Self::Sqlite(pool) => _ = sqlx::raw_sql(statement).execute(pool).await?,
119                #[cfg(feature = "postgres")]
120                Self::Postgres(pool) => _ = sqlx::raw_sql(statement).execute(pool).await?,
121            }
122        }
123        Ok(())
124    }
125
126    /// Begin a transaction. SQLite uses `BEGIN IMMEDIATE` when `immediate` is set,
127    /// acquiring its writer reservation before the first read.
128    pub(crate) async fn begin(&self, immediate: bool) -> AuthResult<SqlxTransaction> {
129        let inner = match self {
130            #[cfg(feature = "sqlite")]
131            Self::Sqlite(pool) if immediate => pool
132                .begin_with("BEGIN IMMEDIATE")
133                .await
134                .map(TransactionKind::Sqlite),
135            #[cfg(feature = "sqlite")]
136            Self::Sqlite(pool) => pool.begin().await.map(TransactionKind::Sqlite),
137            #[cfg(feature = "postgres")]
138            Self::Postgres(pool) => {
139                _ = immediate;
140                pool.begin().await.map(TransactionKind::Postgres)
141            }
142        }
143        .map_err(map_sqlx_err)?;
144        Ok(SqlxTransaction {
145            engine: self.engine(),
146            config: None,
147            inner: tokio::sync::Mutex::new(Some(inner)),
148        })
149    }
150}
151
152/// An open driver transaction.
153#[derive(Debug)]
154pub(crate) enum TransactionKind {
155    #[cfg(feature = "sqlite")]
156    Sqlite(sqlx::Transaction<'static, Sqlite>),
157    #[cfg(feature = "postgres")]
158    Postgres(sqlx::Transaction<'static, Postgres>),
159}
160
161/// An auth transaction shared with lifecycle hooks.
162///
163/// Statements serialize on an internal lock; hooks run between statements, so a
164/// hook may [`lock`](Self::lock) the transaction to run its own queries in it.
165#[derive(Debug)]
166pub struct SqlxTransaction {
167    engine: Engine,
168    pub(crate) config: Option<std::sync::Arc<AuthConfig>>,
169    inner: tokio::sync::Mutex<Option<TransactionKind>>,
170}
171
172/// Exclusive access to an open auth transaction.
173#[derive(Debug)]
174pub struct SqlxTransactionGuard<'a>(tokio::sync::MutexGuard<'a, Option<TransactionKind>>);
175
176impl SqlxTransactionGuard<'_> {
177    /// The SQLite connection running this transaction.
178    #[cfg(feature = "sqlite")]
179    pub fn sqlite(&mut self) -> Option<&mut sqlx::SqliteConnection> {
180        match self.0.as_mut()? {
181            TransactionKind::Sqlite(transaction) => Some(&mut **transaction),
182            #[cfg(feature = "postgres")]
183            TransactionKind::Postgres(_) => None,
184        }
185    }
186
187    /// The PostgreSQL connection running this transaction.
188    #[cfg(feature = "postgres")]
189    pub fn postgres(&mut self) -> Option<&mut sqlx::PgConnection> {
190        match self.0.as_mut()? {
191            TransactionKind::Postgres(transaction) => Some(&mut **transaction),
192            #[cfg(feature = "sqlite")]
193            TransactionKind::Sqlite(_) => None,
194        }
195    }
196}
197
198impl SqlxTransaction {
199    #[must_use]
200    pub const fn engine(&self) -> Engine {
201        self.engine
202    }
203
204    /// Wait for the current statement, then run statements on this transaction.
205    pub async fn lock(&self) -> SqlxTransactionGuard<'_> {
206        SqlxTransactionGuard(self.inner.lock().await)
207    }
208
209    pub(crate) async fn commit(self) -> AuthResult<()> {
210        match self.inner.into_inner() {
211            #[cfg(feature = "sqlite")]
212            Some(TransactionKind::Sqlite(transaction)) => transaction.commit().await,
213            #[cfg(feature = "postgres")]
214            Some(TransactionKind::Postgres(transaction)) => transaction.commit().await,
215            None => return Err(AuthError::internal("Transaction already finished")),
216        }
217        .map_err(map_sqlx_err)
218    }
219
220    pub(crate) async fn rollback(self) -> AuthResult<()> {
221        match self.inner.into_inner() {
222            #[cfg(feature = "sqlite")]
223            Some(TransactionKind::Sqlite(transaction)) => transaction.rollback().await,
224            #[cfg(feature = "postgres")]
225            Some(TransactionKind::Postgres(transaction)) => transaction.rollback().await,
226            None => return Err(AuthError::internal("Transaction already finished")),
227        }
228        .map_err(map_sqlx_err)
229    }
230}
231
232macro_rules! decodable {
233    ($($row:tt)*) => {
234        /// A model decodable from the rows of every enabled engine.
235        pub trait SqlxRow: $($row)* + Send + Unpin {}
236
237        impl<T> SqlxRow for T where T: $($row)* + Send + Unpin {}
238    };
239}
240
241#[cfg(all(feature = "sqlite", feature = "postgres"))]
242decodable!(for<'r> sqlx::FromRow<'r, SqliteRow> + for<'r> sqlx::FromRow<'r, PgRow>);
243#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
244decodable!(for<'r> sqlx::FromRow<'r, SqliteRow>);
245#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
246decodable!(for<'r> sqlx::FromRow<'r, PgRow>);
247
248macro_rules! scalar {
249    ($($bound:tt)*) => {
250        /// A single-column value decodable from every enabled engine.
251        pub(crate) trait SqlxScalar: $($bound)* + Send + Unpin {}
252
253        impl<T> SqlxScalar for T where T: $($bound)* + Send + Unpin {}
254    };
255}
256
257#[cfg(all(feature = "sqlite", feature = "postgres"))]
258scalar!(for<'r> Decode<'r, Sqlite> + Type<Sqlite> + for<'r> Decode<'r, Postgres> + Type<Postgres>);
259#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
260scalar!(for<'r> Decode<'r, Sqlite> + Type<Sqlite>);
261#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
262scalar!(for<'r> Decode<'r, Postgres> + Type<Postgres>);
263
264/// Where a statement runs: the shared pool or an open transaction.
265#[derive(Clone, Copy, Debug)]
266enum ExecConnection<'a> {
267    Pool(&'a SqlxPool),
268    Tx(&'a SqlxTransaction),
269}
270
271#[derive(Clone, Copy, Debug)]
272pub(crate) struct Exec<'a> {
273    connection: ExecConnection<'a>,
274    config: Option<&'a AuthConfig>,
275}
276impl<'a> Exec<'a> {
277    pub(crate) const fn pool(pool: &'a SqlxPool) -> Self {
278        Self {
279            connection: ExecConnection::Pool(pool),
280            config: None,
281        }
282    }
283    pub(crate) fn tx(tx: &'a SqlxTransaction) -> Self {
284        Self {
285            connection: ExecConnection::Tx(tx),
286            config: tx.config.as_deref(),
287        }
288    }
289    pub(crate) const fn with_config(mut self, config: &'a AuthConfig) -> Self {
290        self.config = Some(config);
291        self
292    }
293    pub(crate) fn sso_fields(self) -> Option<&'a alibi_core::field_policy::SessionFields> {
294        self.config
295            .map(|config| &config.advanced.database.sso_provider_fields)
296    }
297    pub(crate) fn schema_name(self) -> Option<&'a str> {
298        self.config
299            .and_then(|config| config.advanced.database.schema_name.as_deref())
300    }
301    fn qualify(self, mut sql: String) -> AuthResult<String> {
302        if let Some(mapping) = self
303            .config
304            .and_then(|config| config.advanced.database.two_factor.as_ref())
305        {
306            sql = alibi_core::database_sql::map_two_factor(&sql, mapping)?;
307        }
308        if let Some(mapping) = self
309            .config
310            .and_then(|config| config.advanced.database.sso_provider.as_ref())
311        {
312            sql = alibi_core::database_sql::map_sso_provider(&sql, mapping)?;
313        }
314        if self.engine() == Engine::Postgres
315            && let Some(schema) = self.schema_name()
316        {
317            return alibi_core::database_sql::qualify_schema(&sql, schema);
318        }
319        Ok(sql)
320    }
321}
322
323/// Bind `$sql` for the engine behind `$exec` and run `$body` with `$query`
324/// (text and arguments) on `$executor`.
325macro_rules! dispatch {
326    ($exec:expr, $sql:expr, |$query:ident, $executor:ident| $body:expr) => {{
327        let exec = $exec;
328        let (text, args) = $sql.into_parts();
329        let text = exec.qualify(text)?;
330        match exec.connection {
331            #[cfg(feature = "sqlite")]
332            ExecConnection::Pool(SqlxPool::Sqlite(pool)) => {
333                match crate::sql::sqlite_arguments(args) {
334                    Ok(arguments) => {
335                        let $executor = pool;
336                        let $query = (AssertSqlSafe(text), arguments);
337                        $body
338                    }
339                    Err(error) => Err(sqlx::Error::Encode(error)),
340                }
341            }
342            #[cfg(feature = "postgres")]
343            ExecConnection::Pool(SqlxPool::Postgres(pool)) => {
344                match crate::sql::postgres_arguments(args) {
345                    Ok(arguments) => {
346                        let $executor = pool;
347                        let $query = (AssertSqlSafe(text), arguments);
348                        $body
349                    }
350                    Err(error) => Err(sqlx::Error::Encode(error)),
351                }
352            }
353            ExecConnection::Tx(transaction) => {
354                let mut guard = transaction.inner.lock().await;
355                match guard.as_mut() {
356                    #[cfg(feature = "sqlite")]
357                    Some(TransactionKind::Sqlite(open)) => match crate::sql::sqlite_arguments(args)
358                    {
359                        Ok(arguments) => {
360                            let $executor = &mut **open;
361                            let $query = (AssertSqlSafe(text), arguments);
362                            $body
363                        }
364                        Err(error) => Err(sqlx::Error::Encode(error)),
365                    },
366                    #[cfg(feature = "postgres")]
367                    Some(TransactionKind::Postgres(open)) => {
368                        match crate::sql::postgres_arguments(args) {
369                            Ok(arguments) => {
370                                let $executor = &mut **open;
371                                let $query = (AssertSqlSafe(text), arguments);
372                                $body
373                            }
374                            Err(error) => Err(sqlx::Error::Encode(error)),
375                        }
376                    }
377                    None => Err(sqlx::Error::PoolClosed),
378                }
379            }
380        }
381        .map_err(map_sqlx_err)
382    }};
383}
384
385impl Exec<'_> {
386    pub(crate) const fn engine(self) -> Engine {
387        match self.connection {
388            ExecConnection::Pool(pool) => pool.engine(),
389            ExecConnection::Tx(transaction) => transaction.engine,
390        }
391    }
392
393    pub(crate) async fn fetch_all<M: SqlxRow>(self, sql: Sql) -> AuthResult<Vec<M>> {
394        dispatch!(self, sql, |query, executor| {
395            sqlx::query_as_with(query.0, query.1)
396                .fetch_all(executor)
397                .await
398        })
399    }
400
401    pub(crate) async fn fetch_optional<M: SqlxRow>(self, sql: Sql) -> AuthResult<Option<M>> {
402        dispatch!(self, sql, |query, executor| {
403            sqlx::query_as_with(query.0, query.1)
404                .fetch_optional(executor)
405                .await
406        })
407    }
408
409    /// Run a statement, returning its affected row count.
410    pub(crate) async fn execute(self, sql: Sql) -> AuthResult<u64> {
411        dispatch!(self, sql, |query, executor| {
412            sqlx::query_with(query.0, query.1)
413                .execute(executor)
414                .await
415                .map(|result| result.rows_affected())
416        })
417    }
418
419    pub(crate) async fn fetch_all_scalar<T: SqlxScalar>(self, sql: Sql) -> AuthResult<Vec<T>> {
420        dispatch!(self, sql, |query, executor| {
421            sqlx::query_scalar_with(query.0, query.1)
422                .fetch_all(executor)
423                .await
424        })
425    }
426
427    pub(crate) async fn fetch_scalar<T: SqlxScalar>(self, sql: Sql) -> AuthResult<Option<T>> {
428        dispatch!(self, sql, |query, executor| {
429            sqlx::query_scalar_with(query.0, query.1)
430                .fetch_optional(executor)
431                .await
432        })
433    }
434
435    /// Run a fixed multi-statement script without bound arguments.
436    pub(crate) async fn execute_script(self, script: &'static str) -> AuthResult<()> {
437        let script = self.qualify(script.to_owned())?;
438        let result = match self.connection {
439            #[cfg(feature = "sqlite")]
440            ExecConnection::Pool(SqlxPool::Sqlite(pool)) => sqlx::raw_sql(AssertSqlSafe(script))
441                .execute(pool)
442                .await
443                .map(|_| ()),
444            #[cfg(feature = "postgres")]
445            ExecConnection::Pool(SqlxPool::Postgres(pool)) => sqlx::raw_sql(AssertSqlSafe(script))
446                .execute(pool)
447                .await
448                .map(|_| ()),
449            ExecConnection::Tx(transaction) => {
450                let mut guard = transaction.inner.lock().await;
451                match guard.as_mut() {
452                    #[cfg(feature = "sqlite")]
453                    Some(TransactionKind::Sqlite(open)) => sqlx::raw_sql(AssertSqlSafe(script))
454                        .execute(&mut **open)
455                        .await
456                        .map(|_| ()),
457                    #[cfg(feature = "postgres")]
458                    Some(TransactionKind::Postgres(open)) => sqlx::raw_sql(AssertSqlSafe(script))
459                        .execute(&mut **open)
460                        .await
461                        .map(|_| ()),
462                    None => Err(sqlx::Error::PoolClosed),
463                }
464            }
465        };
466        result.map_err(map_sqlx_err)
467    }
468}