Skip to main content

sqlmodel_core/
connection.rs

1//! Database connection traits.
2//!
3//! This module defines the core abstractions for database connections:
4//!
5//! - [`Connection`] - Main trait for executing queries and managing transactions
6//! - [`Transaction`] - Trait for transactional operations with savepoint support
7//! - [`IsolationLevel`] - SQL transaction isolation levels
8//! - [`PreparedStatement`] - Pre-compiled statement for efficient repeated execution
9//!
10//! All operations integrate with asupersync's structured concurrency via `Cx` context
11//! for proper cancellation and timeout handling.
12
13use crate::error::Result;
14use crate::row::Row;
15use crate::value::Value;
16use asupersync::{Cx, Outcome};
17
18/// Transaction isolation level.
19///
20/// Defines the degree to which one transaction must be isolated from
21/// resource or data modifications made by other concurrent transactions.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum IsolationLevel {
24    /// Read uncommitted: Transactions can see uncommitted changes from others.
25    /// This is the lowest isolation level, providing minimal guarantees.
26    /// Use with caution - dirty reads, non-repeatable reads, and phantoms possible.
27    ReadUncommitted,
28
29    /// Read committed: Transactions only see committed changes from others.
30    /// This is the default for PostgreSQL. Prevents dirty reads but allows
31    /// non-repeatable reads and phantoms.
32    #[default]
33    ReadCommitted,
34
35    /// Repeatable read: Transactions see a consistent snapshot of the database.
36    /// Prevents dirty reads and non-repeatable reads, but phantoms are possible
37    /// in some databases (though not in PostgreSQL).
38    RepeatableRead,
39
40    /// Serializable: Transactions appear to execute sequentially.
41    /// The highest isolation level, providing complete isolation but potentially
42    /// requiring retries due to serialization failures.
43    Serializable,
44}
45
46impl IsolationLevel {
47    /// Get the SQL syntax for this isolation level.
48    #[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/// A prepared statement for repeated execution.
60///
61/// Prepared statements are pre-compiled by the database, allowing efficient
62/// repeated execution with different parameter values. They also help prevent
63/// SQL injection since parameters are handled separately from the SQL text.
64#[derive(Debug, Clone)]
65pub struct PreparedStatement {
66    /// Unique identifier for this prepared statement (driver-specific)
67    id: u64,
68    /// The original SQL text
69    sql: String,
70    /// Number of expected parameters
71    param_count: usize,
72    /// Column information for result rows (if available)
73    columns: Option<Vec<String>>,
74}
75
76impl PreparedStatement {
77    /// Create a new prepared statement.
78    ///
79    /// This is typically called by the driver, not by users directly.
80    #[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    /// Create a prepared statement with column information.
91    #[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    /// Get the statement ID.
102    #[must_use]
103    pub const fn id(&self) -> u64 {
104        self.id
105    }
106
107    /// Get the original SQL text.
108    #[must_use]
109    pub fn sql(&self) -> &str {
110        &self.sql
111    }
112
113    /// Get the expected number of parameters.
114    #[must_use]
115    pub const fn param_count(&self) -> usize {
116        self.param_count
117    }
118
119    /// Get the column information, if available.
120    #[must_use]
121    pub fn columns(&self) -> Option<&[String]> {
122        self.columns.as_deref()
123    }
124
125    /// Check if the provided parameters match the expected count.
126    #[must_use]
127    pub fn validate_params(&self, params: &[Value]) -> bool {
128        params.len() == self.param_count
129    }
130}
131
132/// A database connection capable of executing queries.
133///
134/// All operations are async and take a `Cx` context for cancellation/timeout support.
135/// Implementations must be `Send + Sync` for use across async boundaries.
136///
137/// # Transaction Support
138///
139/// Use [`begin`](Connection::begin) or [`begin_with`](Connection::begin_with) to
140/// start transactions. Transactions must be explicitly committed or rolled back.
141///
142/// # Example
143///
144/// ```rust,ignore
145/// // Execute a simple query
146/// let rows = conn.query(&cx, "SELECT * FROM users WHERE id = $1", &[Value::Int(1)]).await?;
147///
148/// // Use a transaction
149/// let mut tx = conn.begin(&cx).await?;
150/// tx.execute(&cx, "INSERT INTO logs (msg) VALUES ($1)", &[Value::Text("action".into())]).await?;
151/// tx.commit(&cx).await?;
152/// ```
153/// SQL dialect enumeration for cross-database compatibility.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
155pub enum Dialect {
156    /// PostgreSQL dialect (uses $1, $2 placeholders)
157    #[default]
158    Postgres,
159    /// SQLite dialect (uses ?1, ?2 placeholders)
160    Sqlite,
161    /// MySQL dialect (uses ? placeholders)
162    Mysql,
163}
164
165impl Dialect {
166    /// Generate a placeholder for the given parameter index (1-based).
167    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    /// Get the string concatenation operator for this dialect.
176    pub const fn concat_op(self) -> &'static str {
177        match self {
178            Dialect::Postgres | Dialect::Sqlite => "||",
179            Dialect::Mysql => "", // MySQL uses CONCAT() function
180        }
181    }
182
183    /// Check if this dialect supports ILIKE.
184    pub const fn supports_ilike(self) -> bool {
185        matches!(self, Dialect::Postgres)
186    }
187
188    /// Quote an identifier for this dialect.
189    ///
190    /// Properly escapes embedded quote characters by doubling them:
191    /// - For Postgres/SQLite: `"` becomes `""`
192    /// - For MySQL: `` ` `` becomes ``` `` ```
193    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    /// The transaction type returned by this connection.
209    type Tx<'conn>: TransactionOps
210    where
211        Self: 'conn;
212
213    /// Get the SQL dialect for this connection.
214    ///
215    /// This is used by query builders to generate dialect-specific SQL.
216    /// Defaults to Postgres for backwards compatibility.
217    fn dialect(&self) -> Dialect {
218        Dialect::Postgres
219    }
220
221    /// Execute a query and return all rows.
222    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    /// Execute a query and return the first row, if any.
230    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    /// Execute a statement (INSERT, UPDATE, DELETE) and return rows affected.
238    fn execute(
239        &self,
240        cx: &Cx,
241        sql: &str,
242        params: &[Value],
243    ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send;
244
245    /// Execute an INSERT and return the last inserted ID.
246    ///
247    /// For PostgreSQL, this typically uses RETURNING to get the inserted ID.
248    /// The exact behavior depends on the driver implementation.
249    fn insert(
250        &self,
251        cx: &Cx,
252        sql: &str,
253        params: &[Value],
254    ) -> impl Future<Output = Outcome<i64, crate::Error>> + Send;
255
256    /// Execute multiple statements in a batch.
257    ///
258    /// Returns the number of rows affected by each statement.
259    /// The statements are executed sequentially but may be optimized
260    /// by the driver for better performance.
261    fn batch(
262        &self,
263        cx: &Cx,
264        statements: &[(String, Vec<Value>)],
265    ) -> impl Future<Output = Outcome<Vec<u64>, crate::Error>> + Send;
266
267    /// Begin a transaction with default isolation level (ReadCommitted).
268    fn begin(&self, cx: &Cx) -> impl Future<Output = Outcome<Self::Tx<'_>, crate::Error>> + Send;
269
270    /// Begin a transaction with a specific isolation level.
271    fn begin_with(
272        &self,
273        cx: &Cx,
274        isolation: IsolationLevel,
275    ) -> impl Future<Output = Outcome<Self::Tx<'_>, crate::Error>> + Send;
276
277    /// Prepare a statement for repeated execution.
278    ///
279    /// Prepared statements are cached by the driver and can be executed
280    /// multiple times with different parameters efficiently.
281    fn prepare(
282        &self,
283        cx: &Cx,
284        sql: &str,
285    ) -> impl Future<Output = Outcome<PreparedStatement, crate::Error>> + Send;
286
287    /// Execute a prepared statement and return all rows.
288    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    /// Execute a prepared statement (INSERT, UPDATE, DELETE) and return rows affected.
296    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    /// Check if the connection is still valid by sending a ping.
304    fn ping(&self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
305
306    /// Check if the connection is still valid (alias for ping that returns bool).
307    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    /// Close the connection gracefully.
317    fn close(self, cx: &Cx) -> impl Future<Output = Result<()>> + Send;
318
319    /// Close the connection as part of pool retirement.
320    ///
321    /// The pool routes every teardown (shutdown, idle eviction, max-lifetime
322    /// expiry, and failed checkout validation) through this method exactly
323    /// once after removing the connection from pool state. It is never awaited
324    /// while the pool's internal mutex is held. Returning a healthy connection
325    /// to the idle queue does not call this method.
326    ///
327    /// The default simply delegates to [`Connection::close`], so callers that
328    /// close a connection they own directly see no behavioral change. Drivers
329    /// may override this with a cheaper teardown path — for example skipping a
330    /// close-time WAL checkpoint to avoid contention when many pooled
331    /// connections retire at once — provided all committed data remains
332    /// durable and recoverable by the next open.
333    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
341/// Trait for transaction operations.
342///
343/// This trait defines the interface for database transactions with
344/// support for savepoints. Transactions must be explicitly committed
345/// or rolled back; dropping without commit triggers automatic rollback.
346///
347/// # Savepoints
348///
349/// Savepoints allow partial rollback within a transaction:
350///
351/// ```rust,ignore
352/// let mut tx = conn.begin(&cx).await?;
353/// tx.execute(&cx, "INSERT INTO t1 (a) VALUES (1)", &[]).await?;
354/// tx.savepoint(&cx, "sp1").await?;
355/// tx.execute(&cx, "INSERT INTO t1 (a) VALUES (2)", &[]).await?;
356/// tx.rollback_to(&cx, "sp1").await?;  // Rollback only the second insert
357/// tx.commit(&cx).await?;  // Only first insert is committed
358/// ```
359pub trait TransactionOps: Send {
360    /// Execute a query within this transaction.
361    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    /// Execute a query and return the first row, if any.
369    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    /// Execute a statement within this transaction.
377    fn execute(
378        &self,
379        cx: &Cx,
380        sql: &str,
381        params: &[Value],
382    ) -> impl Future<Output = Outcome<u64, crate::Error>> + Send;
383
384    /// Create a savepoint within this transaction.
385    ///
386    /// Savepoints allow partial rollback without aborting the entire transaction.
387    fn savepoint(
388        &self,
389        cx: &Cx,
390        name: &str,
391    ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
392
393    /// Rollback to a previously created savepoint.
394    ///
395    /// All changes made after the savepoint are discarded, but the transaction
396    /// remains active and changes before the savepoint are preserved.
397    fn rollback_to(
398        &self,
399        cx: &Cx,
400        name: &str,
401    ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
402
403    /// Release a savepoint, making the changes permanent within the transaction.
404    ///
405    /// This frees resources associated with the savepoint but does not commit
406    /// changes to the database (that happens when the transaction commits).
407    fn release(
408        &self,
409        cx: &Cx,
410        name: &str,
411    ) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
412
413    /// Commit the transaction, making all changes permanent.
414    fn commit(self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
415
416    /// Rollback the transaction, discarding all changes.
417    fn rollback(self, cx: &Cx) -> impl Future<Output = Outcome<(), crate::Error>> + Send;
418}
419
420/// A database transaction (concrete implementation).
421///
422/// Transactions provide ACID guarantees and can be committed or rolled back.
423/// If dropped without committing, the transaction is automatically rolled back.
424///
425/// This is a concrete type used by the default transaction implementation.
426/// Driver-specific implementations may use their own types that implement
427/// [`TransactionOps`].
428pub struct Transaction<'conn> {
429    /// The underlying connection
430    conn: &'conn dyn TransactionInternal,
431    /// Whether this transaction has been finalized (committed or rolled back)
432    finalized: bool,
433}
434
435/// Internal trait for transaction operations (object-safe subset).
436///
437/// This trait provides a boxed-future version of TransactionOps for
438/// use with trait objects.
439pub trait TransactionInternal: Send + Sync {
440    /// Execute a query.
441    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    /// Execute a query and return first row.
449    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    /// Execute a statement.
457    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    /// Create a savepoint.
465    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    /// Rollback to a savepoint.
472    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    /// Release a savepoint.
479    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    /// Commit the transaction.
486    fn commit_internal(
487        &self,
488        cx: &Cx,
489    ) -> std::pin::Pin<Box<dyn Future<Output = Outcome<(), crate::Error>> + Send + '_>>;
490
491    /// Rollback the transaction.
492    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    /// Create a new transaction wrapper.
500    ///
501    /// This is typically called by the driver, not by users directly.
502    pub fn new(conn: &'conn dyn TransactionInternal) -> Self {
503        Self {
504            conn,
505            finalized: false,
506        }
507    }
508
509    /// Check if this transaction has been finalized.
510    #[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            // Transaction was not committed/rolled back explicitly.
585            // The actual rollback happens at the protocol level when the
586            // connection detects an unfinalized transaction scope.
587            // We can't do async in drop, so we just mark it here.
588        }
589    }
590}
591
592/// Configuration for database connections.
593#[derive(Debug, Clone)]
594pub struct ConnectionConfig {
595    /// Connection string or URL
596    pub url: String,
597    /// Connection timeout in milliseconds
598    pub connect_timeout_ms: u64,
599    /// Query timeout in milliseconds
600    pub query_timeout_ms: u64,
601    /// SSL mode
602    pub ssl_mode: SslMode,
603    /// Application name for connection identification
604    pub application_name: Option<String>,
605}
606
607/// SSL connection mode.
608#[derive(Debug, Clone, Copy, Default)]
609pub enum SslMode {
610    /// Never use SSL
611    Disable,
612    /// Prefer SSL but allow non-SSL
613    #[default]
614    Prefer,
615    /// Require SSL
616    Require,
617    /// Verify server certificate
618    VerifyCa,
619    /// Verify server certificate and hostname
620    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    /// Create a new connection config with the given URL.
637    pub fn new(url: impl Into<String>) -> Self {
638        Self {
639            url: url.into(),
640            ..Default::default()
641        }
642    }
643
644    /// Set the connection timeout.
645    pub fn connect_timeout(mut self, ms: u64) -> Self {
646        self.connect_timeout_ms = ms;
647        self
648    }
649
650    /// Set the query timeout.
651    pub fn query_timeout(mut self, ms: u64) -> Self {
652        self.query_timeout_ms = ms;
653        self
654    }
655
656    /// Set the SSL mode.
657    pub fn ssl_mode(mut self, mode: SslMode) -> Self {
658        self.ssl_mode = mode;
659        self
660    }
661
662    /// Set the application name.
663    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}