mssql_client/
config.rs

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