Skip to main content

mssql_client/
config.rs

1//! Client configuration.
2//!
3//! Supporting types (`RedirectConfig`, `TimeoutConfig`, `RetryPolicy`) live in
4//! the `types` submodule and are re-exported here for convenience.
5//!
6//! ## Connection strings
7//!
8//! [`Config::from_connection_string`] parses the ADO.NET
9//! `SqlConnection.ConnectionString` format: case-insensitive `Key=Value` pairs
10//! separated by `;`, with surrounding whitespace trimmed. Unknown keywords are
11//! ignored (logged at debug); recognized-but-unsupported keywords are logged at
12//! info rather than silently dropped.
13//!
14//! ```rust,no_run
15//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! use mssql_client::Config;
17//!
18//! let config = Config::from_connection_string(
19//!     "Server=tcp:myserver.database.windows.net,1433;Database=mydb;\
20//!      User Id=user;Password=secret;Encrypt=strict",
21//! )?;
22//! # let _ = config;
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! **Quoting.** A value with `;` or `=` may be wrapped in single or double
28//! quotes; double the quote char to escape it: `Password="my;pass"`,
29//! `Password='it''s complex'`.
30//!
31//! ## Keyword reference
32//!
33//! ### Server
34//!
35//! | Keyword | Aliases | Default | Description |
36//! |---------|---------|---------|-------------|
37//! | `Server` | `Data Source`, `Addr`, `Address`, `Network Address`, `Host` | `localhost` | Hostname or IP. Forms: `host`, `host,1433` (comma port), `host\INSTANCE` (named instance, resolved via SQL Browser), `tcp:host` (Azure prefix, stripped). `.` and `(local)` normalize to `127.0.0.1`; `np:` / `lpc:` (named pipes / shared memory) are rejected. |
38//! | `Port` | — | `1433` | TCP port. |
39//!
40//! ### Authentication and database
41//!
42//! | Keyword | Aliases | Default | Description |
43//! |---------|---------|---------|-------------|
44//! | `User Id` | `UID`, `User` | (empty) | SQL Server login username. |
45//! | `Password` | `PWD` | (empty) | SQL Server login password. |
46//! | `Database` | `Initial Catalog` | none | Target database. |
47//!
48//! ### Security
49//!
50//! | Keyword | Aliases | Default | Description |
51//! |---------|---------|---------|-------------|
52//! | `Encrypt` | — | `false` | `strict` (TDS 8.0, SQL Server 2022+), `mandatory`/`true`, `optional`, `no_tls`, or a boolean. |
53//! | `TrustServerCertificate` | `Trust Server Certificate` | `false` | Skip certificate validation (development only). |
54//!
55//! ### Timeouts
56//!
57//! | Keyword | Aliases | Default | Description |
58//! |---------|---------|---------|-------------|
59//! | `Connect Timeout` | `Connection Timeout`, `Timeout` | `15` | TCP connect timeout (seconds; `0` = none). |
60//! | `Command Timeout` | — | `30` | Query execution timeout (seconds; `0` = none). |
61//!
62//! ### Application identification
63//!
64//! | Keyword | Aliases | Default | Description |
65//! |---------|---------|---------|-------------|
66//! | `Application Name` | `App` | `mssql-client` | Appears in `sys.dm_exec_sessions`. |
67//! | `ApplicationIntent` | `Application Intent` | `ReadWrite` | `ReadOnly` routes to an AlwaysOn AG readable secondary. |
68//! | `Workstation ID` | `WSID` | machine hostname | Sent in LOGIN7 HostName; appears in `sys.dm_exec_sessions.host_name`. |
69//! | `Current Language` | `Language` | server default | Session language for server messages. |
70//!
71//! ### Connection resiliency
72//!
73//! | Keyword | Aliases | Default | Description |
74//! |---------|---------|---------|-------------|
75//! | `ConnectRetryCount` | `Connect Retry Count` | `3` | Reconnect attempts on idle-connection failure (wires to `RetryPolicy`). |
76//! | `ConnectRetryInterval` | `Connect Retry Interval` | `0` | Seconds between reconnect attempts. |
77//! | `MultiSubnetFailover` | `Multi Subnet Failover` | `false` | Race parallel TCP connects to all resolved IPs (AlwaysOn AG listeners). |
78//!
79//! ### Advanced
80//!
81//! | Keyword | Aliases | Default | Description |
82//! |---------|---------|---------|-------------|
83//! | `MultipleActiveResultSets` | `MARS` | `false` | MARS (not fully supported). |
84//! | `Packet Size` | — | `4096` | TDS packet size in bytes. |
85//! | `SendStringParametersAsUnicode` | `Send String Parameters As Unicode` | `true` | When `false`, sends `String`/`&str` params as VARCHAR (Windows-1252) instead of NVARCHAR (UTF-16) so SQL Server can index-seek VARCHAR columns. |
86//!
87//! Booleans accept `true`/`false`/`yes`/`no`/`1`/`0` (case-insensitive); an
88//! invalid boolean is an error, not a silent default.
89//!
90//! ### Recognized but not supported
91//!
92//! Logged at info level rather than silently dropped:
93//!
94//! | Keyword(s) | Guidance |
95//! |------------|----------|
96//! | `Max Pool Size`, `Min Pool Size`, `Pooling`, `Connection Lifetime`, `Load Balance Timeout` | Use the pool crate's `PoolConfig` instead. |
97//! | `Failover Partner` | Database mirroring failover not implemented. |
98//! | `Persist Security Info` | Passwords are never returned in connection strings. |
99//! | `Network Library`, `Enlist`, `Replication`, `Transaction Binding`, `Type System Version`, `User Instance`, `AttachDbFilename`, `Context Connection`, `Asynchronous Processing` | .NET-specific, not applicable. |
100//!
101//! **Common mistakes.** Use a comma (not a colon) for the port outside the
102//! `tcp:` prefix (`host,1433`); quote passwords containing `;`; use
103//! `user@server` for Azure SQL logins; and prefer `Encrypt=strict` or `true`
104//! (never `TrustServerCertificate=true`) in production. Never log a full
105//! connection string — it contains the password.
106
107mod types;
108pub use types::*;
109
110use std::time::Duration;
111
112use mssql_auth::Credentials;
113#[cfg(feature = "tls")]
114use mssql_tls::TlsConfig;
115use tds_protocol::version::TdsVersion;
116
117/// Parse a boolean value from a connection string keyword.
118///
119/// Per the ADO.NET specification, boolean keywords accept:
120/// `true`, `false`, `yes`, `no`, `1`, `0` (case-insensitive).
121/// Returns an error for any other value, preventing silent misconfiguration.
122fn parse_conn_bool(key: &str, value: &str) -> Result<bool, crate::error::Error> {
123    match value.to_lowercase().as_str() {
124        "true" | "yes" | "1" => Ok(true),
125        "false" | "no" | "0" => Ok(false),
126        _ => Err(crate::error::Error::Config(format!(
127            "invalid boolean value for '{key}': '{value}' (expected true/false/yes/no/1/0)"
128        ))),
129    }
130}
131
132/// Split a connection string into key-value pairs, respecting quoted values.
133///
134/// Per the ADO.NET specification:
135/// - Values containing semicolons must be enclosed in double (`"`) or single (`'`) quotes
136/// - Doubled quotes inside are escapes: `""` → `"`, `''` → `'`
137/// - Leading/trailing whitespace around values is trimmed (but preserved inside quotes)
138///
139/// Returns pairs of `(key, value)` where the value has quotes stripped and escapes resolved.
140fn split_connection_string(conn_str: &str) -> Result<Vec<(String, String)>, crate::error::Error> {
141    let mut pairs = Vec::new();
142    let chars: Vec<char> = conn_str.chars().collect();
143    let len = chars.len();
144    let mut i = 0;
145
146    while i < len {
147        // Skip whitespace and semicolons between pairs
148        while i < len && (chars[i] == ';' || chars[i].is_whitespace()) {
149            i += 1;
150        }
151        if i >= len {
152            break;
153        }
154
155        // Read key (up to '=')
156        let key_start = i;
157        while i < len && chars[i] != '=' {
158            i += 1;
159        }
160        if i >= len {
161            // Trailing text with no '=' — skip it (could be trailing whitespace)
162            let remaining = chars[key_start..].iter().collect::<String>();
163            if remaining.trim().is_empty() {
164                break;
165            }
166            return Err(crate::error::Error::Config(format!(
167                "invalid key-value pair (missing '='): '{remaining}'"
168            )));
169        }
170        let key: String = chars[key_start..i].iter().collect();
171        i += 1; // skip '='
172
173        // Read value — may be quoted or unquoted
174        // Skip leading whitespace in value
175        while i < len && chars[i].is_whitespace() {
176            i += 1;
177        }
178
179        let value = if i < len && (chars[i] == '"' || chars[i] == '\'') {
180            // Quoted value: read until matching unescaped closing quote
181            let quote_char = chars[i];
182            i += 1; // skip opening quote
183            let mut val = String::new();
184            loop {
185                if i >= len {
186                    return Err(crate::error::Error::Config(format!(
187                        "unterminated quoted value for key '{}'",
188                        key.trim()
189                    )));
190                }
191                if chars[i] == quote_char {
192                    // Check for escaped quote (doubled: "" or '')
193                    if i + 1 < len && chars[i + 1] == quote_char {
194                        val.push(quote_char);
195                        i += 2;
196                    } else {
197                        i += 1; // skip closing quote
198                        break;
199                    }
200                } else {
201                    val.push(chars[i]);
202                    i += 1;
203                }
204            }
205            // Skip to next semicolon or end
206            while i < len && chars[i] != ';' {
207                i += 1;
208            }
209            val
210        } else {
211            // Unquoted value: read until semicolon or end
212            let val_start = i;
213            while i < len && chars[i] != ';' {
214                i += 1;
215            }
216            chars[val_start..i].iter().collect::<String>()
217        };
218
219        let key_trimmed = key.trim().to_string();
220        if !key_trimmed.is_empty() {
221            pairs.push((key_trimmed, value));
222        }
223    }
224
225    Ok(pairs)
226}
227
228/// Convert a connection string value to `Option<String>`, treating empty strings as `None`.
229///
230/// In ADO.NET, specifying a keyword with an empty value (e.g., `Database=;`) resets it
231/// to its default. We represent this as `None` for optional fields.
232fn non_empty(value: &str) -> Option<String> {
233    if value.is_empty() {
234        None
235    } else {
236        Some(value.to_string())
237    }
238}
239
240/// Configuration for connecting to SQL Server.
241///
242/// This struct is marked `#[non_exhaustive]` to allow adding new fields
243/// in future releases without breaking semver. Use [`Config::default()`]
244/// or [`Config::from_connection_string()`] to construct instances.
245#[derive(Debug, Clone)]
246#[non_exhaustive]
247pub struct Config {
248    /// Server hostname or IP address.
249    pub host: String,
250
251    /// Server port (default: 1433).
252    pub port: u16,
253
254    /// Database name.
255    pub database: Option<String>,
256
257    /// Authentication credentials.
258    pub credentials: Credentials,
259
260    /// TLS configuration (only available when `tls` feature is enabled).
261    #[cfg(feature = "tls")]
262    pub tls: TlsConfig,
263
264    /// Application name (shown in SQL Server management tools).
265    pub application_name: String,
266
267    /// Connection timeout.
268    pub connect_timeout: Duration,
269
270    /// Command timeout.
271    pub command_timeout: Duration,
272
273    /// TDS packet size.
274    pub packet_size: u16,
275
276    /// Whether to use TDS 8.0 strict mode.
277    pub strict_mode: bool,
278
279    /// Whether to trust the server certificate.
280    pub trust_server_certificate: bool,
281
282    /// Instance name (for named instances).
283    pub instance: Option<String>,
284
285    /// Whether to enable MARS (Multiple Active Result Sets).
286    pub mars: bool,
287
288    /// Whether to require encryption (TLS).
289    /// When true, the connection will use TLS even if the server doesn't require it.
290    /// When false, encryption is used only if the server requires it.
291    pub encrypt: bool,
292
293    /// Disable TLS entirely and connect with plaintext.
294    ///
295    /// **⚠️ SECURITY WARNING:** This completely disables TLS/SSL encryption.
296    /// Credentials and data will be transmitted in plaintext. Only use this
297    /// for development/testing on trusted networks with legacy SQL Server
298    /// instances that don't support modern TLS versions.
299    ///
300    /// This option exists for compatibility with legacy SQL Server versions
301    /// (2008 and earlier) that may only support TLS 1.0/1.1, which modern
302    /// TLS libraries (like rustls) don't support for security reasons.
303    ///
304    /// When `true`:
305    /// - Overrides the `encrypt` setting
306    /// - Sends `ENCRYPT_NOT_SUP` in PreLogin
307    /// - No TLS handshake occurs
308    /// - All traffic including login credentials is unencrypted
309    ///
310    /// **Do not use in production without understanding the security implications.**
311    pub no_tls: bool,
312
313    /// Redirect handling configuration (for Azure SQL).
314    pub redirect: RedirectConfig,
315
316    /// Retry policy for transient error handling.
317    pub retry: RetryPolicy,
318
319    /// Timeout configuration for various connection phases.
320    pub timeouts: TimeoutConfig,
321
322    /// Requested TDS protocol version.
323    ///
324    /// This specifies which TDS protocol version to request during connection.
325    /// The server may negotiate a lower version if it doesn't support the requested version.
326    ///
327    /// Supported versions:
328    /// - `TdsVersion::V7_3A` - SQL Server 2008
329    /// - `TdsVersion::V7_3B` - SQL Server 2008 R2
330    /// - `TdsVersion::V7_4` - SQL Server 2012+ (default)
331    /// - `TdsVersion::V8_0` - SQL Server 2022+ strict mode (requires `strict_mode = true`)
332    ///
333    /// Note: When `strict_mode` is enabled, this is ignored and TDS 8.0 is used.
334    pub tds_version: TdsVersion,
335
336    /// Application workload intent for AlwaysOn Availability Group routing.
337    ///
338    /// When set to [`ApplicationIntent::ReadOnly`], SQL Server routes the
339    /// connection to a readable secondary replica. Sent in LOGIN7 TypeFlags
340    /// as the `READONLY_INTENT` bit.
341    pub application_intent: ApplicationIntent,
342
343    /// Client workstation name sent to SQL Server in the LOGIN7 HostName field.
344    ///
345    /// Used for auditing via `sys.dm_exec_sessions.host_name`.
346    /// When `None`, the driver sends the machine hostname (from the `COMPUTERNAME`
347    /// or `HOSTNAME` environment variable). Set via `Workstation ID` or `WSID`
348    /// in connection strings.
349    pub workstation_id: Option<String>,
350
351    /// Session language for server warning/error messages.
352    ///
353    /// When set, sent in LOGIN7's Language field. The language name can be
354    /// up to 128 characters. Set via `Language` or `Current Language` in
355    /// connection strings.
356    pub language: Option<String>,
357
358    /// Enable MultiSubnetFailover for AlwaysOn Availability Group listeners.
359    ///
360    /// When `true`, the driver resolves the server hostname to all IP addresses
361    /// and attempts parallel TCP connections simultaneously. The first successful
362    /// connection wins and all others are cancelled. This reduces connection time
363    /// when the AG listener spans multiple subnets.
364    ///
365    /// Set via `MultiSubnetFailover=True` in connection strings.
366    ///
367    /// Default: `false`
368    pub multi_subnet_failover: bool,
369
370    /// Whether to send `String`/`&str` parameters as NVARCHAR (Unicode).
371    ///
372    /// When `true` (default), string parameters are sent as NVARCHAR using
373    /// UTF-16LE encoding. This is safe for all character sets but prevents
374    /// SQL Server from using index seeks on VARCHAR columns (due to implicit
375    /// NVARCHAR→VARCHAR conversion).
376    ///
377    /// When `false`, string parameters are sent as VARCHAR using Windows-1252
378    /// encoding. This allows index seeks on VARCHAR columns but may lose data
379    /// for characters outside the Windows-1252 range.
380    ///
381    /// Set via `SendStringParametersAsUnicode=false` in connection strings.
382    ///
383    /// Default: `true`
384    pub send_string_parameters_as_unicode: bool,
385
386    /// Always Encrypted configuration.
387    ///
388    /// When `Some`, the client will negotiate Always Encrypted support with the
389    /// server and transparently decrypt encrypted column values in result sets.
390    ///
391    /// Set via `Column Encryption Setting=Enabled` in connection strings, or
392    /// programmatically via [`Config::with_column_encryption`].
393    ///
394    /// Wrapped in `Arc` because `EncryptionConfig` contains trait objects (key store
395    /// providers) which cannot implement `Clone`. The `Arc` allows `Config` to remain
396    /// `Clone` while sharing the encryption configuration.
397    #[cfg(feature = "always-encrypted")]
398    pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
399}
400
401impl Default for Config {
402    fn default() -> Self {
403        let timeouts = TimeoutConfig::default();
404        Self {
405            host: "localhost".to_string(),
406            port: 1433,
407            database: None,
408            credentials: Credentials::sql_server("", ""),
409            #[cfg(feature = "tls")]
410            tls: TlsConfig::default(),
411            application_name: "mssql-client".to_string(),
412            connect_timeout: timeouts.connect_timeout,
413            command_timeout: timeouts.command_timeout,
414            packet_size: 4096,
415            strict_mode: false,
416            trust_server_certificate: false,
417            instance: None,
418            mars: false,
419            encrypt: true, // Default to encrypted for security
420            no_tls: false, // Never plaintext by default
421            redirect: RedirectConfig::default(),
422            retry: RetryPolicy::default(),
423            timeouts,
424            tds_version: TdsVersion::V7_4, // Default to TDS 7.4 for broad compatibility
425            application_intent: ApplicationIntent::default(),
426            workstation_id: None,
427            language: None,
428            multi_subnet_failover: false,
429            send_string_parameters_as_unicode: true,
430            #[cfg(feature = "always-encrypted")]
431            column_encryption: None,
432        }
433    }
434}
435
436impl Config {
437    /// Create a new configuration with default values.
438    #[must_use]
439    pub fn new() -> Self {
440        Self::default()
441    }
442
443    /// Parse a connection string into configuration.
444    ///
445    /// Supports ADO.NET-style connection strings with full quoting support:
446    /// ```text
447    /// Server=localhost;Database=mydb;User Id=sa;Password="complex;pass";
448    /// ```
449    ///
450    /// Values containing semicolons can be enclosed in double or single quotes
451    /// per the ADO.NET specification. The `tcp:` prefix from Azure Portal
452    /// connection strings is automatically stripped.
453    pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
454        let mut config = Self::default();
455        let pairs = split_connection_string(conn_str)?;
456
457        for (key, value) in &pairs {
458            let key = key.trim().to_lowercase();
459            let value = value.trim();
460
461            match key.as_str() {
462                // --- Server / Data Source (ADO.NET aliases: Addr, Address, Network Address) ---
463                "server" | "data source" | "addr" | "address" | "network address" | "host" => {
464                    // Strip tcp: prefix (common in Azure Portal connection strings).
465                    // Reject np: (Named Pipes) and lpc: (Shared Memory) — not supported.
466                    // All prefix checks are case-insensitive per ADO.NET conventions.
467                    let lower_value = value.to_lowercase();
468                    let server_value = if lower_value.starts_with("tcp:") {
469                        &value[4..]
470                    } else if lower_value.starts_with("np:") {
471                        return Err(crate::error::Error::Config(
472                            "Named Pipes connections (np:) are not supported. Use TCP connections instead."
473                                .into(),
474                        ));
475                    } else if lower_value.starts_with("lpc:") {
476                        return Err(crate::error::Error::Config(
477                            "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
478                                .into(),
479                        ));
480                    } else {
481                        value
482                    };
483
484                    // Handle host,port or host\instance format
485                    if let Some((host, port_or_instance)) = server_value.split_once(',') {
486                        config.host = host.to_string();
487                        config.port = port_or_instance.trim().parse().map_err(|_| {
488                            crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
489                        })?;
490                    } else if let Some((host, instance)) = server_value.split_once('\\') {
491                        config.host = host.to_string();
492                        config.instance = non_empty(instance);
493                    } else {
494                        config.host = server_value.to_string();
495                    }
496                }
497                "port" => {
498                    config.port = value.parse().map_err(|_| {
499                        crate::error::Error::Config(format!("invalid port: {value}"))
500                    })?;
501                }
502                // --- Database ---
503                "database" | "initial catalog" => {
504                    config.database = non_empty(value);
505                }
506                // --- Credentials ---
507                "user id" | "uid" | "user" => {
508                    if let Credentials::SqlServer { password, .. } = &config.credentials {
509                        config.credentials =
510                            Credentials::sql_server(value.to_string(), password.clone());
511                    }
512                }
513                "password" | "pwd" => {
514                    if let Credentials::SqlServer { username, .. } = &config.credentials {
515                        config.credentials =
516                            Credentials::sql_server(username.clone(), value.to_string());
517                    }
518                }
519                // --- Application ---
520                "application name" | "app" => {
521                    config.application_name = value.to_string();
522                }
523                "applicationintent" | "application intent" => {
524                    config.application_intent = match value.to_lowercase().as_str() {
525                        "readonly" => ApplicationIntent::ReadOnly,
526                        "readwrite" => ApplicationIntent::ReadWrite,
527                        _ => {
528                            return Err(crate::error::Error::Config(format!(
529                                "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
530                            )));
531                        }
532                    };
533                }
534                "workstation id" | "wsid" => {
535                    config.workstation_id = non_empty(value);
536                }
537                "current language" | "language" => {
538                    config.language = non_empty(value);
539                }
540                // --- Timeouts (ADO.NET alias: Timeout) ---
541                "connect timeout" | "connection timeout" | "timeout" => {
542                    let secs: u64 = value.parse().map_err(|_| {
543                        crate::error::Error::Config(format!("invalid timeout: {value}"))
544                    })?;
545                    config.connect_timeout = Duration::from_secs(secs);
546                }
547                "command timeout" => {
548                    let secs: u64 = value.parse().map_err(|_| {
549                        crate::error::Error::Config(format!("invalid timeout: {value}"))
550                    })?;
551                    config.command_timeout = Duration::from_secs(secs);
552                }
553                // --- Security ---
554                "trustservercertificate" | "trust server certificate" => {
555                    config.trust_server_certificate = parse_conn_bool(&key, value)?;
556                }
557                "encrypt" => {
558                    // Encrypt supports several non-boolean values beyond true/false:
559                    // - "strict" = TDS 8.0 strict mode (always encrypted transport)
560                    // - "mandatory" / "true" / "yes" / "1" = require TLS
561                    // - "optional" / "false" / "no" / "0" = TLS only if server requires
562                    // - "no_tls" = Tiberius-compatible plaintext mode for legacy servers
563                    //
564                    // "mandatory" and "optional" are Microsoft.Data.SqlClient v5+ aliases.
565                    if value.eq_ignore_ascii_case("strict") {
566                        config.strict_mode = true;
567                        config.encrypt = true;
568                        config.no_tls = false;
569                    } else if value.eq_ignore_ascii_case("mandatory") {
570                        config.encrypt = true;
571                        config.no_tls = false;
572                    } else if value.eq_ignore_ascii_case("optional") {
573                        config.encrypt = false;
574                        config.no_tls = false;
575                    } else if value.eq_ignore_ascii_case("no_tls") {
576                        config.no_tls = true;
577                        config.encrypt = false;
578                    } else {
579                        // Standard boolean values (true/false/yes/no/1/0)
580                        let enabled = parse_conn_bool(&key, value)?;
581                        config.encrypt = enabled;
582                        config.no_tls = false;
583                    }
584                }
585                "integrated security" | "trusted_connection" => {
586                    // Accepts standard booleans + "sspi" (ADO.NET strongly-recommended value)
587                    let enabled =
588                        value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
589                    if enabled {
590                        #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
591                        {
592                            config.credentials = Credentials::Integrated;
593                        }
594                        #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
595                        {
596                            return Err(crate::error::Error::Config(
597                                "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
598                                 or 'sspi-auth' (Windows) feature to be enabled"
599                                    .into(),
600                            ));
601                        }
602                    }
603                }
604                // --- Always Encrypted ---
605                "column encryption setting" | "columnencryptionsetting" => {
606                    #[cfg(feature = "always-encrypted")]
607                    if value.eq_ignore_ascii_case("enabled") {
608                        config.column_encryption = Some(std::sync::Arc::new(
609                            crate::encryption::EncryptionConfig::new(),
610                        ));
611                    }
612                    #[cfg(not(feature = "always-encrypted"))]
613                    if value.eq_ignore_ascii_case("enabled") {
614                        return Err(crate::error::Error::Config(
615                            "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
616                             Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
617                                .to_string(),
618                        ));
619                    }
620                }
621                // --- Protocol ---
622                "multipleactiveresultsets" | "mars" => {
623                    config.mars = parse_conn_bool(&key, value)?;
624                }
625                "packet size" => {
626                    config.packet_size = value.parse().map_err(|_| {
627                        crate::error::Error::Config(format!("invalid packet size: {value}"))
628                    })?;
629                }
630                "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
631                    config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
632                        crate::error::Error::Config(format!(
633                            "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
634                        ))
635                    })?;
636                    if config.tds_version.is_tds_8() {
637                        config.strict_mode = true;
638                    }
639                }
640                // --- Connection resiliency ---
641                "connectretrycount" | "connect retry count" => {
642                    config.retry.max_retries = value.parse().map_err(|_| {
643                        crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
644                    })?;
645                }
646                "connectretryinterval" | "connect retry interval" => {
647                    let secs: u64 = value.parse().map_err(|_| {
648                        crate::error::Error::Config(format!(
649                            "invalid ConnectRetryInterval: '{value}'"
650                        ))
651                    })?;
652                    config.retry.initial_backoff = Duration::from_secs(secs);
653                }
654                // --- Pool keywords: recognized but must be set via PoolConfig ---
655                "max pool size"
656                | "min pool size"
657                | "pooling"
658                | "connection lifetime"
659                | "load balance timeout" => {
660                    tracing::info!(
661                        key = key.as_str(),
662                        value = value,
663                        "connection string keyword '{}' is recognized but pool settings \
664                         must be configured via PoolConfig, not the connection string",
665                        key,
666                    );
667                }
668                // --- MultiSubnetFailover ---
669                "multisubnetfailover" | "multi subnet failover" => {
670                    config.multi_subnet_failover = parse_conn_bool(&key, value)?;
671                }
672                // --- String parameter encoding ---
673                "sendstringparametersasunicode" | "send string parameters as unicode" => {
674                    config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
675                }
676                // --- Known ADO.NET keywords not supported by this driver ---
677                "failover partner"
678                | "persist security info"
679                | "persistsecurityinfo"
680                | "enlist"
681                | "replication"
682                | "transaction binding"
683                | "type system version"
684                | "user instance"
685                | "attachdbfilename"
686                | "extended properties"
687                | "initial file name"
688                | "context connection"
689                | "network library"
690                | "network"
691                | "net"
692                | "asynchronous processing"
693                | "async"
694                | "transparentnetworkipresolution"
695                | "poolblockingperiod"
696                | "authentication"
697                | "hostnameincertificate"
698                | "servercertificate" => {
699                    tracing::info!(
700                        key = key.as_str(),
701                        value = value,
702                        "connection string keyword '{}' is recognized but not supported by this driver",
703                        key,
704                    );
705                }
706                _ => {
707                    tracing::debug!(
708                        key = key.as_str(),
709                        value = value,
710                        "ignoring unknown connection string option"
711                    );
712                }
713            }
714        }
715
716        Ok(config)
717    }
718
719    /// Set the server host.
720    #[must_use]
721    pub fn host(mut self, host: impl Into<String>) -> Self {
722        self.host = host.into();
723        self
724    }
725
726    /// Set the server port.
727    #[must_use]
728    pub fn port(mut self, port: u16) -> Self {
729        self.port = port;
730        self
731    }
732
733    /// Set the database name.
734    #[must_use]
735    pub fn database(mut self, database: impl Into<String>) -> Self {
736        self.database = Some(database.into());
737        self
738    }
739
740    /// Set the credentials.
741    #[must_use]
742    pub fn credentials(mut self, credentials: Credentials) -> Self {
743        self.credentials = credentials;
744        self
745    }
746
747    /// Set the application name.
748    #[must_use]
749    pub fn application_name(mut self, name: impl Into<String>) -> Self {
750        self.application_name = name.into();
751        self
752    }
753
754    /// Set the connect timeout.
755    #[must_use]
756    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
757        self.connect_timeout = timeout;
758        self
759    }
760
761    /// Set trust server certificate option.
762    #[must_use]
763    pub fn trust_server_certificate(mut self, trust: bool) -> Self {
764        self.trust_server_certificate = trust;
765        #[cfg(feature = "tls")]
766        {
767            self.tls = self.tls.trust_server_certificate(trust);
768        }
769        self
770    }
771
772    /// Enable TDS 8.0 strict mode.
773    #[must_use]
774    pub fn strict_mode(mut self, enabled: bool) -> Self {
775        self.strict_mode = enabled;
776        #[cfg(feature = "tls")]
777        {
778            self.tls = self.tls.strict_mode(enabled);
779        }
780        if enabled {
781            self.tds_version = TdsVersion::V8_0;
782        }
783        self
784    }
785
786    /// Set the TDS protocol version.
787    ///
788    /// This specifies which TDS protocol version to request during connection.
789    /// The server may negotiate a lower version if it doesn't support the requested version.
790    ///
791    /// # Examples
792    ///
793    /// ```no_run
794    /// use mssql_client::Config;
795    /// use tds_protocol::version::TdsVersion;
796    ///
797    /// // Connect to SQL Server 2008
798    /// let config = Config::new()
799    ///     .host("legacy-server")
800    ///     .tds_version(TdsVersion::V7_3A);
801    ///
802    /// // Connect to SQL Server 2008 R2
803    /// let config = Config::new()
804    ///     .host("legacy-server")
805    ///     .tds_version(TdsVersion::V7_3B);
806    /// ```
807    ///
808    /// Note: When `strict_mode` is enabled, this is ignored and TDS 8.0 is used.
809    #[must_use]
810    pub fn tds_version(mut self, version: TdsVersion) -> Self {
811        self.tds_version = version;
812        // If TDS 8.0 is requested, automatically enable strict mode
813        if version.is_tds_8() {
814            self.strict_mode = true;
815            #[cfg(feature = "tls")]
816            {
817                self.tls = self.tls.strict_mode(true);
818            }
819        }
820        self
821    }
822
823    /// Enable or disable TLS encryption.
824    ///
825    /// When `true` (default), the connection will use TLS encryption.
826    /// When `false`, encryption is used only if the server requires it.
827    ///
828    /// **Warning:** Disabling encryption is insecure and should only be
829    /// used for development/testing on trusted networks.
830    #[must_use]
831    pub fn encrypt(mut self, enabled: bool) -> Self {
832        self.encrypt = enabled;
833        self
834    }
835
836    /// Disable TLS entirely and connect with plaintext (Tiberius-compatible).
837    ///
838    /// **⚠️ SECURITY WARNING:** This completely disables TLS/SSL encryption.
839    /// Credentials and all data will be transmitted in plaintext over the network.
840    ///
841    /// # When to use this
842    ///
843    /// This option exists for compatibility with legacy SQL Server versions
844    /// (2008 and earlier) that may only support TLS 1.0/1.1. Modern TLS libraries
845    /// like rustls require TLS 1.2 or higher for security reasons, making it
846    /// impossible to establish encrypted connections to these older servers.
847    ///
848    /// # Security implications
849    ///
850    /// When enabled:
851    /// - Login credentials are sent in plaintext
852    /// - All query data is transmitted without encryption
853    /// - Network traffic can be intercepted and read by attackers
854    ///
855    /// **Only use this for development/testing on isolated, trusted networks.**
856    ///
857    /// # Example
858    ///
859    /// ```rust,no_run
860    /// # fn ex() -> Result<(), mssql_client::Error> {
861    /// use mssql_client::Config;
862    ///
863    /// // Connection string (Tiberius-compatible)
864    /// let config = Config::from_connection_string(
865    ///     "Server=legacy-server;User Id=sa;Password=secret;Encrypt=no_tls"
866    /// )?;
867    ///
868    /// // Builder API
869    /// let config = Config::new()
870    ///     .host("legacy-server")
871    ///     .no_tls(true);
872    /// # let _ = config;
873    /// # Ok(())
874    /// # }
875    /// ```
876    #[must_use]
877    pub fn no_tls(mut self, enabled: bool) -> Self {
878        self.no_tls = enabled;
879        if enabled {
880            self.encrypt = false;
881        }
882        self
883    }
884
885    /// Enable Always Encrypted with the given encryption configuration.
886    ///
887    /// When enabled, the client will negotiate Always Encrypted support during
888    /// connection and transparently decrypt encrypted column values.
889    ///
890    /// # Example
891    ///
892    /// ```rust,no_run
893    /// # #[cfg(feature = "always-encrypted")]
894    /// # fn ex() {
895    /// use mssql_client::{Config, EncryptionConfig};
896    /// use mssql_auth::InMemoryKeyStore;
897    ///
898    /// let key_store = InMemoryKeyStore::new();
899    /// let config = Config::new()
900    ///     .with_column_encryption(
901    ///         EncryptionConfig::new().with_provider(key_store)
902    ///     );
903    /// # let _ = config;
904    /// # }
905    /// ```
906    #[cfg(feature = "always-encrypted")]
907    #[must_use]
908    pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
909        self.column_encryption = Some(std::sync::Arc::new(config));
910        self
911    }
912
913    /// Create a new configuration with a different host (for routing).
914    #[must_use]
915    pub fn with_host(mut self, host: &str) -> Self {
916        self.host = host.to_string();
917        self
918    }
919
920    /// Create a new configuration with a different port (for routing).
921    #[must_use]
922    pub fn with_port(mut self, port: u16) -> Self {
923        self.port = port;
924        self
925    }
926
927    /// Set the redirect handling configuration.
928    #[must_use]
929    pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
930        self.redirect = redirect;
931        self
932    }
933
934    /// Set the maximum number of redirect attempts.
935    #[must_use]
936    pub fn max_redirects(mut self, max: u8) -> Self {
937        self.redirect.max_redirects = max;
938        self
939    }
940
941    /// Set the retry policy for transient error handling.
942    #[must_use]
943    pub fn retry(mut self, retry: RetryPolicy) -> Self {
944        self.retry = retry;
945        self
946    }
947
948    /// Set the maximum number of retry attempts.
949    #[must_use]
950    pub fn max_retries(mut self, max: u32) -> Self {
951        self.retry.max_retries = max;
952        self
953    }
954
955    /// Set the timeout configuration.
956    #[must_use]
957    pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
958        // Sync the legacy fields for backward compatibility first
959        self.connect_timeout = timeouts.connect_timeout;
960        self.command_timeout = timeouts.command_timeout;
961        self.timeouts = timeouts;
962        self
963    }
964
965    /// Set the application workload intent for AlwaysOn AG routing.
966    #[must_use]
967    pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
968        self.application_intent = intent;
969        self
970    }
971
972    /// Set the client workstation name sent to SQL Server in LOGIN7.
973    ///
974    /// This appears in `sys.dm_exec_sessions.host_name` for auditing.
975    /// When not set, the driver sends the machine hostname automatically.
976    #[must_use]
977    pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
978        self.workstation_id = Some(id.into());
979        self
980    }
981
982    /// Set the session language for server messages.
983    ///
984    /// The language name can be up to 128 characters (e.g., `"us_english"`).
985    #[must_use]
986    pub fn language(mut self, lang: impl Into<String>) -> Self {
987        self.language = Some(lang.into());
988        self
989    }
990
991    /// Enable MultiSubnetFailover for AlwaysOn Availability Group listeners.
992    ///
993    /// When enabled, the driver resolves the server hostname to all IP addresses
994    /// and races parallel TCP connections. The first successful connection wins.
995    #[must_use]
996    pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
997        self.multi_subnet_failover = enabled;
998        self
999    }
1000
1001    /// Control whether string parameters are sent as NVARCHAR (Unicode) or VARCHAR.
1002    ///
1003    /// When `false`, `String`/`&str` parameters are sent as VARCHAR using
1004    /// Windows-1252 encoding, which allows SQL Server to use index seeks on
1005    /// VARCHAR columns.
1006    ///
1007    /// Default: `true` (NVARCHAR)
1008    #[must_use]
1009    pub fn send_string_parameters_as_unicode(mut self, enabled: bool) -> Self {
1010        self.send_string_parameters_as_unicode = enabled;
1011        self
1012    }
1013}
1014
1015#[cfg(test)]
1016#[allow(clippy::unwrap_used)]
1017mod tests {
1018    use super::*;
1019
1020    #[test]
1021    fn test_connection_string_parsing() {
1022        let config = Config::from_connection_string(
1023            "Server=localhost;Database=test;User Id=sa;Password=secret;",
1024        )
1025        .unwrap();
1026
1027        assert_eq!(config.host, "localhost");
1028        assert_eq!(config.database, Some("test".to_string()));
1029    }
1030
1031    #[test]
1032    fn test_connection_string_with_port() {
1033        let config =
1034            Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
1035
1036        assert_eq!(config.host, "localhost");
1037        assert_eq!(config.port, 1434);
1038    }
1039
1040    #[test]
1041    fn test_connection_string_with_instance() {
1042        let config =
1043            Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
1044
1045        assert_eq!(config.host, "localhost");
1046        assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1047    }
1048
1049    #[test]
1050    fn test_connection_string_dot_instance() {
1051        // "." is a standard ADO.NET alias for localhost
1052        let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
1053
1054        assert_eq!(config.host, ".");
1055        assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1056    }
1057
1058    #[test]
1059    fn test_connection_string_local_instance() {
1060        // "(local)" is a standard ADO.NET alias for localhost
1061        let config =
1062            Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
1063
1064        assert_eq!(config.host, "(local)");
1065        assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1066    }
1067
1068    #[test]
1069    fn test_redirect_config_defaults() {
1070        let config = RedirectConfig::default();
1071        assert_eq!(config.max_redirects, 2);
1072        assert!(config.follow_redirects);
1073    }
1074
1075    #[test]
1076    fn test_redirect_config_builder() {
1077        let config = RedirectConfig::new()
1078            .max_redirects(5)
1079            .follow_redirects(false);
1080        assert_eq!(config.max_redirects, 5);
1081        assert!(!config.follow_redirects);
1082    }
1083
1084    #[test]
1085    fn test_redirect_config_no_follow() {
1086        let config = RedirectConfig::no_follow();
1087        assert_eq!(config.max_redirects, 0);
1088        assert!(!config.follow_redirects);
1089    }
1090
1091    #[test]
1092    fn test_config_redirect_builder() {
1093        let config = Config::new().max_redirects(3);
1094        assert_eq!(config.redirect.max_redirects, 3);
1095
1096        let config2 = Config::new().redirect(RedirectConfig::no_follow());
1097        assert!(!config2.redirect.follow_redirects);
1098    }
1099
1100    #[test]
1101    fn test_retry_policy_defaults() {
1102        let policy = RetryPolicy::default();
1103        assert_eq!(policy.max_retries, 3);
1104        assert_eq!(policy.initial_backoff, Duration::from_millis(100));
1105        assert_eq!(policy.max_backoff, Duration::from_secs(30));
1106        assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
1107        assert!(policy.jitter);
1108    }
1109
1110    #[test]
1111    fn test_retry_policy_builder() {
1112        let policy = RetryPolicy::new()
1113            .max_retries(5)
1114            .initial_backoff(Duration::from_millis(200))
1115            .max_backoff(Duration::from_secs(60))
1116            .backoff_multiplier(3.0)
1117            .jitter(false);
1118
1119        assert_eq!(policy.max_retries, 5);
1120        assert_eq!(policy.initial_backoff, Duration::from_millis(200));
1121        assert_eq!(policy.max_backoff, Duration::from_secs(60));
1122        assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
1123        assert!(!policy.jitter);
1124    }
1125
1126    #[test]
1127    fn test_retry_policy_no_retry() {
1128        let policy = RetryPolicy::no_retry();
1129        assert_eq!(policy.max_retries, 0);
1130        assert!(!policy.should_retry(0));
1131    }
1132
1133    #[test]
1134    fn test_retry_policy_should_retry() {
1135        let policy = RetryPolicy::new().max_retries(3);
1136        assert!(policy.should_retry(0));
1137        assert!(policy.should_retry(1));
1138        assert!(policy.should_retry(2));
1139        assert!(!policy.should_retry(3));
1140        assert!(!policy.should_retry(4));
1141    }
1142
1143    #[test]
1144    fn test_retry_policy_backoff_calculation() {
1145        let policy = RetryPolicy::new()
1146            .initial_backoff(Duration::from_millis(100))
1147            .backoff_multiplier(2.0)
1148            .max_backoff(Duration::from_secs(10))
1149            .jitter(false);
1150
1151        assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
1152        assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
1153        assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
1154        assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
1155    }
1156
1157    #[test]
1158    fn test_retry_policy_backoff_capped() {
1159        let policy = RetryPolicy::new()
1160            .initial_backoff(Duration::from_secs(1))
1161            .backoff_multiplier(10.0)
1162            .max_backoff(Duration::from_secs(5))
1163            .jitter(false);
1164
1165        // Attempt 3 would be 1s * 10^2 = 100s, but capped at 5s
1166        assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
1167    }
1168
1169    #[test]
1170    fn test_config_retry_builder() {
1171        let config = Config::new().max_retries(5);
1172        assert_eq!(config.retry.max_retries, 5);
1173
1174        let config2 = Config::new().retry(RetryPolicy::no_retry());
1175        assert_eq!(config2.retry.max_retries, 0);
1176    }
1177
1178    #[test]
1179    fn test_timeout_config_defaults() {
1180        let config = TimeoutConfig::default();
1181        assert_eq!(config.connect_timeout, Duration::from_secs(15));
1182        assert_eq!(config.tls_timeout, Duration::from_secs(10));
1183        assert_eq!(config.login_timeout, Duration::from_secs(30));
1184        assert_eq!(config.command_timeout, Duration::from_secs(30));
1185        assert_eq!(config.idle_timeout, Duration::from_secs(300));
1186        assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1187    }
1188
1189    #[test]
1190    fn test_timeout_config_builder() {
1191        let config = TimeoutConfig::new()
1192            .connect_timeout(Duration::from_secs(5))
1193            .tls_timeout(Duration::from_secs(3))
1194            .login_timeout(Duration::from_secs(10))
1195            .command_timeout(Duration::from_secs(60))
1196            .idle_timeout(Duration::from_secs(600))
1197            .keepalive_interval(Some(Duration::from_secs(60)));
1198
1199        assert_eq!(config.connect_timeout, Duration::from_secs(5));
1200        assert_eq!(config.tls_timeout, Duration::from_secs(3));
1201        assert_eq!(config.login_timeout, Duration::from_secs(10));
1202        assert_eq!(config.command_timeout, Duration::from_secs(60));
1203        assert_eq!(config.idle_timeout, Duration::from_secs(600));
1204        assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1205    }
1206
1207    #[test]
1208    fn test_timeout_config_no_keepalive() {
1209        let config = TimeoutConfig::new().no_keepalive();
1210        assert_eq!(config.keepalive_interval, None);
1211    }
1212
1213    #[test]
1214    fn test_timeout_config_total_connect() {
1215        let config = TimeoutConfig::new()
1216            .connect_timeout(Duration::from_secs(5))
1217            .tls_timeout(Duration::from_secs(3))
1218            .login_timeout(Duration::from_secs(10));
1219
1220        // 5 + 3 + 10 = 18 seconds
1221        assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1222    }
1223
1224    #[test]
1225    fn test_config_timeouts_builder() {
1226        let timeouts = TimeoutConfig::new()
1227            .connect_timeout(Duration::from_secs(5))
1228            .command_timeout(Duration::from_secs(60));
1229
1230        let config = Config::new().timeouts(timeouts);
1231        assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1232        assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1233        // Check that legacy fields are synced
1234        assert_eq!(config.connect_timeout, Duration::from_secs(5));
1235        assert_eq!(config.command_timeout, Duration::from_secs(60));
1236    }
1237
1238    #[test]
1239    fn test_tds_version_default() {
1240        let config = Config::default();
1241        assert_eq!(config.tds_version, TdsVersion::V7_4);
1242        assert!(!config.strict_mode);
1243    }
1244
1245    #[test]
1246    fn test_tds_version_builder() {
1247        let config = Config::new().tds_version(TdsVersion::V7_3A);
1248        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1249        assert!(!config.strict_mode);
1250
1251        let config = Config::new().tds_version(TdsVersion::V7_3B);
1252        assert_eq!(config.tds_version, TdsVersion::V7_3B);
1253        assert!(!config.strict_mode);
1254
1255        // TDS 8.0 should automatically enable strict mode
1256        let config = Config::new().tds_version(TdsVersion::V8_0);
1257        assert_eq!(config.tds_version, TdsVersion::V8_0);
1258        assert!(config.strict_mode);
1259    }
1260
1261    #[test]
1262    fn test_strict_mode_sets_tds_8() {
1263        let config = Config::new().strict_mode(true);
1264        assert!(config.strict_mode);
1265        assert_eq!(config.tds_version, TdsVersion::V8_0);
1266    }
1267
1268    #[test]
1269    fn test_connection_string_tds_version() {
1270        // Test TDS 7.3
1271        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1272        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1273
1274        // Test TDS 7.3A explicitly
1275        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1276        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1277
1278        // Test TDS 7.3B
1279        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1280        assert_eq!(config.tds_version, TdsVersion::V7_3B);
1281
1282        // Test TDS 7.4
1283        let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1284        assert_eq!(config.tds_version, TdsVersion::V7_4);
1285
1286        // Test TDS 8.0 enables strict mode
1287        let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1288        assert_eq!(config.tds_version, TdsVersion::V8_0);
1289        assert!(config.strict_mode);
1290
1291        // Test alternative key names
1292        let config =
1293            Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1294        assert_eq!(config.tds_version, TdsVersion::V7_3A);
1295    }
1296
1297    #[test]
1298    fn test_connection_string_invalid_tds_version() {
1299        let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1300        assert!(result.is_err());
1301
1302        let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1303        assert!(result.is_err());
1304    }
1305
1306    #[test]
1307    fn test_connection_string_no_tls() {
1308        // no_tls should disable TLS entirely
1309        let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1310        assert!(config.no_tls);
1311        assert!(!config.encrypt);
1312        assert!(!config.strict_mode);
1313
1314        // Case insensitive
1315        let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1316        assert!(config.no_tls);
1317
1318        // Encrypt=true should disable no_tls
1319        let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1320        assert!(!config.no_tls);
1321        assert!(config.encrypt);
1322
1323        // Encrypt=strict should disable no_tls
1324        let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1325        assert!(!config.no_tls);
1326        assert!(config.encrypt);
1327        assert!(config.strict_mode);
1328
1329        // Encrypt=mandatory (Microsoft.Data.SqlClient v5+ alias for true)
1330        let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1331        assert!(config.encrypt);
1332        assert!(!config.no_tls);
1333
1334        // Encrypt=optional (Microsoft.Data.SqlClient v5+ alias for false)
1335        let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1336        assert!(!config.encrypt);
1337        assert!(!config.no_tls);
1338    }
1339
1340    #[test]
1341    fn test_no_tls_builder() {
1342        // Builder method
1343        let config = Config::new().no_tls(true);
1344        assert!(config.no_tls);
1345        assert!(!config.encrypt);
1346
1347        // Disable
1348        let config = Config::new().no_tls(true).no_tls(false);
1349        assert!(!config.no_tls);
1350    }
1351
1352    #[test]
1353    #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1354    fn test_connection_string_integrated_security() {
1355        // "Integrated Security=true" should set Credentials::Integrated
1356        let config =
1357            Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1358        assert_eq!(
1359            config.credentials.method_name(),
1360            "Integrated Authentication"
1361        );
1362
1363        // "yes" variant
1364        let config =
1365            Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1366        assert_eq!(
1367            config.credentials.method_name(),
1368            "Integrated Authentication"
1369        );
1370
1371        // "sspi" variant
1372        let config =
1373            Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1374        assert_eq!(
1375            config.credentials.method_name(),
1376            "Integrated Authentication"
1377        );
1378
1379        // "1" variant
1380        let config =
1381            Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1382        assert_eq!(
1383            config.credentials.method_name(),
1384            "Integrated Authentication"
1385        );
1386
1387        // Trusted_Connection synonym
1388        let config =
1389            Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1390        assert_eq!(
1391            config.credentials.method_name(),
1392            "Integrated Authentication"
1393        );
1394    }
1395
1396    #[test]
1397    #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1398    fn test_connection_string_integrated_security_without_feature() {
1399        // Should return an error when the feature is not enabled
1400        let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1401        assert!(result.is_err());
1402        let err = result.unwrap_err().to_string();
1403        assert!(err.contains("integrated-auth"));
1404    }
1405
1406    // =======================================================================
1407    // ADO.NET conformance tests (quoted values, aliases, boolean validation)
1408    // =======================================================================
1409
1410    #[test]
1411    fn test_parse_conn_bool_all_values() {
1412        assert!(parse_conn_bool("test", "true").unwrap());
1413        assert!(parse_conn_bool("test", "True").unwrap());
1414        assert!(parse_conn_bool("test", "TRUE").unwrap());
1415        assert!(parse_conn_bool("test", "yes").unwrap());
1416        assert!(parse_conn_bool("test", "Yes").unwrap());
1417        assert!(parse_conn_bool("test", "1").unwrap());
1418
1419        assert!(!parse_conn_bool("test", "false").unwrap());
1420        assert!(!parse_conn_bool("test", "False").unwrap());
1421        assert!(!parse_conn_bool("test", "FALSE").unwrap());
1422        assert!(!parse_conn_bool("test", "no").unwrap());
1423        assert!(!parse_conn_bool("test", "No").unwrap());
1424        assert!(!parse_conn_bool("test", "0").unwrap());
1425
1426        // Invalid values should error
1427        assert!(parse_conn_bool("test", "banana").is_err());
1428        assert!(parse_conn_bool("test", "tru").is_err());
1429        assert!(parse_conn_bool("test", "").is_err());
1430    }
1431
1432    #[test]
1433    fn test_boolean_validation_trust_server_certificate() {
1434        // Valid boolean → ok
1435        let config =
1436            Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1437                .unwrap();
1438        assert!(config.trust_server_certificate);
1439
1440        let config =
1441            Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1442        assert!(!config.trust_server_certificate);
1443
1444        // Invalid boolean → error (previously silently set to false!)
1445        let result =
1446            Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1447        assert!(result.is_err());
1448        assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1449    }
1450
1451    #[test]
1452    fn test_boolean_validation_mars() {
1453        let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1454        assert!(config.mars);
1455
1456        // Typo → error instead of silent false
1457        let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1458        assert!(result.is_err());
1459    }
1460
1461    #[test]
1462    fn test_quoted_value_semicolon() {
1463        // Password with semicolons — must be quoted per ADO.NET spec
1464        let config = Config::from_connection_string(
1465            r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1466        )
1467        .unwrap();
1468        if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1469            assert_eq!(password.as_ref(), "my;complex;pass");
1470        } else {
1471            unreachable!("expected SqlServer credentials");
1472        }
1473    }
1474
1475    #[test]
1476    fn test_quoted_value_single_quotes() {
1477        let config =
1478            Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1479                .unwrap();
1480        if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1481            assert_eq!(password.as_ref(), "my;pass");
1482        } else {
1483            unreachable!("expected SqlServer credentials");
1484        }
1485    }
1486
1487    #[test]
1488    fn test_quoted_value_escaped_double_quotes() {
1489        // Doubled quotes → single quote per ADO.NET spec
1490        let config = Config::from_connection_string(
1491            r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1492        )
1493        .unwrap();
1494        if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1495            assert_eq!(password.as_ref(), r#"has "quotes""#);
1496        } else {
1497            unreachable!("expected SqlServer credentials");
1498        }
1499    }
1500
1501    #[test]
1502    fn test_quoted_value_escaped_single_quotes() {
1503        let config =
1504            Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1505                .unwrap();
1506        if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1507            assert_eq!(password.as_ref(), "it's complex");
1508        } else {
1509            unreachable!("expected SqlServer credentials");
1510        }
1511    }
1512
1513    #[test]
1514    fn test_quoted_value_unterminated() {
1515        let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1516        assert!(result.is_err());
1517        assert!(result.unwrap_err().to_string().contains("unterminated"));
1518    }
1519
1520    #[test]
1521    fn test_tcp_prefix_stripped() {
1522        // Azure Portal format: tcp:hostname,port
1523        let config = Config::from_connection_string(
1524            "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1525        )
1526        .unwrap();
1527        assert_eq!(config.host, "myserver.database.windows.net");
1528        assert_eq!(config.port, 1433);
1529    }
1530
1531    #[test]
1532    fn test_tcp_prefix_mixed_case() {
1533        // Protocol prefixes are case-insensitive per ADO.NET
1534        let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1535        assert_eq!(config.host, "myhost");
1536
1537        let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1538        assert_eq!(config.host, "myhost");
1539    }
1540
1541    #[test]
1542    fn test_tcp_prefix_with_instance() {
1543        let config =
1544            Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1545        assert_eq!(config.host, "myhost");
1546        assert_eq!(config.instance, Some("INST".to_string()));
1547    }
1548
1549    #[test]
1550    fn test_np_prefix_rejected() {
1551        let result =
1552            Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1553        assert!(result.is_err());
1554        assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1555
1556        // Case-insensitive rejection
1557        let result =
1558            Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1559        assert!(result.is_err());
1560    }
1561
1562    #[test]
1563    fn test_lpc_prefix_rejected() {
1564        let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1565        assert!(result.is_err());
1566        assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1567    }
1568
1569    #[test]
1570    fn test_server_alias_addr() {
1571        let config = Config::from_connection_string("Addr=myhost;").unwrap();
1572        assert_eq!(config.host, "myhost");
1573    }
1574
1575    #[test]
1576    fn test_server_alias_address() {
1577        let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1578        assert_eq!(config.host, "myhost");
1579        assert_eq!(config.port, 1434);
1580    }
1581
1582    #[test]
1583    fn test_server_alias_network_address() {
1584        let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1585        assert_eq!(config.host, "myhost");
1586    }
1587
1588    #[test]
1589    fn test_timeout_alias() {
1590        let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1591        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1592    }
1593
1594    #[test]
1595    fn test_application_intent_readonly() {
1596        let config =
1597            Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1598        assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1599    }
1600
1601    #[test]
1602    fn test_application_intent_readwrite() {
1603        let config =
1604            Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1605                .unwrap();
1606        assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1607    }
1608
1609    #[test]
1610    fn test_application_intent_invalid() {
1611        let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1612        assert!(result.is_err());
1613        assert!(
1614            result
1615                .unwrap_err()
1616                .to_string()
1617                .contains("ApplicationIntent")
1618        );
1619    }
1620
1621    #[test]
1622    fn test_workstation_id() {
1623        let config =
1624            Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1625        assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1626    }
1627
1628    #[test]
1629    fn test_wsid_alias() {
1630        let config =
1631            Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1632        assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1633    }
1634
1635    #[test]
1636    fn test_language() {
1637        let config =
1638            Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1639        assert_eq!(config.language, Some("us_english".to_string()));
1640    }
1641
1642    #[test]
1643    fn test_current_language_alias() {
1644        let config =
1645            Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1646        assert_eq!(config.language, Some("Deutsch".to_string()));
1647    }
1648
1649    #[test]
1650    fn test_connect_retry_count() {
1651        let config =
1652            Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1653        assert_eq!(config.retry.max_retries, 5);
1654    }
1655
1656    #[test]
1657    fn test_connect_retry_interval() {
1658        let config =
1659            Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1660        assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1661    }
1662
1663    #[test]
1664    fn test_pool_keywords_accepted_without_error() {
1665        // Pool keywords should be recognized (not error) but not affect Config
1666        let result = Config::from_connection_string(
1667            "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1668        );
1669        assert!(result.is_ok());
1670    }
1671
1672    #[test]
1673    fn test_known_unsupported_keywords_accepted() {
1674        // Known ADO.NET keywords we don't support should not error
1675        let result = Config::from_connection_string(
1676            "Server=localhost;Failover Partner=backup;Persist Security Info=false;",
1677        );
1678        assert!(result.is_ok());
1679    }
1680
1681    #[test]
1682    fn test_multi_subnet_failover_connection_string() {
1683        let config =
1684            Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=true;").unwrap();
1685        assert!(config.multi_subnet_failover);
1686
1687        // Space-separated variant
1688        let config =
1689            Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1690                .unwrap();
1691        assert!(config.multi_subnet_failover);
1692
1693        // Disabled
1694        let config =
1695            Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1696                .unwrap();
1697        assert!(!config.multi_subnet_failover);
1698
1699        // Default is false
1700        let config = Config::from_connection_string("Server=localhost;").unwrap();
1701        assert!(!config.multi_subnet_failover);
1702    }
1703
1704    #[test]
1705    fn test_multi_subnet_failover_builder() {
1706        let config = Config::new().multi_subnet_failover(true);
1707        assert!(config.multi_subnet_failover);
1708
1709        let config = Config::new().multi_subnet_failover(false);
1710        assert!(!config.multi_subnet_failover);
1711    }
1712
1713    #[test]
1714    fn test_multi_subnet_failover_invalid_value() {
1715        let result = Config::from_connection_string("Server=localhost;MultiSubnetFailover=banana;");
1716        assert!(result.is_err());
1717    }
1718
1719    #[test]
1720    fn test_application_intent_builder() {
1721        let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1722        assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1723    }
1724
1725    #[test]
1726    fn test_workstation_id_builder() {
1727        let config = Config::new().workstation_id("MY-PC");
1728        assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1729    }
1730
1731    #[test]
1732    fn test_language_builder() {
1733        let config = Config::new().language("us_english");
1734        assert_eq!(config.language, Some("us_english".to_string()));
1735    }
1736
1737    #[test]
1738    fn test_send_string_parameters_as_unicode_connection_string() {
1739        let config =
1740            Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=false;")
1741                .unwrap();
1742        assert!(!config.send_string_parameters_as_unicode);
1743
1744        // Space-separated variant
1745        let config = Config::from_connection_string(
1746            "Server=localhost;Send String Parameters As Unicode=false;",
1747        )
1748        .unwrap();
1749        assert!(!config.send_string_parameters_as_unicode);
1750
1751        // Enabled explicitly
1752        let config =
1753            Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1754                .unwrap();
1755        assert!(config.send_string_parameters_as_unicode);
1756
1757        // Default is true
1758        let config = Config::from_connection_string("Server=localhost;").unwrap();
1759        assert!(config.send_string_parameters_as_unicode);
1760    }
1761
1762    #[test]
1763    fn test_send_string_parameters_as_unicode_builder() {
1764        let config = Config::new().send_string_parameters_as_unicode(false);
1765        assert!(!config.send_string_parameters_as_unicode);
1766
1767        let config = Config::new().send_string_parameters_as_unicode(true);
1768        assert!(config.send_string_parameters_as_unicode);
1769    }
1770
1771    #[test]
1772    fn test_send_string_parameters_as_unicode_invalid_value() {
1773        let result = Config::from_connection_string(
1774            "Server=localhost;SendStringParametersAsUnicode=banana;",
1775        );
1776        assert!(result.is_err());
1777    }
1778
1779    #[test]
1780    fn test_empty_values_become_none() {
1781        // Per ADO.NET, empty values reset optional fields to default (None)
1782        let config =
1783            Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
1784        assert_eq!(config.database, None);
1785        assert_eq!(config.language, None);
1786    }
1787}