1use 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#[derive(Clone)]
41pub struct SqlxEngine {
42 pool: Arc<SqlxPool>,
43 backend: DatabaseBackend,
44 tx: Option<TxHandle>,
47}
48
49#[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
70type TxSlot<DB> = Arc<futures::lock::Mutex<Option<sqlx::Transaction<'static, DB>>>>;
72
73type TxGuard<'m, DB> = futures::lock::MutexGuard<'m, Option<sqlx::Transaction<'static, DB>>>;
75
76const TX_FINALIZED: &str = "transaction has already been committed or rolled back";
79
80impl SqlxEngine {
81 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 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 pub fn backend(&self) -> DatabaseBackend {
104 self.backend
105 }
106
107 pub fn pool(&self) -> &SqlxPool {
109 &self.pool
110 }
111
112 pub async fn close(&self) {
114 self.pool.close().await;
115 }
116
117 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 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 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 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 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 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 #[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 #[allow(unreachable_patterns)]
399 Some(_) => Err(SqlxError::Internal(
400 "transaction handle backend does not match engine pool".into(),
401 )),
402 }
403 }
404
405 #[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 #[allow(unreachable_patterns)]
413 Some(_) => Err(SqlxError::Internal(
414 "transaction handle backend does not match engine pool".into(),
415 )),
416 }
417 }
418
419 #[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 #[allow(unreachable_patterns)]
427 Some(_) => Err(SqlxError::Internal(
428 "transaction handle backend does not match engine pool".into(),
429 )),
430 }
431 }
432}
433
434#[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 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
532async 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
566fn 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#[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 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#[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 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 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#[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
686impl 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, ¶ms)
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, ¶ms).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, ¶ms)
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, ¶ms)
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, ¶ms)
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, ¶ms)
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, ¶ms)
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, ¶ms)
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, ¶ms)
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 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 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 let result = f(tx_engine).await;
985
986 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
999impl 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 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 #[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 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 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 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 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 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}