1use sqlx::postgres::{PgPool, PgPoolOptions, PgRow};
16use tokio::sync::Mutex;
17
18use crate::error::FlozError;
19use crate::value::Value;
20
21#[derive(Debug, Clone)]
30pub struct Db {
31 pool: PgPool,
32}
33
34impl Db {
35 pub async fn connect(url: &str) -> Result<Self, FlozError> {
41 let pool = PgPoolOptions::new()
42 .max_connections(10)
43 .connect(url)
44 .await?;
45 Ok(Self { pool })
46 }
47
48 pub async fn connect_env(env_var: &str, default_url: &str) -> Result<Self, FlozError> {
54 let url = std::env::var(env_var).unwrap_or_else(|_| default_url.to_string());
55 Self::connect(&url).await
56 }
57
58 pub fn from_pool(pool: PgPool) -> Self {
62 Self { pool }
63 }
64
65 pub fn pool(&self) -> &PgPool {
67 &self.pool
68 }
69
70 pub async fn begin(&self) -> Result<Tx, FlozError> {
72 let mut conn = self.pool.acquire().await?;
73 sqlx::query("BEGIN").execute(&mut *conn).await?;
74 Ok(Tx {
75 conn: Mutex::new(conn),
76 committed: false,
77 })
78 }
79}
80
81pub struct Tx {
92 conn: Mutex<sqlx::pool::PoolConnection<sqlx::Postgres>>,
93 committed: bool,
94}
95
96impl Tx {
97 pub async fn commit(mut self) -> Result<(), FlozError> {
99 let mut conn = self.conn.lock().await;
100 sqlx::query("COMMIT").execute(&mut **conn).await?;
101 self.committed = true;
102 Ok(())
103 }
104
105 pub async fn rollback(self) -> Result<(), FlozError> {
107 let mut conn = self.conn.lock().await;
108 sqlx::query("ROLLBACK").execute(&mut **conn).await?;
109 Ok(())
110 }
111}
112
113impl std::fmt::Debug for Tx {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("Tx")
117 .field("committed", &self.committed)
118 .finish()
119 }
120}
121
122pub trait Executor {
137 fn execute_raw(
139 &self,
140 sql: &str,
141 params: Vec<Value>,
142 ) -> impl std::future::Future<Output = Result<u64, FlozError>> + Send;
143
144 fn fetch_all<T>(
146 &self,
147 sql: &str,
148 params: Vec<Value>,
149 ) -> impl std::future::Future<Output = Result<Vec<T>, FlozError>> + Send
150 where
151 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>;
152
153 fn fetch_one<T>(
155 &self,
156 sql: &str,
157 params: Vec<Value>,
158 ) -> impl std::future::Future<Output = Result<T, FlozError>> + Send
159 where
160 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>;
161
162 fn fetch_optional<T>(
164 &self,
165 sql: &str,
166 params: Vec<Value>,
167 ) -> impl std::future::Future<Output = Result<Option<T>, FlozError>> + Send
168 where
169 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>;
170}
171
172macro_rules! bind_params {
181 ($query:expr, $params:expr) => {{
182 let mut q = $query;
183 for param in $params {
184 q = match param {
185 Value::Short(v) => q.bind(v),
187 Value::Int(v) => q.bind(v),
188 Value::BigInt(v) => q.bind(v),
189 Value::Real(v) => q.bind(v),
190 Value::Double(v) => q.bind(v),
191 Value::Bool(v) => q.bind(v),
192 Value::String(v) => q.bind(v),
193 Value::Bytes(v) => q.bind(v),
194 Value::Uuid(v) => q.bind(v),
195 Value::DateTime(v) => q.bind(v),
196 Value::NaiveDateTime(v) => q.bind(v),
197 Value::NaiveDate(v) => q.bind(v),
198 Value::NaiveTime(v) => q.bind(v),
199 Value::Json(v) => q.bind(v),
200 Value::Jsonb(v) => q.bind(v),
201 Value::OptionShort(v) => q.bind(v),
203 Value::OptionInt(v) => q.bind(v),
204 Value::OptionBigInt(v) => q.bind(v),
205 Value::OptionReal(v) => q.bind(v),
206 Value::OptionDouble(v) => q.bind(v),
207 Value::OptionBool(v) => q.bind(v),
208 Value::OptionString(v) => q.bind(v),
209 Value::OptionBytes(v) => q.bind(v),
210 Value::OptionUuid(v) => q.bind(v),
211 Value::OptionDateTime(v) => q.bind(v),
212 Value::OptionNaiveDateTime(v) => q.bind(v),
213 Value::OptionNaiveDate(v) => q.bind(v),
214 Value::OptionNaiveTime(v) => q.bind(v),
215 Value::OptionJson(v) => q.bind(v),
216 Value::OptionJsonb(v) => q.bind(v),
217 };
218 }
219 q
220 }};
221}
222
223impl Executor for Db {
228 async fn execute_raw(&self, sql: &str, params: Vec<Value>) -> Result<u64, FlozError> {
229 let query = bind_params!(sqlx::query(sql), params);
230 let result = query.execute(&self.pool).await?;
231 Ok(result.rows_affected())
232 }
233
234 async fn fetch_all<T>(&self, sql: &str, params: Vec<Value>) -> Result<Vec<T>, FlozError>
235 where
236 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
237 {
238 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
239 let rows = query.fetch_all(&self.pool).await?;
240 Ok(rows)
241 }
242
243 async fn fetch_one<T>(&self, sql: &str, params: Vec<Value>) -> Result<T, FlozError>
244 where
245 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
246 {
247 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
248 let row = query
249 .fetch_optional(&self.pool)
250 .await?
251 .ok_or(FlozError::NotFound)?;
252 Ok(row)
253 }
254
255 async fn fetch_optional<T>(&self, sql: &str, params: Vec<Value>) -> Result<Option<T>, FlozError>
256 where
257 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
258 {
259 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
260 let row = query.fetch_optional(&self.pool).await?;
261 Ok(row)
262 }
263}
264
265impl Executor for Tx {
270 async fn execute_raw(&self, sql: &str, params: Vec<Value>) -> Result<u64, FlozError> {
271 let mut conn = self.conn.lock().await;
272 let query = bind_params!(sqlx::query(sql), params);
273 let result = query.execute(&mut **conn).await?;
274 Ok(result.rows_affected())
275 }
276
277 async fn fetch_all<T>(&self, sql: &str, params: Vec<Value>) -> Result<Vec<T>, FlozError>
278 where
279 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
280 {
281 let mut conn = self.conn.lock().await;
282 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
283 let rows = query.fetch_all(&mut **conn).await?;
284 Ok(rows)
285 }
286
287 async fn fetch_one<T>(&self, sql: &str, params: Vec<Value>) -> Result<T, FlozError>
288 where
289 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
290 {
291 let mut conn = self.conn.lock().await;
292 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
293 let row = query
294 .fetch_optional(&mut **conn)
295 .await?
296 .ok_or(FlozError::NotFound)?;
297 Ok(row)
298 }
299
300 async fn fetch_optional<T>(&self, sql: &str, params: Vec<Value>) -> Result<Option<T>, FlozError>
301 where
302 T: Send + Unpin + for<'r> sqlx::FromRow<'r, PgRow>,
303 {
304 let mut conn = self.conn.lock().await;
305 let query = bind_params!(sqlx::query_as::<_, T>(sql), params);
306 let row = query.fetch_optional(&mut **conn).await?;
307 Ok(row)
308 }
309}
310
311#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
322 fn db_from_pool_type_check() {
323 fn _assert_send<T: Send>() {}
325 fn _assert_sync<T: Sync>() {}
326 _assert_send::<Db>();
327 _assert_sync::<Db>();
328 }
329
330 #[test]
331 fn tx_is_debug() {
332 fn _assert_debug<T: std::fmt::Debug>() {}
334 _assert_debug::<Tx>();
335 }
336
337 fn test_url() -> Option<String> {
341 std::env::var("DATABASE_URL").ok()
342 }
343
344 #[tokio::test]
345 async fn connect_to_postgres() {
346 let Some(url) = test_url() else { return };
347 let db = Db::connect(&url).await.unwrap();
348 assert!(db.pool().size() > 0 || true); }
350
351 #[tokio::test]
352 async fn execute_raw_select() {
353 let Some(url) = test_url() else { return };
354 let db = Db::connect(&url).await.unwrap();
355
356 let affected = db.execute_raw("SELECT 1", vec![]).await.unwrap();
358 assert_eq!(affected, 0);
360 }
361
362 #[tokio::test]
363 async fn execute_with_params() {
364 let Some(url) = test_url() else { return };
365 let db = Db::connect(&url).await.unwrap();
366
367 db.execute_raw("CREATE TEMP TABLE xql_test_params (id INT, name TEXT)", vec![])
369 .await
370 .unwrap();
371
372 let affected = db
373 .execute_raw(
374 "INSERT INTO xql_test_params (id, name) VALUES ($1, $2)",
375 vec![Value::Int(1), Value::String("Alice".into())],
376 )
377 .await
378 .unwrap();
379 assert_eq!(affected, 1);
380 }
381
382 #[tokio::test]
383 async fn fetch_all_rows() {
384 let Some(url) = test_url() else { return };
385 let db = Db::connect(&url).await.unwrap();
386
387 db.execute_raw("CREATE TEMP TABLE xql_test_fetch (id INT, name TEXT)", vec![])
388 .await
389 .unwrap();
390 db.execute_raw(
391 "INSERT INTO xql_test_fetch VALUES (1, 'Alice'), (2, 'Bob')",
392 vec![],
393 )
394 .await
395 .unwrap();
396
397 #[derive(Debug, sqlx::FromRow)]
398 struct Row {
399 id: i32,
400 name: String,
401 }
402
403 let rows: Vec<Row> = db
404 .fetch_all("SELECT id, name FROM xql_test_fetch ORDER BY id", vec![])
405 .await
406 .unwrap();
407 assert_eq!(rows.len(), 2);
408 assert_eq!(rows[0].name, "Alice");
409 assert_eq!(rows[1].name, "Bob");
410 }
411
412 #[tokio::test]
413 async fn fetch_one_found() {
414 let Some(url) = test_url() else { return };
415 let db = Db::connect(&url).await.unwrap();
416
417 db.execute_raw("CREATE TEMP TABLE xql_test_one (id INT, name TEXT)", vec![])
418 .await
419 .unwrap();
420 db.execute_raw("INSERT INTO xql_test_one VALUES (1, 'Alice')", vec![])
421 .await
422 .unwrap();
423
424 #[derive(Debug, sqlx::FromRow)]
425 struct Row {
426 id: i32,
427 name: String,
428 }
429
430 let row: Row = db
431 .fetch_one(
432 "SELECT id, name FROM xql_test_one WHERE id = $1",
433 vec![Value::Int(1)],
434 )
435 .await
436 .unwrap();
437 assert_eq!(row.name, "Alice");
438 }
439
440 #[tokio::test]
441 async fn fetch_one_not_found() {
442 let Some(url) = test_url() else { return };
443 let db = Db::connect(&url).await.unwrap();
444
445 db.execute_raw("CREATE TEMP TABLE xql_test_nf (id INT)", vec![])
446 .await
447 .unwrap();
448
449 #[derive(Debug, sqlx::FromRow)]
450 struct Row {
451 id: i32,
452 }
453
454 let result: Result<Row, _> = db
455 .fetch_one(
456 "SELECT id FROM xql_test_nf WHERE id = $1",
457 vec![Value::Int(999)],
458 )
459 .await;
460 assert!(matches!(result, Err(FlozError::NotFound)));
461 }
462
463 #[tokio::test]
464 async fn fetch_optional_some() {
465 let Some(url) = test_url() else { return };
466 let db = Db::connect(&url).await.unwrap();
467
468 db.execute_raw("CREATE TEMP TABLE xql_test_opt (id INT)", vec![])
469 .await
470 .unwrap();
471 db.execute_raw("INSERT INTO xql_test_opt VALUES (42)", vec![])
472 .await
473 .unwrap();
474
475 #[derive(Debug, sqlx::FromRow)]
476 struct Row {
477 id: i32,
478 }
479
480 let row: Option<Row> = db
481 .fetch_optional("SELECT id FROM xql_test_opt WHERE id = $1", vec![Value::Int(42)])
482 .await
483 .unwrap();
484 assert!(row.is_some());
485 assert_eq!(row.unwrap().id, 42);
486 }
487
488 #[tokio::test]
489 async fn fetch_optional_none() {
490 let Some(url) = test_url() else { return };
491 let db = Db::connect(&url).await.unwrap();
492
493 db.execute_raw("CREATE TEMP TABLE xql_test_opt_n (id INT)", vec![])
494 .await
495 .unwrap();
496
497 #[derive(Debug, sqlx::FromRow)]
498 struct Row {
499 id: i32,
500 }
501
502 let row: Option<Row> = db
503 .fetch_optional(
504 "SELECT id FROM xql_test_opt_n WHERE id = $1",
505 vec![Value::Int(999)],
506 )
507 .await
508 .unwrap();
509 assert!(row.is_none());
510 }
511
512 #[tokio::test]
515 async fn transaction_commit() {
516 let Some(url) = test_url() else { return };
517 let db = Db::connect(&url).await.unwrap();
518
519 db.execute_raw("CREATE TEMP TABLE xql_test_tx (id INT)", vec![])
520 .await
521 .unwrap();
522
523 let tx = db.begin().await.unwrap();
524 tx.execute_raw(
525 "INSERT INTO xql_test_tx VALUES ($1)",
526 vec![Value::Int(1)],
527 )
528 .await
529 .unwrap();
530 tx.commit().await.unwrap();
531
532 #[derive(Debug, sqlx::FromRow)]
534 struct Row { id: i32 }
535
536 let rows: Vec<Row> = db
537 .fetch_all("SELECT id FROM xql_test_tx", vec![])
538 .await
539 .unwrap();
540 assert_eq!(rows.len(), 1);
541 }
542
543 #[tokio::test]
544 async fn transaction_rollback_on_drop() {
545 let Some(url) = test_url() else { return };
546 let db = Db::connect(&url).await.unwrap();
547
548 db.execute_raw("CREATE TABLE IF NOT EXISTS xql_test_txd (id INT)", vec![])
549 .await
550 .unwrap();
551 db.execute_raw("DELETE FROM xql_test_txd", vec![])
553 .await
554 .unwrap();
555
556 {
557 let tx = db.begin().await.unwrap();
558 tx.execute_raw(
559 "INSERT INTO xql_test_txd VALUES ($1)",
560 vec![Value::Int(99)],
561 )
562 .await
563 .unwrap();
564 }
566
567 #[derive(Debug, sqlx::FromRow)]
569 struct Row { id: i32 }
570
571 let rows: Vec<Row> = db
572 .fetch_all("SELECT id FROM xql_test_txd WHERE id = 99", vec![])
573 .await
574 .unwrap();
575 assert_eq!(rows.len(), 0, "Row should not exist after rollback-on-drop");
576
577 db.execute_raw("DROP TABLE IF EXISTS xql_test_txd", vec![])
579 .await
580 .unwrap();
581 }
582
583 #[tokio::test]
584 async fn transaction_explicit_rollback() {
585 let Some(url) = test_url() else { return };
586 let db = Db::connect(&url).await.unwrap();
587
588 db.execute_raw("CREATE TEMP TABLE xql_test_rb (id INT)", vec![])
589 .await
590 .unwrap();
591
592 let tx = db.begin().await.unwrap();
593 tx.execute_raw(
594 "INSERT INTO xql_test_rb VALUES ($1)",
595 vec![Value::Int(1)],
596 )
597 .await
598 .unwrap();
599 tx.rollback().await.unwrap();
600
601 #[derive(Debug, sqlx::FromRow)]
602 struct Row { id: i32 }
603
604 let rows: Vec<Row> = db
605 .fetch_all("SELECT id FROM xql_test_rb", vec![])
606 .await
607 .unwrap();
608 assert_eq!(rows.len(), 0);
609 }
610
611 #[tokio::test]
612 async fn transaction_fetch() {
613 let Some(url) = test_url() else { return };
614 let db = Db::connect(&url).await.unwrap();
615
616 db.execute_raw("CREATE TEMP TABLE xql_test_txf (id INT, name TEXT)", vec![])
617 .await
618 .unwrap();
619
620 let tx = db.begin().await.unwrap();
621 tx.execute_raw(
622 "INSERT INTO xql_test_txf VALUES (1, 'Inside Tx')",
623 vec![],
624 )
625 .await
626 .unwrap();
627
628 #[derive(Debug, sqlx::FromRow)]
630 struct Row { id: i32, name: String }
631
632 let rows: Vec<Row> = tx
633 .fetch_all("SELECT id, name FROM xql_test_txf", vec![])
634 .await
635 .unwrap();
636 assert_eq!(rows.len(), 1);
637 assert_eq!(rows[0].name, "Inside Tx");
638
639 tx.rollback().await.unwrap();
640 }
641
642 #[tokio::test]
645 async fn bind_typed_null() {
646 let Some(url) = test_url() else { return };
647 let db = Db::connect(&url).await.unwrap();
648
649 db.execute_raw(
650 "CREATE TEMP TABLE xql_test_null (id INT, name TEXT)",
651 vec![],
652 )
653 .await
654 .unwrap();
655
656 let affected = db
659 .execute_raw(
660 "INSERT INTO xql_test_null (id, name) VALUES ($1, $2)",
661 vec![Value::Int(1), Value::OptionString(None)],
662 )
663 .await
664 .unwrap();
665 assert_eq!(affected, 1);
666
667 #[derive(Debug, sqlx::FromRow)]
668 struct Row {
669 id: i32,
670 name: Option<String>,
671 }
672
673 let row: Row = db
674 .fetch_one("SELECT id, name FROM xql_test_null WHERE id = 1", vec![])
675 .await
676 .unwrap();
677 assert_eq!(row.id, 1);
678 assert!(row.name.is_none());
679 }
680}