Skip to main content

prax_sqlx/
engine.rs

1//! SQLx query engine implementation.
2
3use crate::config::{DatabaseBackend, SqlxConfig};
4use crate::error::{SqlxError, SqlxResult};
5use crate::pool::SqlxPool;
6use crate::row::SqlxRow;
7use crate::types::quote_identifier;
8use prax_query::QueryResult;
9use prax_query::filter::FilterValue;
10use prax_query::traits::{BoxFuture, Model, QueryEngine};
11use sqlx::{Column, Row};
12use std::sync::Arc;
13use tracing::debug;
14
15/// SQLx-based query engine for Prax.
16///
17/// This engine provides compile-time checked queries through SQLx,
18/// supporting PostgreSQL, MySQL, and SQLite.
19///
20/// Two modes, controlled by the `tx` field:
21///
22/// - **Pool mode** (`tx == None`, the default): each query acquires a fresh
23///   connection from the pool and drops it after the call.
24/// - **Transaction mode** (`tx == Some(handle)`): every query routes through
25///   the transaction's pinned connection. The tx-bound engine is built by
26///   [`QueryEngine::transaction`], which issues `COMMIT` or `ROLLBACK` on the
27///   same transaction when the closure resolves.
28///
29/// # Example
30///
31/// ```rust,ignore
32/// use prax_sqlx::{SqlxEngine, SqlxConfig};
33///
34/// let config = SqlxConfig::from_url("postgres://localhost/mydb")?;
35/// let engine = SqlxEngine::new(config).await?;
36///
37/// // Execute queries
38/// let count = engine.count_table("users", None).await?;
39/// ```
40#[derive(Clone)]
41pub struct SqlxEngine {
42    pool: Arc<SqlxPool>,
43    backend: DatabaseBackend,
44    /// Present when this engine is bound to an in-flight transaction.
45    /// `None` in the normal pool-backed case.
46    tx: Option<TxHandle>,
47}
48
49/// A transaction in flight on one of the supported backends.
50///
51/// `Pool::begin()` yields a `sqlx::Transaction<'static>` that owns its
52/// pooled connection, so it can be shared between the finalizer in
53/// `QueryEngine::transaction` and every clone of the tx-bound engine. It is
54/// neither `Clone` nor usable as an `Executor` through a shared reference,
55/// so it lives behind an `Arc<futures::lock::Mutex<Option<_>>>`: the async
56/// mutex serializes query access and stays `Send` across `.await`, and the
57/// `Option` lets the finalizer `take()` the transaction to consume it via
58/// `commit` / `rollback` — after which any engine clones the closure stashed
59/// fail loudly instead of running queries against a dead transaction.
60#[derive(Clone)]
61enum TxHandle {
62    #[cfg(feature = "postgres")]
63    Postgres(TxSlot<sqlx::Postgres>),
64    #[cfg(feature = "mysql")]
65    MySql(TxSlot<sqlx::MySql>),
66    #[cfg(feature = "sqlite")]
67    Sqlite(TxSlot<sqlx::Sqlite>),
68}
69
70/// Shared, lockable slot holding an open backend transaction.
71type TxSlot<DB> = Arc<futures::lock::Mutex<Option<sqlx::Transaction<'static, DB>>>>;
72
73/// Guard on a [`TxSlot`], held for the duration of a single query.
74type TxGuard<'m, DB> = futures::lock::MutexGuard<'m, Option<sqlx::Transaction<'static, DB>>>;
75
76/// Error returned when a query is attempted through a transaction that has
77/// already been committed or rolled back.
78const TX_FINALIZED: &str = "transaction has already been committed or rolled back";
79
80impl SqlxEngine {
81    /// Create a new SQLx engine from configuration.
82    pub async fn new(config: SqlxConfig) -> SqlxResult<Self> {
83        let backend = config.backend;
84        let pool = SqlxPool::connect(&config).await?;
85        Ok(Self {
86            pool: Arc::new(pool),
87            backend,
88            tx: None,
89        })
90    }
91
92    /// Create a new engine from an existing pool.
93    pub fn from_pool(pool: SqlxPool) -> Self {
94        let backend = pool.backend();
95        Self {
96            pool: Arc::new(pool),
97            backend,
98            tx: None,
99        }
100    }
101
102    /// Get the database backend type.
103    pub fn backend(&self) -> DatabaseBackend {
104        self.backend
105    }
106
107    /// Get the connection pool.
108    pub fn pool(&self) -> &SqlxPool {
109        &self.pool
110    }
111
112    /// Close the engine and all connections.
113    pub async fn close(&self) {
114        self.pool.close().await;
115    }
116
117    // ==================== Low-Level Query Methods ====================
118
119    /// Execute a raw SQL query and return multiple rows.
120    pub async fn raw_query_many(
121        &self,
122        sql: &str,
123        params: &[FilterValue],
124    ) -> SqlxResult<Vec<SqlxRow>> {
125        debug!(sql = %sql, "Executing raw_query_many");
126
127        match &*self.pool {
128            #[cfg(feature = "postgres")]
129            SqlxPool::Postgres(pool) => {
130                let mut query = sqlx::query(sql);
131                for param in params {
132                    query = bind_pg_param(query, param)?;
133                }
134                let rows = match self.pg_tx_guard().await? {
135                    Some(mut guard) => {
136                        // Tx mode: run on the transaction's pinned connection
137                        // so the query lands inside the same BEGIN…COMMIT
138                        // block as every sibling call.
139                        let tx = guard
140                            .as_mut()
141                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
142                        query.fetch_all(&mut **tx).await?
143                    }
144                    None => query.fetch_all(pool).await?,
145                };
146                Ok(rows.into_iter().map(SqlxRow::Postgres).collect())
147            }
148            #[cfg(feature = "mysql")]
149            SqlxPool::MySql(pool) => {
150                let mut query = sqlx::query(sql);
151                for param in params {
152                    query = bind_mysql_param(query, param)?;
153                }
154                let rows = match self.mysql_tx_guard().await? {
155                    Some(mut guard) => {
156                        let tx = guard
157                            .as_mut()
158                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
159                        query.fetch_all(&mut **tx).await?
160                    }
161                    None => query.fetch_all(pool).await?,
162                };
163                Ok(rows.into_iter().map(SqlxRow::MySql).collect())
164            }
165            #[cfg(feature = "sqlite")]
166            SqlxPool::Sqlite(pool) => {
167                let mut query = sqlx::query(sql);
168                for param in params {
169                    query = bind_sqlite_param(query, param)?;
170                }
171                let rows = match self.sqlite_tx_guard().await? {
172                    Some(mut guard) => {
173                        let tx = guard
174                            .as_mut()
175                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
176                        query.fetch_all(&mut **tx).await?
177                    }
178                    None => query.fetch_all(pool).await?,
179                };
180                Ok(rows.into_iter().map(SqlxRow::Sqlite).collect())
181            }
182        }
183    }
184
185    /// Execute a raw SQL query and return a single row.
186    pub async fn raw_query_one(&self, sql: &str, params: &[FilterValue]) -> SqlxResult<SqlxRow> {
187        debug!(sql = %sql, "Executing raw_query_one");
188
189        match &*self.pool {
190            #[cfg(feature = "postgres")]
191            SqlxPool::Postgres(pool) => {
192                let mut query = sqlx::query(sql);
193                for param in params {
194                    query = bind_pg_param(query, param)?;
195                }
196                let row = match self.pg_tx_guard().await? {
197                    Some(mut guard) => {
198                        let tx = guard
199                            .as_mut()
200                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
201                        query.fetch_one(&mut **tx).await?
202                    }
203                    None => query.fetch_one(pool).await?,
204                };
205                Ok(SqlxRow::Postgres(row))
206            }
207            #[cfg(feature = "mysql")]
208            SqlxPool::MySql(pool) => {
209                let mut query = sqlx::query(sql);
210                for param in params {
211                    query = bind_mysql_param(query, param)?;
212                }
213                let row = match self.mysql_tx_guard().await? {
214                    Some(mut guard) => {
215                        let tx = guard
216                            .as_mut()
217                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
218                        query.fetch_one(&mut **tx).await?
219                    }
220                    None => query.fetch_one(pool).await?,
221                };
222                Ok(SqlxRow::MySql(row))
223            }
224            #[cfg(feature = "sqlite")]
225            SqlxPool::Sqlite(pool) => {
226                let mut query = sqlx::query(sql);
227                for param in params {
228                    query = bind_sqlite_param(query, param)?;
229                }
230                let row = match self.sqlite_tx_guard().await? {
231                    Some(mut guard) => {
232                        let tx = guard
233                            .as_mut()
234                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
235                        query.fetch_one(&mut **tx).await?
236                    }
237                    None => query.fetch_one(pool).await?,
238                };
239                Ok(SqlxRow::Sqlite(row))
240            }
241        }
242    }
243
244    /// Execute a raw SQL query and return an optional row.
245    pub async fn raw_query_optional(
246        &self,
247        sql: &str,
248        params: &[FilterValue],
249    ) -> SqlxResult<Option<SqlxRow>> {
250        debug!(sql = %sql, "Executing raw_query_optional");
251
252        match &*self.pool {
253            #[cfg(feature = "postgres")]
254            SqlxPool::Postgres(pool) => {
255                let mut query = sqlx::query(sql);
256                for param in params {
257                    query = bind_pg_param(query, param)?;
258                }
259                let row = match self.pg_tx_guard().await? {
260                    Some(mut guard) => {
261                        let tx = guard
262                            .as_mut()
263                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
264                        query.fetch_optional(&mut **tx).await?
265                    }
266                    None => query.fetch_optional(pool).await?,
267                };
268                Ok(row.map(SqlxRow::Postgres))
269            }
270            #[cfg(feature = "mysql")]
271            SqlxPool::MySql(pool) => {
272                let mut query = sqlx::query(sql);
273                for param in params {
274                    query = bind_mysql_param(query, param)?;
275                }
276                let row = match self.mysql_tx_guard().await? {
277                    Some(mut guard) => {
278                        let tx = guard
279                            .as_mut()
280                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
281                        query.fetch_optional(&mut **tx).await?
282                    }
283                    None => query.fetch_optional(pool).await?,
284                };
285                Ok(row.map(SqlxRow::MySql))
286            }
287            #[cfg(feature = "sqlite")]
288            SqlxPool::Sqlite(pool) => {
289                let mut query = sqlx::query(sql);
290                for param in params {
291                    query = bind_sqlite_param(query, param)?;
292                }
293                let row = match self.sqlite_tx_guard().await? {
294                    Some(mut guard) => {
295                        let tx = guard
296                            .as_mut()
297                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
298                        query.fetch_optional(&mut **tx).await?
299                    }
300                    None => query.fetch_optional(pool).await?,
301                };
302                Ok(row.map(SqlxRow::Sqlite))
303            }
304        }
305    }
306
307    /// Execute a SQL statement (INSERT, UPDATE, DELETE) and return affected rows.
308    pub async fn raw_execute(&self, sql: &str, params: &[FilterValue]) -> SqlxResult<u64> {
309        debug!(sql = %sql, "Executing raw_execute");
310
311        match &*self.pool {
312            #[cfg(feature = "postgres")]
313            SqlxPool::Postgres(pool) => {
314                let mut query = sqlx::query(sql);
315                for param in params {
316                    query = bind_pg_param(query, param)?;
317                }
318                let result = match self.pg_tx_guard().await? {
319                    Some(mut guard) => {
320                        let tx = guard
321                            .as_mut()
322                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
323                        query.execute(&mut **tx).await?
324                    }
325                    None => query.execute(pool).await?,
326                };
327                Ok(result.rows_affected())
328            }
329            #[cfg(feature = "mysql")]
330            SqlxPool::MySql(pool) => {
331                let mut query = sqlx::query(sql);
332                for param in params {
333                    query = bind_mysql_param(query, param)?;
334                }
335                let result = match self.mysql_tx_guard().await? {
336                    Some(mut guard) => {
337                        let tx = guard
338                            .as_mut()
339                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
340                        query.execute(&mut **tx).await?
341                    }
342                    None => query.execute(pool).await?,
343                };
344                Ok(result.rows_affected())
345            }
346            #[cfg(feature = "sqlite")]
347            SqlxPool::Sqlite(pool) => {
348                let mut query = sqlx::query(sql);
349                for param in params {
350                    query = bind_sqlite_param(query, param)?;
351                }
352                let result = match self.sqlite_tx_guard().await? {
353                    Some(mut guard) => {
354                        let tx = guard
355                            .as_mut()
356                            .ok_or_else(|| SqlxError::Internal(TX_FINALIZED.into()))?;
357                        query.execute(&mut **tx).await?
358                    }
359                    None => query.execute(pool).await?,
360                };
361                Ok(result.rows_affected())
362            }
363        }
364    }
365
366    /// Count rows in a table with optional filter.
367    pub async fn count_table(&self, table: &str, filter: Option<&str>) -> SqlxResult<u64> {
368        let table = quote_identifier(self.backend, table);
369        let sql = match filter {
370            Some(f) => format!("SELECT COUNT(*) as count FROM {} WHERE {}", table, f),
371            None => format!("SELECT COUNT(*) as count FROM {}", table),
372        };
373
374        let row = self.raw_query_one(&sql, &[]).await?;
375        match row {
376            #[cfg(feature = "postgres")]
377            SqlxRow::Postgres(r) => Ok(r.try_get::<i64, _>("count")? as u64),
378            #[cfg(feature = "mysql")]
379            SqlxRow::MySql(r) => Ok(r.try_get::<i64, _>("count")? as u64),
380            #[cfg(feature = "sqlite")]
381            SqlxRow::Sqlite(r) => Ok(r.try_get::<i64, _>("count")? as u64),
382        }
383    }
384
385    // ==================== Transaction Helpers ====================
386
387    /// Lock the in-flight Postgres transaction, if this engine is tx-bound.
388    ///
389    /// Returns `Ok(None)` in pool mode. A backend mismatch between the
390    /// transaction handle and the pool is an internal invariant violation
391    /// and errors rather than silently running outside the transaction.
392    #[cfg(feature = "postgres")]
393    async fn pg_tx_guard(&self) -> SqlxResult<Option<TxGuard<'_, sqlx::Postgres>>> {
394        match self.tx.as_ref() {
395            None => Ok(None),
396            Some(TxHandle::Postgres(slot)) => Ok(Some(slot.lock().await)),
397            // unreachable with a single backend feature enabled; keep the arm for multi-feature builds
398            #[allow(unreachable_patterns)]
399            Some(_) => Err(SqlxError::Internal(
400                "transaction handle backend does not match engine pool".into(),
401            )),
402        }
403    }
404
405    /// Lock the in-flight MySQL transaction, if this engine is tx-bound.
406    #[cfg(feature = "mysql")]
407    async fn mysql_tx_guard(&self) -> SqlxResult<Option<TxGuard<'_, sqlx::MySql>>> {
408        match self.tx.as_ref() {
409            None => Ok(None),
410            Some(TxHandle::MySql(slot)) => Ok(Some(slot.lock().await)),
411            // unreachable with a single backend feature enabled; keep the arm for multi-feature builds
412            #[allow(unreachable_patterns)]
413            Some(_) => Err(SqlxError::Internal(
414                "transaction handle backend does not match engine pool".into(),
415            )),
416        }
417    }
418
419    /// Lock the in-flight SQLite transaction, if this engine is tx-bound.
420    #[cfg(feature = "sqlite")]
421    async fn sqlite_tx_guard(&self) -> SqlxResult<Option<TxGuard<'_, sqlx::Sqlite>>> {
422        match self.tx.as_ref() {
423            None => Ok(None),
424            Some(TxHandle::Sqlite(slot)) => Ok(Some(slot.lock().await)),
425            // unreachable with a single backend feature enabled; keep the arm for multi-feature builds
426            #[allow(unreachable_patterns)]
427            Some(_) => Err(SqlxError::Internal(
428                "transaction handle backend does not match engine pool".into(),
429            )),
430        }
431    }
432}
433
434// ==================== Parameter Binding Helpers ====================
435
436/// Untyped NULL binding for Postgres.
437///
438/// `FilterValue::Null` must bind successfully against a column of any type
439/// (nullable INT4, UUID, TIMESTAMPTZ, custom ENUM, ...), but sqlx derives the
440/// parameter's declared type from the Rust type of the bind — so the old
441/// `Option::<String>::None` sent a TEXT-typed NULL that Postgres rejects on
442/// assignment to non-text columns ("column is of type integer but expression
443/// is of type text"). Declaring the parameter as the `unknown` pseudo-type
444/// makes the server infer the parameter type from context, which is always
445/// valid for a NULL. sqlx resolves the name to OID 705 once per connection
446/// and caches it. This mirrors `PgNull` in prax-postgres/src/types.rs.
447#[cfg(feature = "postgres")]
448#[derive(Debug, Clone, Copy)]
449struct UntypedNull;
450
451#[cfg(feature = "postgres")]
452impl sqlx::Type<sqlx::Postgres> for UntypedNull {
453    fn type_info() -> sqlx::postgres::PgTypeInfo {
454        sqlx::postgres::PgTypeInfo::with_name("unknown")
455    }
456}
457
458#[cfg(feature = "postgres")]
459impl<'q> sqlx::Encode<'q, sqlx::Postgres> for UntypedNull {
460    fn encode_by_ref(
461        &self,
462        _buf: &mut sqlx::postgres::PgArgumentBuffer,
463    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
464        Ok(sqlx::encode::IsNull::Yes)
465    }
466}
467
468#[cfg(feature = "postgres")]
469fn bind_pg_param<'q>(
470    query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
471    value: &'q FilterValue,
472) -> SqlxResult<sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>> {
473    Ok(match value {
474        FilterValue::String(s) => query.bind(s.as_str()),
475        FilterValue::Int(i) => query.bind(*i),
476        FilterValue::Float(f) => query.bind(*f),
477        FilterValue::Bool(b) => query.bind(*b),
478        FilterValue::Null => query.bind(UntypedNull),
479        FilterValue::Json(j) => query.bind(j.clone()),
480        FilterValue::List(_) => {
481            // Lists are deliberately unsupported in raw binds. Typed IN
482            // filters expand to scalar placeholders upstream
483            // (prax-query/src/filter.rs), so this arm only fires for
484            // hand-built raw queries.
485            return Err(SqlxError::type_conversion(
486                "list values not supported in raw binds; use typed IN filters",
487            ));
488        }
489    })
490}
491
492#[cfg(feature = "mysql")]
493fn bind_mysql_param<'q>(
494    query: sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>,
495    value: &'q FilterValue,
496) -> SqlxResult<sqlx::query::Query<'q, sqlx::MySql, sqlx::mysql::MySqlArguments>> {
497    Ok(match value {
498        FilterValue::String(s) => query.bind(s.as_str()),
499        FilterValue::Int(i) => query.bind(*i),
500        FilterValue::Float(f) => query.bind(*f),
501        FilterValue::Bool(b) => query.bind(*b),
502        FilterValue::Null => query.bind(Option::<String>::None),
503        FilterValue::Json(j) => query.bind(j.to_string()),
504        FilterValue::List(_) => {
505            return Err(SqlxError::type_conversion(
506                "list values not supported in raw binds; use typed IN filters",
507            ));
508        }
509    })
510}
511
512#[cfg(feature = "sqlite")]
513fn bind_sqlite_param<'q>(
514    query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>,
515    value: &'q FilterValue,
516) -> SqlxResult<sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>> {
517    Ok(match value {
518        FilterValue::String(s) => query.bind(s.as_str()),
519        FilterValue::Int(i) => query.bind(*i),
520        FilterValue::Float(f) => query.bind(*f),
521        FilterValue::Bool(b) => query.bind(*b),
522        FilterValue::Null => query.bind(Option::<String>::None),
523        FilterValue::Json(j) => query.bind(j.to_string()),
524        FilterValue::List(_) => {
525            return Err(SqlxError::type_conversion(
526                "list values not supported in raw binds; use typed IN filters",
527            ));
528        }
529    })
530}
531
532// ==================== Transaction Finalization ====================
533
534/// Commit or roll back a transaction after the closure has resolved.
535///
536/// The transaction is `take()`n out of the shared slot so any engine clones
537/// the closure stashed past the end of `transaction` fail loudly on their
538/// next query instead of silently running outside the transaction. On
539/// failure the rollback is best-effort: the caller's error is preserved, and
540/// dropping the connection aborts the transaction server-side anyway.
541async fn finalize_tx<R, DB>(slot: &TxSlot<DB>, result: QueryResult<R>) -> QueryResult<R>
542where
543    DB: sqlx::Database,
544{
545    let tx = slot.lock().await.take();
546    match (result, tx) {
547        (Ok(value), Some(tx)) => {
548            tx.commit()
549                .await
550                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
551            Ok(value)
552        }
553        (Err(err), Some(tx)) => {
554            if let Err(rb) = tx.rollback().await {
555                tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
556            }
557            Err(err)
558        }
559        (Ok(_), None) => Err(prax_query::QueryError::internal(
560            "transaction was finalized before commit could be issued",
561        )),
562        (Err(err), None) => Err(err),
563    }
564}
565
566// ==================== Aggregate Result Decoding ====================
567
568/// Decode one aggregate result row into a column-name → [`FilterValue`] map.
569///
570/// Aggregate and group-by result sets have no fixed schema: `COUNT` comes
571/// back as a backend-specific integer width, `AVG` as NUMERIC/NEWDECIMAL/
572/// REAL, and group-by queries include the grouped columns themselves. Each
573/// cell is probed in specificity order; NULL and unrecognized types decode
574/// to `FilterValue::Null` rather than aborting the whole query (matching the
575/// reference `decode_aggregate_cell` in prax-postgres).
576fn decode_aggregate_row(row: &SqlxRow) -> std::collections::HashMap<String, FilterValue> {
577    let mut map = std::collections::HashMap::new();
578    match row {
579        #[cfg(feature = "postgres")]
580        SqlxRow::Postgres(r) => {
581            for (i, col) in r.columns().iter().enumerate() {
582                map.insert(col.name().to_string(), decode_pg_aggregate_cell(r, i));
583            }
584        }
585        #[cfg(feature = "mysql")]
586        SqlxRow::MySql(r) => {
587            for (i, col) in r.columns().iter().enumerate() {
588                map.insert(col.name().to_string(), decode_mysql_aggregate_cell(r, i));
589            }
590        }
591        #[cfg(feature = "sqlite")]
592        SqlxRow::Sqlite(r) => {
593            for (i, col) in r.columns().iter().enumerate() {
594                map.insert(col.name().to_string(), decode_sqlite_aggregate_cell(r, i));
595            }
596        }
597    }
598    map
599}
600
601/// Probe a Postgres cell in width order. sqlx type-checks strictly per OID,
602/// so each probe only matches its own column type.
603#[cfg(feature = "postgres")]
604fn decode_pg_aggregate_cell(r: &sqlx::postgres::PgRow, i: usize) -> FilterValue {
605    if let Ok(Some(b)) = r.try_get::<Option<bool>, _>(i) {
606        return FilterValue::Bool(b);
607    }
608    if let Ok(Some(n)) = r.try_get::<Option<i64>, _>(i) {
609        return FilterValue::Int(n);
610    }
611    if let Ok(Some(n)) = r.try_get::<Option<i32>, _>(i) {
612        return FilterValue::Int(n as i64);
613    }
614    if let Ok(Some(n)) = r.try_get::<Option<i16>, _>(i) {
615        return FilterValue::Int(n as i64);
616    }
617    if let Ok(Some(f)) = r.try_get::<Option<f64>, _>(i) {
618        return FilterValue::Float(f);
619    }
620    if let Ok(Some(f)) = r.try_get::<Option<f32>, _>(i) {
621        return FilterValue::Float(f as f64);
622    }
623    // NUMERIC (what AVG and SUM over numeric/int8 return) has no direct
624    // string/float decode in sqlx; go through rust_decimal and render as
625    // text. The aggregate result folder parses the text back into f64 for
626    // the sum/avg accessors.
627    if let Ok(Some(d)) = r.try_get::<Option<rust_decimal::Decimal>, _>(i) {
628        return FilterValue::String(d.to_string());
629    }
630    if let Ok(Some(s)) = r.try_get::<Option<String>, _>(i) {
631        return FilterValue::String(s);
632    }
633    if let Ok(Some(j)) = r.try_get::<Option<serde_json::Value>, _>(i) {
634        return FilterValue::Json(j);
635    }
636    FilterValue::Null
637}
638
639/// Probe a MySQL cell. NOTE: integers must be probed before anything else —
640/// MySQL `bool::compatible` accepts every integer column type, so probing
641/// bool first would misclassify `COUNT(*)` (LONGLONG) as `Bool(true)`.
642#[cfg(feature = "mysql")]
643fn decode_mysql_aggregate_cell(r: &sqlx::mysql::MySqlRow, i: usize) -> FilterValue {
644    if let Ok(Some(n)) = r.try_get::<Option<i64>, _>(i) {
645        return FilterValue::Int(n);
646    }
647    if let Ok(Some(n)) = r.try_get::<Option<u64>, _>(i) {
648        // Unsigned columns; values beyond i64::MAX saturate rather than wrap.
649        return FilterValue::Int(i64::try_from(n).unwrap_or(i64::MAX));
650    }
651    if let Ok(Some(f)) = r.try_get::<Option<f64>, _>(i) {
652        return FilterValue::Float(f);
653    }
654    if let Ok(Some(f)) = r.try_get::<Option<f32>, _>(i) {
655        return FilterValue::Float(f as f64);
656    }
657    // NEWDECIMAL (SUM/AVG) — same text-render rationale as the pg NUMERIC arm.
658    if let Ok(Some(d)) = r.try_get::<Option<rust_decimal::Decimal>, _>(i) {
659        return FilterValue::String(d.to_string());
660    }
661    if let Ok(Some(s)) = r.try_get::<Option<String>, _>(i) {
662        return FilterValue::String(s);
663    }
664    if let Ok(Some(j)) = r.try_get::<Option<serde_json::Value>, _>(i) {
665        return FilterValue::Json(j);
666    }
667    FilterValue::Null
668}
669
670/// Probe a SQLite cell. NOTE: no bool probe — SQLite `bool::compatible`
671/// accepts every INTEGER value, which would misclassify `COUNT(*)` as Bool.
672#[cfg(feature = "sqlite")]
673fn decode_sqlite_aggregate_cell(r: &sqlx::sqlite::SqliteRow, i: usize) -> FilterValue {
674    if let Ok(Some(n)) = r.try_get::<Option<i64>, _>(i) {
675        return FilterValue::Int(n);
676    }
677    if let Ok(Some(f)) = r.try_get::<Option<f64>, _>(i) {
678        return FilterValue::Float(f);
679    }
680    if let Ok(Some(s)) = r.try_get::<Option<String>, _>(i) {
681        return FilterValue::String(s);
682    }
683    FilterValue::Null
684}
685
686// ==================== QueryEngine Trait Implementation ====================
687
688impl QueryEngine for SqlxEngine {
689    fn dialect(&self) -> &dyn prax_query::dialect::SqlDialect {
690        match self.backend {
691            DatabaseBackend::Postgres => &prax_query::dialect::Postgres,
692            DatabaseBackend::MySql => &prax_query::dialect::Mysql,
693            DatabaseBackend::Sqlite => &prax_query::dialect::Sqlite,
694        }
695    }
696
697    fn query_many<T: Model + prax_query::row::FromRow + Send + 'static>(
698        &self,
699        sql: &str,
700        params: Vec<FilterValue>,
701    ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
702        let sql = sql.to_string();
703        Box::pin(async move {
704            debug!(sql = %sql, "Executing query_many via QueryEngine");
705
706            let rows = self
707                .raw_query_many(&sql, &params)
708                .await
709                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
710
711            rows.iter()
712                .map(|r| {
713                    let rr = crate::row_ref::SqlxRowRef::from_sqlx(r).map_err(|e| {
714                        let msg = e.to_string();
715                        prax_query::QueryError::deserialization(msg).with_source(e)
716                    })?;
717                    T::from_row(&rr).map_err(|e| {
718                        let msg = e.to_string();
719                        prax_query::QueryError::deserialization(msg).with_source(e)
720                    })
721                })
722                .collect()
723        })
724    }
725
726    fn query_one<T: Model + prax_query::row::FromRow + Send + 'static>(
727        &self,
728        sql: &str,
729        params: Vec<FilterValue>,
730    ) -> BoxFuture<'_, QueryResult<T>> {
731        let sql = sql.to_string();
732        Box::pin(async move {
733            debug!(sql = %sql, "Executing query_one via QueryEngine");
734
735            let row = self.raw_query_one(&sql, &params).await.map_err(|e| {
736                let msg = e.to_string();
737                if msg.contains("no rows") {
738                    prax_query::QueryError::not_found(T::MODEL_NAME)
739                } else {
740                    prax_query::QueryError::database(msg)
741                }
742            })?;
743
744            let rr = crate::row_ref::SqlxRowRef::from_sqlx(&row).map_err(|e| {
745                let msg = e.to_string();
746                prax_query::QueryError::deserialization(msg).with_source(e)
747            })?;
748            T::from_row(&rr).map_err(|e| {
749                let msg = e.to_string();
750                prax_query::QueryError::deserialization(msg).with_source(e)
751            })
752        })
753    }
754
755    fn query_optional<T: Model + prax_query::row::FromRow + Send + 'static>(
756        &self,
757        sql: &str,
758        params: Vec<FilterValue>,
759    ) -> BoxFuture<'_, QueryResult<Option<T>>> {
760        let sql = sql.to_string();
761        Box::pin(async move {
762            debug!(sql = %sql, "Executing query_optional via QueryEngine");
763
764            let row = self
765                .raw_query_optional(&sql, &params)
766                .await
767                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
768
769            match row {
770                Some(r) => {
771                    let rr = crate::row_ref::SqlxRowRef::from_sqlx(&r).map_err(|e| {
772                        let msg = e.to_string();
773                        prax_query::QueryError::deserialization(msg).with_source(e)
774                    })?;
775                    T::from_row(&rr).map(Some).map_err(|e| {
776                        let msg = e.to_string();
777                        prax_query::QueryError::deserialization(msg).with_source(e)
778                    })
779                }
780                None => Ok(None),
781            }
782        })
783    }
784
785    fn execute_insert<T: Model + prax_query::row::FromRow + Send + 'static>(
786        &self,
787        sql: &str,
788        params: Vec<FilterValue>,
789    ) -> BoxFuture<'_, QueryResult<T>> {
790        let sql = sql.to_string();
791        Box::pin(async move {
792            debug!(sql = %sql, "Executing execute_insert via QueryEngine");
793
794            let row = self
795                .raw_query_one(&sql, &params)
796                .await
797                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
798
799            let rr = crate::row_ref::SqlxRowRef::from_sqlx(&row).map_err(|e| {
800                let msg = e.to_string();
801                prax_query::QueryError::deserialization(msg).with_source(e)
802            })?;
803            T::from_row(&rr).map_err(|e| {
804                let msg = e.to_string();
805                prax_query::QueryError::deserialization(msg).with_source(e)
806            })
807        })
808    }
809
810    fn execute_update<T: Model + prax_query::row::FromRow + Send + 'static>(
811        &self,
812        sql: &str,
813        params: Vec<FilterValue>,
814    ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
815        let sql = sql.to_string();
816        Box::pin(async move {
817            debug!(sql = %sql, "Executing execute_update via QueryEngine");
818
819            let rows = self
820                .raw_query_many(&sql, &params)
821                .await
822                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
823
824            rows.iter()
825                .map(|r| {
826                    let rr = crate::row_ref::SqlxRowRef::from_sqlx(r).map_err(|e| {
827                        let msg = e.to_string();
828                        prax_query::QueryError::deserialization(msg).with_source(e)
829                    })?;
830                    T::from_row(&rr).map_err(|e| {
831                        let msg = e.to_string();
832                        prax_query::QueryError::deserialization(msg).with_source(e)
833                    })
834                })
835                .collect()
836        })
837    }
838
839    fn execute_delete(
840        &self,
841        sql: &str,
842        params: Vec<FilterValue>,
843    ) -> BoxFuture<'_, QueryResult<u64>> {
844        let sql = sql.to_string();
845        Box::pin(async move {
846            debug!(sql = %sql, "Executing execute_delete via QueryEngine");
847
848            let affected = self
849                .raw_execute(&sql, &params)
850                .await
851                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
852
853            Ok(affected)
854        })
855    }
856
857    fn execute_raw(&self, sql: &str, params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
858        let sql = sql.to_string();
859        Box::pin(async move {
860            debug!(sql = %sql, "Executing execute_raw via QueryEngine");
861
862            let affected = self
863                .raw_execute(&sql, &params)
864                .await
865                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
866
867            Ok(affected)
868        })
869    }
870
871    fn count(&self, sql: &str, params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
872        let sql = sql.to_string();
873        Box::pin(async move {
874            debug!(sql = %sql, "Executing count via QueryEngine");
875
876            let row = self
877                .raw_query_one(&sql, &params)
878                .await
879                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
880
881            let count = match row {
882                #[cfg(feature = "postgres")]
883                SqlxRow::Postgres(r) => r
884                    .try_get::<i64, _>(0)
885                    .map_err(|e| prax_query::QueryError::database(e.to_string()))?
886                    as u64,
887                #[cfg(feature = "mysql")]
888                SqlxRow::MySql(r) => r
889                    .try_get::<i64, _>(0)
890                    .map_err(|e| prax_query::QueryError::database(e.to_string()))?
891                    as u64,
892                #[cfg(feature = "sqlite")]
893                SqlxRow::Sqlite(r) => r
894                    .try_get::<i64, _>(0)
895                    .map_err(|e| prax_query::QueryError::database(e.to_string()))?
896                    as u64,
897            };
898
899            Ok(count)
900        })
901    }
902
903    fn aggregate_query(
904        &self,
905        sql: &str,
906        params: Vec<FilterValue>,
907    ) -> BoxFuture<'_, QueryResult<Vec<std::collections::HashMap<String, FilterValue>>>> {
908        let sql = sql.to_string();
909        Box::pin(async move {
910            debug!(sql = %sql, "Executing aggregate_query via QueryEngine");
911
912            let rows = self
913                .raw_query_many(&sql, &params)
914                .await
915                .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
916
917            Ok(rows.iter().map(decode_aggregate_row).collect())
918        })
919    }
920
921    fn in_transaction(&self) -> bool {
922        self.tx.is_some()
923    }
924
925    fn transaction<'a, R, Fut, F>(&'a self, f: F) -> BoxFuture<'a, QueryResult<R>>
926    where
927        F: FnOnce(Self) -> Fut + Send + 'a,
928        Fut: std::future::Future<Output = QueryResult<R>> + Send + 'a,
929        R: Send + 'a,
930        Self: Clone,
931    {
932        Box::pin(async move {
933            // Refuse nested transactions: a tx-bound engine can only hold one
934            // open transaction at a time. Issue SAVEPOINT via execute_raw if
935            // nesting is needed.
936            if self.tx.is_some() {
937                return Err(prax_query::QueryError::internal(
938                    "nested transactions not supported on SqlxEngine \
939                     (call .transaction() on the outer engine only, or \
940                     issue SAVEPOINT via execute_raw)",
941                ));
942            }
943
944            // Begin a backend-native transaction. `Pool::begin()` yields a
945            // `Transaction<'static>` owning its pooled connection, so it can
946            // be shared with the tx-bound engine clone behind an Arc.
947            let tx_handle = match &*self.pool {
948                #[cfg(feature = "postgres")]
949                SqlxPool::Postgres(pool) => {
950                    let tx = pool
951                        .begin()
952                        .await
953                        .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
954                    TxHandle::Postgres(Arc::new(futures::lock::Mutex::new(Some(tx))))
955                }
956                #[cfg(feature = "mysql")]
957                SqlxPool::MySql(pool) => {
958                    let tx = pool
959                        .begin()
960                        .await
961                        .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
962                    TxHandle::MySql(Arc::new(futures::lock::Mutex::new(Some(tx))))
963                }
964                #[cfg(feature = "sqlite")]
965                SqlxPool::Sqlite(pool) => {
966                    let tx = pool
967                        .begin()
968                        .await
969                        .map_err(|e| prax_query::QueryError::database(e.to_string()))?;
970                    TxHandle::Sqlite(Arc::new(futures::lock::Mutex::new(Some(tx))))
971                }
972            };
973
974            let tx_engine = SqlxEngine {
975                pool: self.pool.clone(),
976                backend: self.backend,
977                tx: Some(tx_handle.clone()),
978            };
979
980            // Run the caller's closure on the tx-bound engine clone. When the
981            // future resolves, its engine clone is dropped; any clones the
982            // closure stashed away keep pointing at the (soon to be emptied)
983            // slot and will fail loudly on their next query.
984            let result = f(tx_engine).await;
985
986            // Finalize: COMMIT on success, best-effort ROLLBACK on failure.
987            match &tx_handle {
988                #[cfg(feature = "postgres")]
989                TxHandle::Postgres(slot) => finalize_tx(slot, result).await,
990                #[cfg(feature = "mysql")]
991                TxHandle::MySql(slot) => finalize_tx(slot, result).await,
992                #[cfg(feature = "sqlite")]
993                TxHandle::Sqlite(slot) => finalize_tx(slot, result).await,
994            }
995        })
996    }
997}
998
999/// SQLx routes exclusively to SQL backends (Postgres, MySQL, SQLite), all of
1000/// which support scalar subqueries in SELECT. Implementing this marker here
1001/// lets callers use [`FindManyOperation::with_scalar_projection`] and its
1002/// siblings on `SqlxEngine` just like the individual SQL engine crates.
1003impl prax_query::capabilities::SupportsScalarSubqueryInSelect for SqlxEngine {}
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::*;
1008    use crate::types::placeholder;
1009
1010    #[test]
1011    fn test_placeholder_generation() {
1012        assert_eq!(placeholder(DatabaseBackend::Postgres, 1), "$1");
1013        assert_eq!(placeholder(DatabaseBackend::MySql, 1), "?");
1014        assert_eq!(placeholder(DatabaseBackend::Sqlite, 1), "?");
1015    }
1016
1017    #[test]
1018    fn test_quote_identifier() {
1019        assert_eq!(
1020            quote_identifier(DatabaseBackend::Postgres, "users"),
1021            "\"users\""
1022        );
1023        assert_eq!(quote_identifier(DatabaseBackend::MySql, "users"), "`users`");
1024    }
1025
1026    #[cfg(feature = "postgres")]
1027    #[test]
1028    fn test_untyped_null_binds_as_server_inferred_type() {
1029        use sqlx::TypeInfo as _;
1030
1031        // The parameter is declared as the `unknown` pseudo-type with no
1032        // pre-resolved OID; sqlx resolves (and caches) OID 705 per connection,
1033        // and the server then infers the parameter type from context, so a
1034        // NULL binds cleanly against a column of any type.
1035        let info = <UntypedNull as sqlx::Type<sqlx::Postgres>>::type_info();
1036        assert_eq!(info.name(), "unknown");
1037        assert!(info.oid().is_none());
1038
1039        let mut buf = sqlx::postgres::PgArgumentBuffer::default();
1040        let is_null = <UntypedNull as sqlx::Encode<'_, sqlx::Postgres>>::encode_by_ref(
1041            &UntypedNull,
1042            &mut buf,
1043        )
1044        .unwrap();
1045        assert!(is_null.is_null());
1046    }
1047
1048    /// Build an engine backed by a single-connection in-memory SQLite pool.
1049    /// `max_connections(1)` keeps every statement on the same connection,
1050    /// so the in-memory database (and its schema) survives across calls.
1051    #[cfg(feature = "sqlite")]
1052    async fn sqlite_engine() -> SqlxEngine {
1053        let pool = sqlx::sqlite::SqlitePoolOptions::new()
1054            .max_connections(1)
1055            .connect("sqlite::memory:")
1056            .await
1057            .expect("connect to in-memory sqlite");
1058        SqlxEngine::from_pool(SqlxPool::Sqlite(pool))
1059    }
1060
1061    #[cfg(feature = "sqlite")]
1062    #[tokio::test]
1063    async fn transaction_commits_on_success() {
1064        let engine = sqlite_engine().await;
1065        engine
1066            .raw_execute(
1067                "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
1068                &[],
1069            )
1070            .await
1071            .unwrap();
1072
1073        let result: prax_query::QueryResult<()> = engine
1074            .transaction(|tx| async move {
1075                tx.raw_execute(
1076                    "INSERT INTO items (name) VALUES (?1)",
1077                    &[FilterValue::String("widget".into())],
1078                )
1079                .await?;
1080                Ok(())
1081            })
1082            .await;
1083        result.expect("transaction should commit");
1084
1085        // The insert is visible outside the transaction: it committed.
1086        assert_eq!(engine.count_table("items", None).await.unwrap(), 1);
1087    }
1088
1089    #[cfg(feature = "sqlite")]
1090    #[tokio::test]
1091    async fn transaction_rolls_back_on_error() {
1092        let engine = sqlite_engine().await;
1093        engine
1094            .raw_execute(
1095                "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
1096                &[],
1097            )
1098            .await
1099            .unwrap();
1100
1101        let result: prax_query::QueryResult<()> = engine
1102            .transaction(|tx| async move {
1103                tx.raw_execute(
1104                    "INSERT INTO items (name) VALUES (?1)",
1105                    &[FilterValue::String("widget".into())],
1106                )
1107                .await?;
1108                Err(prax_query::QueryError::internal("boom"))
1109            })
1110            .await;
1111        assert!(result.is_err());
1112
1113        // The insert was rolled back: the table is still empty.
1114        assert_eq!(engine.count_table("items", None).await.unwrap(), 0);
1115    }
1116
1117    #[cfg(feature = "sqlite")]
1118    #[tokio::test]
1119    async fn stashed_engine_clone_fails_after_finalize() {
1120        let engine = sqlite_engine().await;
1121
1122        // Stash the tx-bound engine by returning it from the closure; the
1123        // finalizer commits and empties the shared slot before handing it
1124        // back, so it must fail loudly on its next query.
1125        let stashed: SqlxEngine = engine
1126            .transaction(|tx| async move { Ok(tx) })
1127            .await
1128            .expect("transaction should commit");
1129
1130        assert!(stashed.in_transaction());
1131        let err = stashed
1132            .raw_query_one("SELECT 1", &[])
1133            .await
1134            .err()
1135            .expect("queries on a finalized transaction must fail");
1136        assert!(
1137            matches!(
1138                err,
1139                SqlxError::Internal(ref msg) if msg.contains("committed or rolled back")
1140            ),
1141            "unexpected error: {err}"
1142        );
1143    }
1144
1145    #[cfg(feature = "sqlite")]
1146    #[tokio::test]
1147    async fn nested_transaction_is_rejected() {
1148        let engine = sqlite_engine().await;
1149
1150        let result: prax_query::QueryResult<()> = engine
1151            .transaction(|tx| async move { tx.transaction(|_inner| async move { Ok(()) }).await })
1152            .await;
1153        let err = result.expect_err("nested transaction must be rejected");
1154        assert!(
1155            err.to_string().contains("nested transactions"),
1156            "unexpected error: {err}"
1157        );
1158    }
1159
1160    #[cfg(feature = "sqlite")]
1161    #[tokio::test]
1162    async fn aggregate_query_decodes_count_as_int_not_bool() {
1163        let engine = sqlite_engine().await;
1164        engine
1165            .raw_execute(
1166                "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
1167                &[],
1168            )
1169            .await
1170            .unwrap();
1171        for name in ["a", "b", "c"] {
1172            engine
1173                .raw_execute(
1174                    "INSERT INTO items (name) VALUES (?1)",
1175                    &[FilterValue::String(name.into())],
1176                )
1177                .await
1178                .unwrap();
1179        }
1180
1181        let rows = engine
1182            .aggregate_query("SELECT COUNT(*) AS n, AVG(id) AS avg_id FROM items", vec![])
1183            .await
1184            .expect("aggregate query");
1185        assert_eq!(rows.len(), 1);
1186        let row = &rows[0];
1187        // Pins the probe order: SQLite `bool::compatible` accepts every
1188        // INTEGER value, so a bool probe would misclassify COUNT(*) as
1189        // Bool. Probing i64 first (with no bool probe at all) keeps it Int.
1190        assert_eq!(row.get("n"), Some(&FilterValue::Int(3)));
1191        assert!(!matches!(row.get("n"), Some(FilterValue::Bool(_))));
1192        assert_eq!(row.get("avg_id"), Some(&FilterValue::Float(2.0)));
1193    }
1194
1195    #[cfg(feature = "sqlite")]
1196    #[test]
1197    fn bind_sqlite_param_rejects_list_values() {
1198        // Offline: sqlx::query only builds the statement, no connection is
1199        // needed to hit the List-rejection arm.
1200        let value = FilterValue::List(vec![FilterValue::Int(1)]);
1201        let query = sqlx::query::<sqlx::Sqlite>("SELECT ?1");
1202        let err = bind_sqlite_param(query, &value)
1203            .err()
1204            .expect("list bind must be rejected");
1205        assert!(
1206            matches!(err, SqlxError::TypeConversion(ref msg) if msg.contains("list values")),
1207            "unexpected error: {err}"
1208        );
1209    }
1210}