1use crate::error::Result;
14use crate::row::Row;
15use crate::value::Value;
16use asupersync::{Cx, Outcome};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum IsolationLevel {
24 ReadUncommitted,
28
29 #[default]
33 ReadCommitted,
34
35 RepeatableRead,
39
40 Serializable,
44}
45
46impl IsolationLevel {
47 #[must_use]
49 pub const fn as_sql(&self) -> &'static str {
50 match self {
51 IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
52 IsolationLevel::ReadCommitted => "READ COMMITTED",
53 IsolationLevel::RepeatableRead => "REPEATABLE READ",
54 IsolationLevel::Serializable => "SERIALIZABLE",
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
65pub struct PreparedStatement {
66 id: u64,
68 sql: String,
70 param_count: usize,
72 columns: Option<Vec<String>>,
74}
75
76impl PreparedStatement {
77 #[must_use]
81 pub fn new(id: u64, sql: String, param_count: usize) -> Self {
82 Self {
83 id,
84 sql,
85 param_count,
86 columns: None,
87 }
88 }
89
90 #[must_use]
92 pub fn with_columns(id: u64, sql: String, param_count: usize, columns: Vec<String>) -> Self {
93 Self {
94 id,
95 sql,
96 param_count,
97 columns: Some(columns),
98 }
99 }
100
101 #[must_use]
103 pub const fn id(&self) -> u64 {
104 self.id
105 }
106
107 #[must_use]
109 pub fn sql(&self) -> &str {
110 &self.sql
111 }
112
113 #[must_use]
115 pub const fn param_count(&self) -> usize {
116 self.param_count
117 }
118
119 #[must_use]
121 pub fn columns(&self) -> Option<&[String]> {
122 self.columns.as_deref()
123 }
124
125 #[must_use]
127 pub fn validate_params(&self, params: &[Value]) -> bool {
128 params.len() == self.param_count
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
155pub enum Dialect {
156 #[default]
158 Postgres,
159 Sqlite,
161 Mysql,
163}
164
165impl Dialect {
166 pub fn placeholder(self, index: usize) -> String {
168 match self {
169 Dialect::Postgres => format!("${index}"),
170 Dialect::Sqlite => format!("?{index}"),
171 Dialect::Mysql => "?".to_string(),
172 }
173 }
174
175 pub const fn concat_op(self) -> &'static str {
177 match self {
178 Dialect::Postgres | Dialect::Sqlite => "||",
179 Dialect::Mysql => "", }
181 }
182
183 pub const fn supports_ilike(self) -> bool {
185 matches!(self, Dialect::Postgres)
186 }
187
188 pub fn quote_identifier(self, name: &str) -> String {
194 match self {
195 Dialect::Postgres | Dialect::Sqlite => {
196 let escaped = name.replace('"', "\"\"");
197 format!("\"{escaped}\"")
198 }
199 Dialect::Mysql => {
200 let escaped = name.replace('`', "``");
201 format!("`{escaped}`")
202 }
203 }
204 }
205}
206
207pub trait Connection: Send + Sync {
208 type Tx<'conn>: TransactionOps
210 where
211 Self: 'conn;
212
213 fn dialect(&self) -> Dialect {
218 Dialect::Postgres
219 }
220
221 fn query(
223 &self,
224 cx: &Cx,
225 sql: &str,
226 params: &[Value],
227 ) -> impl Future<Output = Outcome<Vec<Row>, crate::Error>> + Send;
228
229 fn query_one(
231 &self,
232 cx: &Cx,
233 sql: &str,
234 params: &[Value],
235 ) -> impl Future<Output = Outcome<Option<Row>, crate::Error>> + Send;
236
237 fn execute(
239 &self,
240 cx: &Cx,
241 sql: &str,
242 params: &[Value],
243 ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send;
244
245 fn insert(
250 &self,
251 cx: &Cx,
252 sql: &str,
253 params: &[Value],
254 ) -> impl Future<Output = Outcome<i64, crate::Error>> + Send;
255
256 fn batch(
262 &self,
263 cx: &Cx,
264 statements: &[(String, Vec<Value>)],
265 ) -> impl Future<Output = Outcome<Vec<u64>, crate::Error>> + Send;
266
267 fn begin(&self, cx: &Cx) -> impl Future<Output = Outcome<Self::Tx<'_>, crate::Error>> + Send;
269
270 fn begin_with(
272 &self,
273 cx: &Cx,
274 isolation: IsolationLevel,
275 ) -> impl Future<Output = Outcome<Self::Tx<'_>, crate::Error>> + Send;
276
277 fn prepare(
282 &self,
283 cx: &Cx,
284 sql: &str,
285 ) -> impl Future<Output = Outcome<PreparedStatement, crate::Error>> + Send;
286
287 fn query_prepared(
289 &self,
290 cx: &Cx,
291 stmt: &PreparedStatement,
292 params: &[Value],
293 ) -> impl Future<Output = Outcome<Vec<Row>, crate::Error>> + Send;
294
295 fn execute_prepared(
297 &self,
298 cx: &Cx,
299 stmt: &PreparedStatement,
300 params: &[Value],
301 ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send;
302
303 fn ping(&self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
305
306 fn is_valid(&self, cx: &Cx) -> impl Future<Output = bool> + Send {
308 async {
309 match self.ping(cx).await {
310 Outcome::Ok(()) => true,
311 Outcome::Err(_) | Outcome::Cancelled(_) | Outcome::Panicked(_) => false,
312 }
313 }
314 }
315
316 fn close(self, cx: &Cx) -> impl Future<Output = Result<()>> + Send;
318
319 fn close_for_pool(self, cx: &Cx) -> impl Future<Output = Result<()>> + Send
334 where
335 Self: Sized,
336 {
337 self.close(cx)
338 }
339}
340
341pub trait TransactionOps: Send {
360 fn query(
362 &self,
363 cx: &Cx,
364 sql: &str,
365 params: &[Value],
366 ) -> impl Future<Output = Outcome<Vec<Row>, crate::Error>> + Send;
367
368 fn query_one(
370 &self,
371 cx: &Cx,
372 sql: &str,
373 params: &[Value],
374 ) -> impl Future<Output = Outcome<Option<Row>, crate::Error>> + Send;
375
376 fn execute(
378 &self,
379 cx: &Cx,
380 sql: &str,
381 params: &[Value],
382 ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send;
383
384 fn savepoint(
388 &self,
389 cx: &Cx,
390 name: &str,
391 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
392
393 fn rollback_to(
398 &self,
399 cx: &Cx,
400 name: &str,
401 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
402
403 fn release(
408 &self,
409 cx: &Cx,
410 name: &str,
411 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
412
413 fn commit(self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
415
416 fn rollback(self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
418}
419
420pub struct Transaction<'conn> {
429 conn: &'conn dyn TransactionInternal,
431 finalized: bool,
433}
434
435pub trait TransactionInternal: Send + Sync {
440 fn query_internal(
442 &self,
443 cx: &Cx,
444 sql: &str,
445 params: &[Value],
446 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<Vec<Row>, crate::Error>> + Send + '_>>;
447
448 fn query_one_internal(
450 &self,
451 cx: &Cx,
452 sql: &str,
453 params: &[Value],
454 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<Option<Row>, crate::Error>> + Send + '_>>;
455
456 fn execute_internal(
458 &self,
459 cx: &Cx,
460 sql: &str,
461 params: &[Value],
462 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<u64, crate::Error>> + Send + '_>>;
463
464 fn savepoint_internal(
466 &self,
467 cx: &Cx,
468 name: &str,
469 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
470
471 fn rollback_to_internal(
473 &self,
474 cx: &Cx,
475 name: &str,
476 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
477
478 fn release_internal(
480 &self,
481 cx: &Cx,
482 name: &str,
483 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
484
485 fn commit_internal(
487 &self,
488 cx: &Cx,
489 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
490
491 fn rollback_internal(
493 &self,
494 cx: &Cx,
495 ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
496}
497
498impl<'conn> Transaction<'conn> {
499 pub fn new(conn: &'conn dyn TransactionInternal) -> Self {
503 Self {
504 conn,
505 finalized: false,
506 }
507 }
508
509 #[must_use]
511 pub const fn is_finalized(&self) -> bool {
512 self.finalized
513 }
514}
515
516impl TransactionOps for Transaction<'_> {
517 fn query(
518 &self,
519 cx: &Cx,
520 sql: &str,
521 params: &[Value],
522 ) -> impl Future<Output = Outcome<Vec<Row>, crate::Error>> + Send {
523 self.conn.query_internal(cx, sql, params)
524 }
525
526 fn query_one(
527 &self,
528 cx: &Cx,
529 sql: &str,
530 params: &[Value],
531 ) -> impl Future<Output = Outcome<Option<Row>, crate::Error>> + Send {
532 self.conn.query_one_internal(cx, sql, params)
533 }
534
535 fn execute(
536 &self,
537 cx: &Cx,
538 sql: &str,
539 params: &[Value],
540 ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send {
541 self.conn.execute_internal(cx, sql, params)
542 }
543
544 fn savepoint(
545 &self,
546 cx: &Cx,
547 name: &str,
548 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send {
549 self.conn.savepoint_internal(cx, name)
550 }
551
552 fn rollback_to(
553 &self,
554 cx: &Cx,
555 name: &str,
556 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send {
557 self.conn.rollback_to_internal(cx, name)
558 }
559
560 fn release(
561 &self,
562 cx: &Cx,
563 name: &str,
564 ) -> impl Future<Output = Outcome<(), crate::Error>> + Send {
565 self.conn.release_internal(cx, name)
566 }
567
568 async fn commit(mut self, cx: &Cx) -> Outcome<(), crate::Error> {
569 self.finalized = true;
570 self.conn.commit_internal(cx).await
571 }
572
573 async fn rollback(mut self, cx: &Cx) -> Outcome<(), crate::Error> {
574 self.finalized = true;
575 self.conn.rollback_internal(cx).await
576 }
577}
578
579use std::future::Future;
580
581impl Drop for Transaction<'_> {
582 fn drop(&mut self) {
583 if !self.finalized {
584 }
589 }
590}
591
592#[derive(Debug, Clone)]
594pub struct ConnectionConfig {
595 pub url: String,
597 pub connect_timeout_ms: u64,
599 pub query_timeout_ms: u64,
601 pub ssl_mode: SslMode,
603 pub application_name: Option<String>,
605}
606
607#[derive(Debug, Clone, Copy, Default)]
609pub enum SslMode {
610 Disable,
612 #[default]
614 Prefer,
615 Require,
617 VerifyCa,
619 VerifyFull,
621}
622
623impl Default for ConnectionConfig {
624 fn default() -> Self {
625 Self {
626 url: String::new(),
627 connect_timeout_ms: 30_000,
628 query_timeout_ms: 30_000,
629 ssl_mode: SslMode::default(),
630 application_name: None,
631 }
632 }
633}
634
635impl ConnectionConfig {
636 pub fn new(url: impl Into<String>) -> Self {
638 Self {
639 url: url.into(),
640 ..Default::default()
641 }
642 }
643
644 pub fn connect_timeout(mut self, ms: u64) -> Self {
646 self.connect_timeout_ms = ms;
647 self
648 }
649
650 pub fn query_timeout(mut self, ms: u64) -> Self {
652 self.query_timeout_ms = ms;
653 self
654 }
655
656 pub fn ssl_mode(mut self, mode: SslMode) -> Self {
658 self.ssl_mode = mode;
659 self
660 }
661
662 pub fn application_name(mut self, name: impl Into<String>) -> Self {
664 self.application_name = Some(name.into());
665 self
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn test_isolation_level_default() {
675 let level = IsolationLevel::default();
676 assert_eq!(level, IsolationLevel::ReadCommitted);
677 }
678
679 #[test]
680 fn test_isolation_level_as_sql() {
681 assert_eq!(IsolationLevel::ReadUncommitted.as_sql(), "READ UNCOMMITTED");
682 assert_eq!(IsolationLevel::ReadCommitted.as_sql(), "READ COMMITTED");
683 assert_eq!(IsolationLevel::RepeatableRead.as_sql(), "REPEATABLE READ");
684 assert_eq!(IsolationLevel::Serializable.as_sql(), "SERIALIZABLE");
685 }
686
687 #[test]
688 fn test_prepared_statement_new() {
689 let stmt = PreparedStatement::new(1, "SELECT * FROM users WHERE id = $1".to_string(), 1);
690 assert_eq!(stmt.id(), 1);
691 assert_eq!(stmt.sql(), "SELECT * FROM users WHERE id = $1");
692 assert_eq!(stmt.param_count(), 1);
693 assert!(stmt.columns().is_none());
694 }
695
696 #[test]
697 fn test_prepared_statement_with_columns() {
698 let stmt = PreparedStatement::with_columns(
699 2,
700 "SELECT id, name FROM users".to_string(),
701 0,
702 vec!["id".to_string(), "name".to_string()],
703 );
704 assert_eq!(stmt.id(), 2);
705 assert_eq!(stmt.param_count(), 0);
706 assert_eq!(
707 stmt.columns(),
708 Some(&["id".to_string(), "name".to_string()][..])
709 );
710 }
711
712 #[test]
713 fn test_prepared_statement_validate_params() {
714 let stmt = PreparedStatement::new(1, "SELECT $1, $2".to_string(), 2);
715
716 assert!(!stmt.validate_params(&[]));
717 assert!(!stmt.validate_params(&[Value::Int(1)]));
718 assert!(stmt.validate_params(&[Value::Int(1), Value::Int(2)]));
719 assert!(!stmt.validate_params(&[Value::Int(1), Value::Int(2), Value::Int(3)]));
720 }
721
722 #[test]
723 fn test_ssl_mode_default() {
724 let mode = SslMode::default();
725 assert!(matches!(mode, SslMode::Prefer));
726 }
727
728 #[test]
729 fn test_connection_config_builder() {
730 let config = ConnectionConfig::new("postgres://localhost/test")
731 .connect_timeout(5000)
732 .query_timeout(10000)
733 .ssl_mode(SslMode::Require)
734 .application_name("test_app");
735
736 assert_eq!(config.url, "postgres://localhost/test");
737 assert_eq!(config.connect_timeout_ms, 5000);
738 assert_eq!(config.query_timeout_ms, 10000);
739 assert!(matches!(config.ssl_mode, SslMode::Require));
740 assert_eq!(config.application_name, Some("test_app".to_string()));
741 }
742
743 #[test]
744 fn test_connection_config_default() {
745 let config = ConnectionConfig::default();
746 assert_eq!(config.url, "");
747 assert_eq!(config.connect_timeout_ms, 30_000);
748 assert_eq!(config.query_timeout_ms, 30_000);
749 assert!(matches!(config.ssl_mode, SslMode::Prefer));
750 assert!(config.application_name.is_none());
751 }
752}