mssql_client/
config.rs

1//! Client configuration.
2
3use std::time::Duration;
4
5use mssql_auth::Credentials;
6#[cfg(feature = "tls")]
7use mssql_tls::TlsConfig;
8use tds_protocol::version::TdsVersion;
9
10/// Configuration for Azure SQL redirect handling.
11///
12/// Azure SQL Gateway may redirect connections to different backend servers.
13/// This configuration controls how the driver handles these redirects.
14#[derive(Debug, Clone)]
15pub struct RedirectConfig {
16    /// Maximum number of redirect attempts (default: 2).
17    pub max_redirects: u8,
18    /// Whether to follow redirects automatically (default: true).
19    pub follow_redirects: bool,
20}
21
22impl Default for RedirectConfig {
23    fn default() -> Self {
24        Self {
25            max_redirects: 2,
26            follow_redirects: true,
27        }
28    }
29}
30
31impl RedirectConfig {
32    /// Create a new redirect configuration with defaults.
33    #[must_use]
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Set the maximum number of redirect attempts.
39    #[must_use]
40    pub fn max_redirects(mut self, max: u8) -> Self {
41        self.max_redirects = max;
42        self
43    }
44
45    /// Enable or disable automatic redirect following.
46    #[must_use]
47    pub fn follow_redirects(mut self, follow: bool) -> Self {
48        self.follow_redirects = follow;
49        self
50    }
51
52    /// Disable automatic redirect following.
53    ///
54    /// When disabled, the driver will return an error with the redirect
55    /// information instead of automatically following the redirect.
56    #[must_use]
57    pub fn no_follow() -> Self {
58        Self {
59            max_redirects: 0,
60            follow_redirects: false,
61        }
62    }
63}
64
65/// Timeout configuration for various connection phases.
66///
67/// Per ARCHITECTURE.md §4.4, different phases of connection and command
68/// execution have separate timeout controls.
69#[derive(Debug, Clone)]
70pub struct TimeoutConfig {
71    /// Time to establish TCP connection (default: 15s).
72    pub connect_timeout: Duration,
73    /// Time to complete TLS handshake (default: 10s).
74    pub tls_timeout: Duration,
75    /// Time to complete login sequence (default: 30s).
76    pub login_timeout: Duration,
77    /// Default timeout for command execution (default: 30s).
78    pub command_timeout: Duration,
79    /// Time before idle connection is closed (default: 300s).
80    pub idle_timeout: Duration,
81    /// Interval for connection keep-alive (default: 30s).
82    pub keepalive_interval: Option<Duration>,
83}
84
85impl Default for TimeoutConfig {
86    fn default() -> Self {
87        Self {
88            connect_timeout: Duration::from_secs(15),
89            tls_timeout: Duration::from_secs(10),
90            login_timeout: Duration::from_secs(30),
91            command_timeout: Duration::from_secs(30),
92            idle_timeout: Duration::from_secs(300),
93            keepalive_interval: Some(Duration::from_secs(30)),
94        }
95    }
96}
97
98impl TimeoutConfig {
99    /// Create a new timeout configuration with defaults.
100    #[must_use]
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Set the TCP connection timeout.
106    #[must_use]
107    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
108        self.connect_timeout = timeout;
109        self
110    }
111
112    /// Set the TLS handshake timeout.
113    #[must_use]
114    pub fn tls_timeout(mut self, timeout: Duration) -> Self {
115        self.tls_timeout = timeout;
116        self
117    }
118
119    /// Set the login sequence timeout.
120    #[must_use]
121    pub fn login_timeout(mut self, timeout: Duration) -> Self {
122        self.login_timeout = timeout;
123        self
124    }
125
126    /// Set the default command execution timeout.
127    #[must_use]
128    pub fn command_timeout(mut self, timeout: Duration) -> Self {
129        self.command_timeout = timeout;
130        self
131    }
132
133    /// Set the idle connection timeout.
134    #[must_use]
135    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
136        self.idle_timeout = timeout;
137        self
138    }
139
140    /// Set the keep-alive interval.
141    #[must_use]
142    pub fn keepalive_interval(mut self, interval: Option<Duration>) -> Self {
143        self.keepalive_interval = interval;
144        self
145    }
146
147    /// Disable keep-alive.
148    #[must_use]
149    pub fn no_keepalive(mut self) -> Self {
150        self.keepalive_interval = None;
151        self
152    }
153
154    /// Get the total time allowed for a full connection (TCP + TLS + login).
155    #[must_use]
156    pub fn total_connect_timeout(&self) -> Duration {
157        self.connect_timeout + self.tls_timeout + self.login_timeout
158    }
159}
160
161/// Retry policy for transient error handling.
162///
163/// Per ADR-009, the driver can automatically retry operations that fail
164/// with transient errors (deadlocks, Azure service busy, etc.).
165#[derive(Debug, Clone)]
166pub struct RetryPolicy {
167    /// Maximum number of retry attempts (default: 3).
168    pub max_retries: u32,
169    /// Initial backoff duration before first retry (default: 100ms).
170    pub initial_backoff: Duration,
171    /// Maximum backoff duration between retries (default: 30s).
172    pub max_backoff: Duration,
173    /// Multiplier for exponential backoff (default: 2.0).
174    pub backoff_multiplier: f64,
175    /// Whether to add random jitter to backoff times (default: true).
176    pub jitter: bool,
177}
178
179impl Default for RetryPolicy {
180    fn default() -> Self {
181        Self {
182            max_retries: 3,
183            initial_backoff: Duration::from_millis(100),
184            max_backoff: Duration::from_secs(30),
185            backoff_multiplier: 2.0,
186            jitter: true,
187        }
188    }
189}
190
191impl RetryPolicy {
192    /// Create a new retry policy with defaults.
193    #[must_use]
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Set the maximum number of retry attempts.
199    #[must_use]
200    pub fn max_retries(mut self, max: u32) -> Self {
201        self.max_retries = max;
202        self
203    }
204
205    /// Set the initial backoff duration.
206    #[must_use]
207    pub fn initial_backoff(mut self, backoff: Duration) -> Self {
208        self.initial_backoff = backoff;
209        self
210    }
211
212    /// Set the maximum backoff duration.
213    #[must_use]
214    pub fn max_backoff(mut self, backoff: Duration) -> Self {
215        self.max_backoff = backoff;
216        self
217    }
218
219    /// Set the backoff multiplier for exponential backoff.
220    #[must_use]
221    pub fn backoff_multiplier(mut self, multiplier: f64) -> Self {
222        self.backoff_multiplier = multiplier;
223        self
224    }
225
226    /// Enable or disable jitter.
227    #[must_use]
228    pub fn jitter(mut self, enabled: bool) -> Self {
229        self.jitter = enabled;
230        self
231    }
232
233    /// Disable automatic retries.
234    #[must_use]
235    pub fn no_retry() -> Self {
236        Self {
237            max_retries: 0,
238            ..Self::default()
239        }
240    }
241
242    /// Calculate the backoff duration for a given retry attempt.
243    ///
244    /// Uses exponential backoff with optional jitter.
245    #[must_use]
246    pub fn backoff_for_attempt(&self, attempt: u32) -> Duration {
247        if attempt == 0 {
248            return Duration::ZERO;
249        }
250
251        let base = self.initial_backoff.as_millis() as f64
252            * self
253                .backoff_multiplier
254                .powi(attempt.saturating_sub(1) as i32);
255        let capped = base.min(self.max_backoff.as_millis() as f64);
256
257        if self.jitter {
258            // Simple jitter: multiply by random factor between 0.5 and 1.5
259            // In production, this would use a proper RNG
260            Duration::from_millis(capped as u64)
261        } else {
262            Duration::from_millis(capped as u64)
263        }
264    }
265
266    /// Check if more retries are allowed for the given attempt number.
267    #[must_use]
268    pub fn should_retry(&self, attempt: u32) -> bool {
269        attempt < self.max_retries
270    }
271}
272
273/// Configuration for connecting to SQL Server.
274///
275/// This struct is marked `#[non_exhaustive]` to allow adding new fields
276/// in future releases without breaking semver. Use [`Config::default()`]
277/// or [`Config::from_connection_string()`] to construct instances.
278#[derive(Debug, Clone)]
279#[non_exhaustive]
280pub struct Config {
281    /// Server hostname or IP address.
282    pub host: String,
283
284    /// Server port (default: 1433).
285    pub port: u16,
286
287    /// Database name.
288    pub database: Option<String>,
289
290    /// Authentication credentials.
291    pub credentials: Credentials,
292
293    /// TLS configuration (only available when `tls` feature is enabled).
294    #[cfg(feature = "tls")]
295    pub tls: TlsConfig,
296
297    /// Application name (shown in SQL Server management tools).
298    pub application_name: String,
299
300    /// Connection timeout.
301    pub connect_timeout: Duration,
302
303    /// Command timeout.
304    pub command_timeout: Duration,
305
306    /// TDS packet size.
307    pub packet_size: u16,
308
309    /// Whether to use TDS 8.0 strict mode.
310    pub strict_mode: bool,
311
312    /// Whether to trust the server certificate.
313    pub trust_server_certificate: bool,
314
315    /// Instance name (for named instances).
316    pub instance: Option<String>,
317
318    /// Whether to enable MARS (Multiple Active Result Sets).
319    pub mars: bool,
320
321    /// Whether to require encryption (TLS).
322    /// When true, the connection will use TLS even if the server doesn't require it.
323    /// When false, encryption is used only if the server requires it.
324    pub encrypt: bool,
325
326    /// Disable TLS entirely and connect with plaintext.
327    ///
328    /// **⚠️ SECURITY WARNING:** This completely disables TLS/SSL encryption.
329    /// Credentials and data will be transmitted in plaintext. Only use this
330    /// for development/testing on trusted networks with legacy SQL Server
331    /// instances that don't support modern TLS versions.
332    ///
333    /// This option exists for compatibility with legacy SQL Server versions
334    /// (2008 and earlier) that may only support TLS 1.0/1.1, which modern
335    /// TLS libraries (like rustls) don't support for security reasons.
336    ///
337    /// When `true`:
338    /// - Overrides the `encrypt` setting
339    /// - Sends `ENCRYPT_NOT_SUP` in PreLogin
340    /// - No TLS handshake occurs
341    /// - All traffic including login credentials is unencrypted
342    ///
343    /// **Do not use in production without understanding the security implications.**
344    pub no_tls: bool,
345
346    /// Redirect handling configuration (for Azure SQL).
347    pub redirect: RedirectConfig,
348
349    /// Retry policy for transient error handling.
350    pub retry: RetryPolicy,
351
352    /// Timeout configuration for various connection phases.
353    pub timeouts: TimeoutConfig,
354
355    /// Requested TDS protocol version.
356    ///
357    /// This specifies which TDS protocol version to request during connection.
358    /// The server may negotiate a lower version if it doesn't support the requested version.
359    ///
360    /// Supported versions:
361    /// - `TdsVersion::V7_3A` - SQL Server 2008
362    /// - `TdsVersion::V7_3B` - SQL Server 2008 R2
363    /// - `TdsVersion::V7_4` - SQL Server 2012+ (default)
364    /// - `TdsVersion::V8_0` - SQL Server 2022+ strict mode (requires `strict_mode = true`)
365    ///
366    /// Note: When `strict_mode` is enabled, this is ignored and TDS 8.0 is used.
367    pub tds_version: TdsVersion,
368}
369
370impl Default for Config {
371    fn default() -> Self {
372        let timeouts = TimeoutConfig::default();
373        Self {
374            host: "localhost".to_string(),
375            port: 1433,
376            database: None,
377            credentials: Credentials::sql_server("", ""),
378            #[cfg(feature = "tls")]
379            tls: TlsConfig::default(),
380            application_name: "mssql-client".to_string(),
381            connect_timeout: timeouts.connect_timeout,
382            command_timeout: timeouts.command_timeout,
383            packet_size: 4096,
384            strict_mode: false,
385            trust_server_certificate: false,
386            instance: None,
387            mars: false,
388            encrypt: true, // Default to encrypted for security
389            no_tls: false, // Never plaintext by default
390            redirect: RedirectConfig::default(),
391            retry: RetryPolicy::default(),
392            timeouts,
393            tds_version: TdsVersion::V7_4, // Default to TDS 7.4 for broad compatibility
394        }
395    }
396}
397
398impl Config {
399    /// Create a new configuration with default values.
400    #[must_use]
401    pub fn new() -> Self {
402        Self::default()
403    }
404
405    /// Parse a connection string into configuration.
406    ///
407    /// Supports ADO.NET-style connection strings:
408    /// ```text
409    /// Server=localhost;Database=mydb;User Id=sa;Password=secret;
410    /// ```
411    pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
412        let mut config = Self::default();
413
414        for part in conn_str.split(';') {
415            let part = part.trim();
416            if part.is_empty() {
417                continue;
418            }
419
420            let (key, value) = part
421                .split_once('=')
422                .ok_or_else(|| crate::error::Error::Config(format!("invalid key-value: {part}")))?;
423
424            let key = key.trim().to_lowercase();
425            let value = value.trim();
426
427            match key.as_str() {
428                "server" | "data source" | "host" => {
429                    // Handle host:port or host\instance format
430                    if let Some((host, port_or_instance)) = value.split_once(',') {
431                        config.host = host.to_string();
432                        config.port = port_or_instance.parse().map_err(|_| {
433                            crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
434                        })?;
435                    } else if let Some((host, instance)) = value.split_once('\\') {
436                        config.host = host.to_string();
437                        config.instance = Some(instance.to_string());
438                    } else {
439                        config.host = value.to_string();
440                    }
441                }
442                "port" => {
443                    config.port = value.parse().map_err(|_| {
444                        crate::error::Error::Config(format!("invalid port: {value}"))
445                    })?;
446                }
447                "database" | "initial catalog" => {
448                    config.database = Some(value.to_string());
449                }
450                "user id" | "uid" | "user" => {
451                    // Update credentials with new username
452                    if let Credentials::SqlServer { password, .. } = &config.credentials {
453                        config.credentials =
454                            Credentials::sql_server(value.to_string(), password.clone());
455                    }
456                }
457                "password" | "pwd" => {
458                    // Update credentials with new password
459                    if let Credentials::SqlServer { username, .. } = &config.credentials {
460                        config.credentials =
461                            Credentials::sql_server(username.clone(), value.to_string());
462                    }
463                }
464                "application name" | "app" => {
465                    config.application_name = value.to_string();
466                }
467                "connect timeout" | "connection timeout" => {
468                    let secs: u64 = value.parse().map_err(|_| {
469                        crate::error::Error::Config(format!("invalid timeout: {value}"))
470                    })?;
471                    config.connect_timeout = Duration::from_secs(secs);
472                }
473                "command timeout" => {
474                    let secs: u64 = value.parse().map_err(|_| {
475                        crate::error::Error::Config(format!("invalid timeout: {value}"))
476                    })?;
477                    config.command_timeout = Duration::from_secs(secs);
478                }
479                "trustservercertificate" | "trust server certificate" => {
480                    config.trust_server_certificate = value.eq_ignore_ascii_case("true")
481                        || value.eq_ignore_ascii_case("yes")
482                        || value == "1";
483                }
484                "encrypt" => {
485                    // Handle encryption levels: strict, true, false, yes, no, 1, 0, no_tls
486                    if value.eq_ignore_ascii_case("strict") {
487                        config.strict_mode = true;
488                        config.encrypt = true;
489                        config.no_tls = false;
490                    } else if value.eq_ignore_ascii_case("no_tls") {
491                        // Tiberius-compatible option for truly unencrypted connections.
492                        // This is for legacy SQL Server instances that don't support TLS 1.2+.
493                        config.no_tls = true;
494                        config.encrypt = false;
495                    } else if value.eq_ignore_ascii_case("true")
496                        || value.eq_ignore_ascii_case("yes")
497                        || value == "1"
498                    {
499                        config.encrypt = true;
500                        config.no_tls = false;
501                    } else if value.eq_ignore_ascii_case("false")
502                        || value.eq_ignore_ascii_case("no")
503                        || value == "0"
504                    {
505                        config.encrypt = false;
506                        config.no_tls = false;
507                    }
508                }
509                "multipleactiveresultsets" | "mars" => {
510                    config.mars = value.eq_ignore_ascii_case("true")
511                        || value.eq_ignore_ascii_case("yes")
512                        || value == "1";
513                }
514                "packet size" => {
515                    config.packet_size = value.parse().map_err(|_| {
516                        crate::error::Error::Config(format!("invalid packet size: {value}"))
517                    })?;
518                }
519                "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
520                    // Parse TDS version from connection string
521                    // Supports: "7.3", "7.3A", "7.3B", "7.4", "8.0"
522                    config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
523                        crate::error::Error::Config(format!(
524                            "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
525                        ))
526                    })?;
527                    // If TDS 8.0 is requested, enable strict mode
528                    if config.tds_version.is_tds_8() {
529                        config.strict_mode = true;
530                    }
531                }
532                _ => {
533                    // Ignore unknown options for forward compatibility
534                    tracing::debug!(
535                        key = key,
536                        value = value,
537                        "ignoring unknown connection string option"
538                    );
539                }
540            }
541        }
542
543        Ok(config)
544    }
545
546    /// Set the server host.
547    #[must_use]
548    pub fn host(mut self, host: impl Into<String>) -> Self {
549        self.host = host.into();
550        self
551    }
552
553    /// Set the server port.
554    #[must_use]
555    pub fn port(mut self, port: u16) -> Self {
556        self.port = port;
557        self
558    }
559
560    /// Set the database name.
561    #[must_use]
562    pub fn database(mut self, database: impl Into<String>) -> Self {
563        self.database = Some(database.into());
564        self
565    }
566
567    /// Set the credentials.
568    #[must_use]
569    pub fn credentials(mut self, credentials: Credentials) -> Self {
570        self.credentials = credentials;
571        self
572    }
573
574    /// Set the application name.
575    #[must_use]
576    pub fn application_name(mut self, name: impl Into<String>) -> Self {
577        self.application_name = name.into();
578        self
579    }
580
581    /// Set the connect timeout.
582    #[must_use]
583    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
584        self.connect_timeout = timeout;
585        self
586    }
587
588    /// Set trust server certificate option.
589    #[must_use]
590    pub fn trust_server_certificate(mut self, trust: bool) -> Self {
591        self.trust_server_certificate = trust;
592        #[cfg(feature = "tls")]
593        {
594            self.tls = self.tls.trust_server_certificate(trust);
595        }
596        self
597    }
598
599    /// Enable TDS 8.0 strict mode.
600    #[must_use]
601    pub fn strict_mode(mut self, enabled: bool) -> Self {
602        self.strict_mode = enabled;
603        #[cfg(feature = "tls")]
604        {
605            self.tls = self.tls.strict_mode(enabled);
606        }
607        if enabled {
608            self.tds_version = TdsVersion::V8_0;
609        }
610        self
611    }
612
613    /// Set the TDS protocol version.
614    ///
615    /// This specifies which TDS protocol version to request during connection.
616    /// The server may negotiate a lower version if it doesn't support the requested version.
617    ///
618    /// # Examples
619    ///
620    /// ```rust,ignore
621    /// use mssql_client::Config;
622    /// use tds_protocol::version::TdsVersion;
623    ///
624    /// // Connect to SQL Server 2008
625    /// let config = Config::new()
626    ///     .host("legacy-server")
627    ///     .tds_version(TdsVersion::V7_3A);
628    ///
629    /// // Connect to SQL Server 2008 R2
630    /// let config = Config::new()
631    ///     .host("legacy-server")
632    ///     .tds_version(TdsVersion::V7_3B);
633    /// ```
634    ///
635    /// Note: When `strict_mode` is enabled, this is ignored and TDS 8.0 is used.
636    #[must_use]
637    pub fn tds_version(mut self, version: TdsVersion) -> Self {
638        self.tds_version = version;
639        // If TDS 8.0 is requested, automatically enable strict mode
640        if version.is_tds_8() {
641            self.strict_mode = true;
642            #[cfg(feature = "tls")]
643            {
644                self.tls = self.tls.strict_mode(true);
645            }
646        }
647        self
648    }
649
650    /// Enable or disable TLS encryption.
651    ///
652    /// When `true` (default), the connection will use TLS encryption.
653    /// When `false`, encryption is used only if the server requires it.
654    ///
655    /// **Warning:** Disabling encryption is insecure and should only be
656    /// used for development/testing on trusted networks.
657    #[must_use]
658    pub fn encrypt(mut self, enabled: bool) -> Self {
659        self.encrypt = enabled;
660        self
661    }
662
663    /// Disable TLS entirely and connect with plaintext (Tiberius-compatible).
664    ///
665    /// **⚠️ SECURITY WARNING:** This completely disables TLS/SSL encryption.
666    /// Credentials and all data will be transmitted in plaintext over the network.
667    ///
668    /// # When to use this
669    ///
670    /// This option exists for compatibility with legacy SQL Server versions
671    /// (2008 and earlier) that may only support TLS 1.0/1.1. Modern TLS libraries
672    /// like rustls require TLS 1.2 or higher for security reasons, making it
673    /// impossible to establish encrypted connections to these older servers.
674    ///
675    /// # Security implications
676    ///
677    /// When enabled:
678    /// - Login credentials are sent in plaintext
679    /// - All query data is transmitted without encryption
680    /// - Network traffic can be intercepted and read by attackers
681    ///
682    /// **Only use this for development/testing on isolated, trusted networks.**
683    ///
684    /// # Example
685    ///
686    /// ```rust,ignore
687    /// // Connection string (Tiberius-compatible)
688    /// let config = Config::from_connection_string(
689    ///     "Server=legacy-server;User Id=sa;Password=secret;Encrypt=no_tls"
690    /// )?;
691    ///
692    /// // Builder API
693    /// let config = Config::new()
694    ///     .host("legacy-server")
695    ///     .no_tls(true);
696    /// ```
697    #[must_use]
698    pub fn no_tls(mut self, enabled: bool) -> Self {
699        self.no_tls = enabled;
700        if enabled {
701            self.encrypt = false;
702        }
703        self
704    }
705
706    /// Create a new configuration with a different host (for routing).
707    #[must_use]
708    pub fn with_host(mut self, host: &str) -> Self {
709        self.host = host.to_string();
710        self
711    }
712
713    /// Create a new configuration with a different port (for routing).
714    #[must_use]
715    pub fn with_port(mut self, port: u16) -> Self {
716        self.port = port;
717        self
718    }
719
720    /// Set the redirect handling configuration.
721    #[must_use]
722    pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
723        self.redirect = redirect;
724        self
725    }
726
727    /// Set the maximum number of redirect attempts.
728    #[must_use]
729    pub fn max_redirects(mut self, max: u8) -> Self {
730        self.redirect.max_redirects = max;
731        self
732    }
733
734    /// Set the retry policy for transient error handling.
735    #[must_use]
736    pub fn retry(mut self, retry: RetryPolicy) -> Self {
737        self.retry = retry;
738        self
739    }
740
741    /// Set the maximum number of retry attempts.
742    #[must_use]
743    pub fn max_retries(mut self, max: u32) -> Self {
744        self.retry.max_retries = max;
745        self
746    }
747
748    /// Set the timeout configuration.
749    #[must_use]
750    pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
751        // Sync the legacy fields for backward compatibility first
752        self.connect_timeout = timeouts.connect_timeout;
753        self.command_timeout = timeouts.command_timeout;
754        self.timeouts = timeouts;
755        self
756    }
757}
758
759#[cfg(test)]
760#[allow(clippy::unwrap_used)]
761mod tests {
762    use super::*;
763
764    #[test]
765    fn test_connection_string_parsing() {
766        let config = Config::from_connection_string(
767            "Server=localhost;Database=test;User Id=sa;Password=secret;",
768        )
769        .unwrap();
770
771        assert_eq!(config.host, "localhost");
772        assert_eq!(config.database, Some("test".to_string()));
773    }
774
775    #[test]
776    fn test_connection_string_with_port() {
777        let config =
778            Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
779
780        assert_eq!(config.host, "localhost");
781        assert_eq!(config.port, 1434);
782    }
783
784    #[test]
785    fn test_connection_string_with_instance() {
786        let config =
787            Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
788
789        assert_eq!(config.host, "localhost");
790        assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
791    }
792
793    #[test]
794    fn test_redirect_config_defaults() {
795        let config = RedirectConfig::default();
796        assert_eq!(config.max_redirects, 2);
797        assert!(config.follow_redirects);
798    }
799
800    #[test]
801    fn test_redirect_config_builder() {
802        let config = RedirectConfig::new()
803            .max_redirects(5)
804            .follow_redirects(false);
805        assert_eq!(config.max_redirects, 5);
806        assert!(!config.follow_redirects);
807    }
808
809    #[test]
810    fn test_redirect_config_no_follow() {
811        let config = RedirectConfig::no_follow();
812        assert_eq!(config.max_redirects, 0);
813        assert!(!config.follow_redirects);
814    }
815
816    #[test]
817    fn test_config_redirect_builder() {
818        let config = Config::new().max_redirects(3);
819        assert_eq!(config.redirect.max_redirects, 3);
820
821        let config2 = Config::new().redirect(RedirectConfig::no_follow());
822        assert!(!config2.redirect.follow_redirects);
823    }
824
825    #[test]
826    fn test_retry_policy_defaults() {
827        let policy = RetryPolicy::default();
828        assert_eq!(policy.max_retries, 3);
829        assert_eq!(policy.initial_backoff, Duration::from_millis(100));
830        assert_eq!(policy.max_backoff, Duration::from_secs(30));
831        assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
832        assert!(policy.jitter);
833    }
834
835    #[test]
836    fn test_retry_policy_builder() {
837        let policy = RetryPolicy::new()
838            .max_retries(5)
839            .initial_backoff(Duration::from_millis(200))
840            .max_backoff(Duration::from_secs(60))
841            .backoff_multiplier(3.0)
842            .jitter(false);
843
844        assert_eq!(policy.max_retries, 5);
845        assert_eq!(policy.initial_backoff, Duration::from_millis(200));
846        assert_eq!(policy.max_backoff, Duration::from_secs(60));
847        assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
848        assert!(!policy.jitter);
849    }
850
851    #[test]
852    fn test_retry_policy_no_retry() {
853        let policy = RetryPolicy::no_retry();
854        assert_eq!(policy.max_retries, 0);
855        assert!(!policy.should_retry(0));
856    }
857
858    #[test]
859    fn test_retry_policy_should_retry() {
860        let policy = RetryPolicy::new().max_retries(3);
861        assert!(policy.should_retry(0));
862        assert!(policy.should_retry(1));
863        assert!(policy.should_retry(2));
864        assert!(!policy.should_retry(3));
865        assert!(!policy.should_retry(4));
866    }
867
868    #[test]
869    fn test_retry_policy_backoff_calculation() {
870        let policy = RetryPolicy::new()
871            .initial_backoff(Duration::from_millis(100))
872            .backoff_multiplier(2.0)
873            .max_backoff(Duration::from_secs(10))
874            .jitter(false);
875
876        assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
877        assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
878        assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
879        assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
880    }
881
882    #[test]
883    fn test_retry_policy_backoff_capped() {
884        let policy = RetryPolicy::new()
885            .initial_backoff(Duration::from_secs(1))
886            .backoff_multiplier(10.0)
887            .max_backoff(Duration::from_secs(5))
888            .jitter(false);
889
890        // Attempt 3 would be 1s * 10^2 = 100s, but capped at 5s
891        assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
892    }
893
894    #[test]
895    fn test_config_retry_builder() {
896        let config = Config::new().max_retries(5);
897        assert_eq!(config.retry.max_retries, 5);
898
899        let config2 = Config::new().retry(RetryPolicy::no_retry());
900        assert_eq!(config2.retry.max_retries, 0);
901    }
902
903    #[test]
904    fn test_timeout_config_defaults() {
905        let config = TimeoutConfig::default();
906        assert_eq!(config.connect_timeout, Duration::from_secs(15));
907        assert_eq!(config.tls_timeout, Duration::from_secs(10));
908        assert_eq!(config.login_timeout, Duration::from_secs(30));
909        assert_eq!(config.command_timeout, Duration::from_secs(30));
910        assert_eq!(config.idle_timeout, Duration::from_secs(300));
911        assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
912    }
913
914    #[test]
915    fn test_timeout_config_builder() {
916        let config = TimeoutConfig::new()
917            .connect_timeout(Duration::from_secs(5))
918            .tls_timeout(Duration::from_secs(3))
919            .login_timeout(Duration::from_secs(10))
920            .command_timeout(Duration::from_secs(60))
921            .idle_timeout(Duration::from_secs(600))
922            .keepalive_interval(Some(Duration::from_secs(60)));
923
924        assert_eq!(config.connect_timeout, Duration::from_secs(5));
925        assert_eq!(config.tls_timeout, Duration::from_secs(3));
926        assert_eq!(config.login_timeout, Duration::from_secs(10));
927        assert_eq!(config.command_timeout, Duration::from_secs(60));
928        assert_eq!(config.idle_timeout, Duration::from_secs(600));
929        assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
930    }
931
932    #[test]
933    fn test_timeout_config_no_keepalive() {
934        let config = TimeoutConfig::new().no_keepalive();
935        assert_eq!(config.keepalive_interval, None);
936    }
937
938    #[test]
939    fn test_timeout_config_total_connect() {
940        let config = TimeoutConfig::new()
941            .connect_timeout(Duration::from_secs(5))
942            .tls_timeout(Duration::from_secs(3))
943            .login_timeout(Duration::from_secs(10));
944
945        // 5 + 3 + 10 = 18 seconds
946        assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
947    }
948
949    #[test]
950    fn test_config_timeouts_builder() {
951        let timeouts = TimeoutConfig::new()
952            .connect_timeout(Duration::from_secs(5))
953            .command_timeout(Duration::from_secs(60));
954
955        let config = Config::new().timeouts(timeouts);
956        assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
957        assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
958        // Check that legacy fields are synced
959        assert_eq!(config.connect_timeout, Duration::from_secs(5));
960        assert_eq!(config.command_timeout, Duration::from_secs(60));
961    }
962
963    #[test]
964    fn test_tds_version_default() {
965        let config = Config::default();
966        assert_eq!(config.tds_version, TdsVersion::V7_4);
967        assert!(!config.strict_mode);
968    }
969
970    #[test]
971    fn test_tds_version_builder() {
972        let config = Config::new().tds_version(TdsVersion::V7_3A);
973        assert_eq!(config.tds_version, TdsVersion::V7_3A);
974        assert!(!config.strict_mode);
975
976        let config = Config::new().tds_version(TdsVersion::V7_3B);
977        assert_eq!(config.tds_version, TdsVersion::V7_3B);
978        assert!(!config.strict_mode);
979
980        // TDS 8.0 should automatically enable strict mode
981        let config = Config::new().tds_version(TdsVersion::V8_0);
982        assert_eq!(config.tds_version, TdsVersion::V8_0);
983        assert!(config.strict_mode);
984    }
985
986    #[test]
987    fn test_strict_mode_sets_tds_8() {
988        let config = Config::new().strict_mode(true);
989        assert!(config.strict_mode);
990        assert_eq!(config.tds_version, TdsVersion::V8_0);
991    }
992
993    #[test]
994    fn test_connection_string_tds_version() {
995        // Test TDS 7.3
996        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
997        assert_eq!(config.tds_version, TdsVersion::V7_3A);
998
999        // Test TDS 7.3A explicitly
1000        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1001        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1002
1003        // Test TDS 7.3B
1004        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1005        assert_eq!(config.tds_version, TdsVersion::V7_3B);
1006
1007        // Test TDS 7.4
1008        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1009        assert_eq!(config.tds_version, TdsVersion::V7_4);
1010
1011        // Test TDS 8.0 enables strict mode
1012        let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1013        assert_eq!(config.tds_version, TdsVersion::V8_0);
1014        assert!(config.strict_mode);
1015
1016        // Test alternative key names
1017        let config =
1018            Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1019        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1020    }
1021
1022    #[test]
1023    fn test_connection_string_invalid_tds_version() {
1024        let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1025        assert!(result.is_err());
1026
1027        let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1028        assert!(result.is_err());
1029    }
1030
1031    #[test]
1032    fn test_connection_string_no_tls() {
1033        // no_tls should disable TLS entirely
1034        let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1035        assert!(config.no_tls);
1036        assert!(!config.encrypt);
1037        assert!(!config.strict_mode);
1038
1039        // Case insensitive
1040        let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1041        assert!(config.no_tls);
1042
1043        // Encrypt=true should disable no_tls
1044        let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1045        assert!(!config.no_tls);
1046        assert!(config.encrypt);
1047
1048        // Encrypt=strict should disable no_tls
1049        let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1050        assert!(!config.no_tls);
1051        assert!(config.encrypt);
1052        assert!(config.strict_mode);
1053    }
1054
1055    #[test]
1056    fn test_no_tls_builder() {
1057        // Builder method
1058        let config = Config::new().no_tls(true);
1059        assert!(config.no_tls);
1060        assert!(!config.encrypt);
1061
1062        // Disable
1063        let config = Config::new().no_tls(true).no_tls(false);
1064        assert!(!config.no_tls);
1065    }
1066}