1mod types;
7pub use types::*;
8
9use std::time::Duration;
10
11use mssql_auth::Credentials;
12#[cfg(feature = "tls")]
13use mssql_tls::TlsConfig;
14use tds_protocol::version::TdsVersion;
15
16fn parse_conn_bool(key: &str, value: &str) -> Result<bool, crate::error::Error> {
22 match value.to_lowercase().as_str() {
23 "true" | "yes" | "1" => Ok(true),
24 "false" | "no" | "0" => Ok(false),
25 _ => Err(crate::error::Error::Config(format!(
26 "invalid boolean value for '{key}': '{value}' (expected true/false/yes/no/1/0)"
27 ))),
28 }
29}
30
31fn split_connection_string(conn_str: &str) -> Result<Vec<(String, String)>, crate::error::Error> {
40 let mut pairs = Vec::new();
41 let chars: Vec<char> = conn_str.chars().collect();
42 let len = chars.len();
43 let mut i = 0;
44
45 while i < len {
46 while i < len && (chars[i] == ';' || chars[i].is_whitespace()) {
48 i += 1;
49 }
50 if i >= len {
51 break;
52 }
53
54 let key_start = i;
56 while i < len && chars[i] != '=' {
57 i += 1;
58 }
59 if i >= len {
60 let remaining = chars[key_start..].iter().collect::<String>();
62 if remaining.trim().is_empty() {
63 break;
64 }
65 return Err(crate::error::Error::Config(format!(
66 "invalid key-value pair (missing '='): '{remaining}'"
67 )));
68 }
69 let key: String = chars[key_start..i].iter().collect();
70 i += 1; while i < len && chars[i].is_whitespace() {
75 i += 1;
76 }
77
78 let value = if i < len && (chars[i] == '"' || chars[i] == '\'') {
79 let quote_char = chars[i];
81 i += 1; let mut val = String::new();
83 loop {
84 if i >= len {
85 return Err(crate::error::Error::Config(format!(
86 "unterminated quoted value for key '{}'",
87 key.trim()
88 )));
89 }
90 if chars[i] == quote_char {
91 if i + 1 < len && chars[i + 1] == quote_char {
93 val.push(quote_char);
94 i += 2;
95 } else {
96 i += 1; break;
98 }
99 } else {
100 val.push(chars[i]);
101 i += 1;
102 }
103 }
104 while i < len && chars[i] != ';' {
106 i += 1;
107 }
108 val
109 } else {
110 let val_start = i;
112 while i < len && chars[i] != ';' {
113 i += 1;
114 }
115 chars[val_start..i].iter().collect::<String>()
116 };
117
118 let key_trimmed = key.trim().to_string();
119 if !key_trimmed.is_empty() {
120 pairs.push((key_trimmed, value));
121 }
122 }
123
124 Ok(pairs)
125}
126
127fn non_empty(value: &str) -> Option<String> {
132 if value.is_empty() {
133 None
134 } else {
135 Some(value.to_string())
136 }
137}
138
139#[derive(Debug, Clone)]
145#[non_exhaustive]
146pub struct Config {
147 pub host: String,
149
150 pub port: u16,
152
153 pub database: Option<String>,
155
156 pub credentials: Credentials,
158
159 #[cfg(feature = "tls")]
161 pub tls: TlsConfig,
162
163 pub application_name: String,
165
166 pub connect_timeout: Duration,
168
169 pub command_timeout: Duration,
171
172 pub packet_size: u16,
174
175 pub strict_mode: bool,
177
178 pub trust_server_certificate: bool,
180
181 pub instance: Option<String>,
183
184 pub mars: bool,
186
187 pub encrypt: bool,
191
192 pub no_tls: bool,
211
212 pub redirect: RedirectConfig,
214
215 pub retry: RetryPolicy,
217
218 pub timeouts: TimeoutConfig,
220
221 pub tds_version: TdsVersion,
234
235 pub application_intent: ApplicationIntent,
241
242 pub workstation_id: Option<String>,
249
250 pub language: Option<String>,
256
257 #[cfg(feature = "always-encrypted")]
269 pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
270}
271
272impl Default for Config {
273 fn default() -> Self {
274 let timeouts = TimeoutConfig::default();
275 Self {
276 host: "localhost".to_string(),
277 port: 1433,
278 database: None,
279 credentials: Credentials::sql_server("", ""),
280 #[cfg(feature = "tls")]
281 tls: TlsConfig::default(),
282 application_name: "mssql-client".to_string(),
283 connect_timeout: timeouts.connect_timeout,
284 command_timeout: timeouts.command_timeout,
285 packet_size: 4096,
286 strict_mode: false,
287 trust_server_certificate: false,
288 instance: None,
289 mars: false,
290 encrypt: true, no_tls: false, redirect: RedirectConfig::default(),
293 retry: RetryPolicy::default(),
294 timeouts,
295 tds_version: TdsVersion::V7_4, application_intent: ApplicationIntent::default(),
297 workstation_id: None,
298 language: None,
299 #[cfg(feature = "always-encrypted")]
300 column_encryption: None,
301 }
302 }
303}
304
305impl Config {
306 #[must_use]
308 pub fn new() -> Self {
309 Self::default()
310 }
311
312 pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
323 let mut config = Self::default();
324 let pairs = split_connection_string(conn_str)?;
325
326 for (key, value) in &pairs {
327 let key = key.trim().to_lowercase();
328 let value = value.trim();
329
330 match key.as_str() {
331 "server" | "data source" | "addr" | "address" | "network address" | "host" => {
333 let lower_value = value.to_lowercase();
337 let server_value = if lower_value.starts_with("tcp:") {
338 &value[4..]
339 } else if lower_value.starts_with("np:") {
340 return Err(crate::error::Error::Config(
341 "Named Pipes connections (np:) are not supported. Use TCP connections instead."
342 .into(),
343 ));
344 } else if lower_value.starts_with("lpc:") {
345 return Err(crate::error::Error::Config(
346 "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
347 .into(),
348 ));
349 } else {
350 value
351 };
352
353 if let Some((host, port_or_instance)) = server_value.split_once(',') {
355 config.host = host.to_string();
356 config.port = port_or_instance.trim().parse().map_err(|_| {
357 crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
358 })?;
359 } else if let Some((host, instance)) = server_value.split_once('\\') {
360 config.host = host.to_string();
361 config.instance = non_empty(instance);
362 } else {
363 config.host = server_value.to_string();
364 }
365 }
366 "port" => {
367 config.port = value.parse().map_err(|_| {
368 crate::error::Error::Config(format!("invalid port: {value}"))
369 })?;
370 }
371 "database" | "initial catalog" => {
373 config.database = non_empty(value);
374 }
375 "user id" | "uid" | "user" => {
377 if let Credentials::SqlServer { password, .. } = &config.credentials {
378 config.credentials =
379 Credentials::sql_server(value.to_string(), password.clone());
380 }
381 }
382 "password" | "pwd" => {
383 if let Credentials::SqlServer { username, .. } = &config.credentials {
384 config.credentials =
385 Credentials::sql_server(username.clone(), value.to_string());
386 }
387 }
388 "application name" | "app" => {
390 config.application_name = value.to_string();
391 }
392 "applicationintent" | "application intent" => {
393 config.application_intent = match value.to_lowercase().as_str() {
394 "readonly" => ApplicationIntent::ReadOnly,
395 "readwrite" => ApplicationIntent::ReadWrite,
396 _ => {
397 return Err(crate::error::Error::Config(format!(
398 "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
399 )));
400 }
401 };
402 }
403 "workstation id" | "wsid" => {
404 config.workstation_id = non_empty(value);
405 }
406 "current language" | "language" => {
407 config.language = non_empty(value);
408 }
409 "connect timeout" | "connection timeout" | "timeout" => {
411 let secs: u64 = value.parse().map_err(|_| {
412 crate::error::Error::Config(format!("invalid timeout: {value}"))
413 })?;
414 config.connect_timeout = Duration::from_secs(secs);
415 }
416 "command timeout" => {
417 let secs: u64 = value.parse().map_err(|_| {
418 crate::error::Error::Config(format!("invalid timeout: {value}"))
419 })?;
420 config.command_timeout = Duration::from_secs(secs);
421 }
422 "trustservercertificate" | "trust server certificate" => {
424 config.trust_server_certificate = parse_conn_bool(&key, value)?;
425 }
426 "encrypt" => {
427 if value.eq_ignore_ascii_case("strict") {
435 config.strict_mode = true;
436 config.encrypt = true;
437 config.no_tls = false;
438 } else if value.eq_ignore_ascii_case("mandatory") {
439 config.encrypt = true;
440 config.no_tls = false;
441 } else if value.eq_ignore_ascii_case("optional") {
442 config.encrypt = false;
443 config.no_tls = false;
444 } else if value.eq_ignore_ascii_case("no_tls") {
445 config.no_tls = true;
446 config.encrypt = false;
447 } else {
448 let enabled = parse_conn_bool(&key, value)?;
450 config.encrypt = enabled;
451 config.no_tls = false;
452 }
453 }
454 "integrated security" | "trusted_connection" => {
455 let enabled =
457 value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
458 if enabled {
459 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
460 {
461 config.credentials = Credentials::Integrated;
462 }
463 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
464 {
465 return Err(crate::error::Error::Config(
466 "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
467 or 'sspi-auth' (Windows) feature to be enabled"
468 .into(),
469 ));
470 }
471 }
472 }
473 "column encryption setting" | "columnencryptionsetting" => {
475 #[cfg(feature = "always-encrypted")]
476 if value.eq_ignore_ascii_case("enabled") {
477 config.column_encryption = Some(std::sync::Arc::new(
478 crate::encryption::EncryptionConfig::new(),
479 ));
480 }
481 #[cfg(not(feature = "always-encrypted"))]
482 if value.eq_ignore_ascii_case("enabled") {
483 return Err(crate::error::Error::Config(
484 "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
485 Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
486 .to_string(),
487 ));
488 }
489 }
490 "multipleactiveresultsets" | "mars" => {
492 config.mars = parse_conn_bool(&key, value)?;
493 }
494 "packet size" => {
495 config.packet_size = value.parse().map_err(|_| {
496 crate::error::Error::Config(format!("invalid packet size: {value}"))
497 })?;
498 }
499 "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
500 config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
501 crate::error::Error::Config(format!(
502 "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
503 ))
504 })?;
505 if config.tds_version.is_tds_8() {
506 config.strict_mode = true;
507 }
508 }
509 "connectretrycount" | "connect retry count" => {
511 config.retry.max_retries = value.parse().map_err(|_| {
512 crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
513 })?;
514 }
515 "connectretryinterval" | "connect retry interval" => {
516 let secs: u64 = value.parse().map_err(|_| {
517 crate::error::Error::Config(format!(
518 "invalid ConnectRetryInterval: '{value}'"
519 ))
520 })?;
521 config.retry.initial_backoff = Duration::from_secs(secs);
522 }
523 "max pool size"
525 | "min pool size"
526 | "pooling"
527 | "connection lifetime"
528 | "load balance timeout" => {
529 tracing::info!(
530 key = key.as_str(),
531 value = value,
532 "connection string keyword '{}' is recognized but pool settings \
533 must be configured via PoolConfig, not the connection string",
534 key,
535 );
536 }
537 "failover partner"
539 | "multisubnetfailover"
540 | "multi subnet failover"
541 | "persist security info"
542 | "persistsecurityinfo"
543 | "enlist"
544 | "replication"
545 | "transaction binding"
546 | "type system version"
547 | "user instance"
548 | "attachdbfilename"
549 | "extended properties"
550 | "initial file name"
551 | "context connection"
552 | "network library"
553 | "network"
554 | "net"
555 | "asynchronous processing"
556 | "async"
557 | "transparentnetworkipresolution"
558 | "poolblockingperiod"
559 | "authentication"
560 | "hostnameincertificate"
561 | "servercertificate" => {
562 tracing::info!(
563 key = key.as_str(),
564 value = value,
565 "connection string keyword '{}' is recognized but not supported by this driver",
566 key,
567 );
568 }
569 _ => {
570 tracing::debug!(
571 key = key.as_str(),
572 value = value,
573 "ignoring unknown connection string option"
574 );
575 }
576 }
577 }
578
579 Ok(config)
580 }
581
582 #[must_use]
584 pub fn host(mut self, host: impl Into<String>) -> Self {
585 self.host = host.into();
586 self
587 }
588
589 #[must_use]
591 pub fn port(mut self, port: u16) -> Self {
592 self.port = port;
593 self
594 }
595
596 #[must_use]
598 pub fn database(mut self, database: impl Into<String>) -> Self {
599 self.database = Some(database.into());
600 self
601 }
602
603 #[must_use]
605 pub fn credentials(mut self, credentials: Credentials) -> Self {
606 self.credentials = credentials;
607 self
608 }
609
610 #[must_use]
612 pub fn application_name(mut self, name: impl Into<String>) -> Self {
613 self.application_name = name.into();
614 self
615 }
616
617 #[must_use]
619 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
620 self.connect_timeout = timeout;
621 self
622 }
623
624 #[must_use]
626 pub fn trust_server_certificate(mut self, trust: bool) -> Self {
627 self.trust_server_certificate = trust;
628 #[cfg(feature = "tls")]
629 {
630 self.tls = self.tls.trust_server_certificate(trust);
631 }
632 self
633 }
634
635 #[must_use]
637 pub fn strict_mode(mut self, enabled: bool) -> Self {
638 self.strict_mode = enabled;
639 #[cfg(feature = "tls")]
640 {
641 self.tls = self.tls.strict_mode(enabled);
642 }
643 if enabled {
644 self.tds_version = TdsVersion::V8_0;
645 }
646 self
647 }
648
649 #[must_use]
673 pub fn tds_version(mut self, version: TdsVersion) -> Self {
674 self.tds_version = version;
675 if version.is_tds_8() {
677 self.strict_mode = true;
678 #[cfg(feature = "tls")]
679 {
680 self.tls = self.tls.strict_mode(true);
681 }
682 }
683 self
684 }
685
686 #[must_use]
694 pub fn encrypt(mut self, enabled: bool) -> Self {
695 self.encrypt = enabled;
696 self
697 }
698
699 #[must_use]
734 pub fn no_tls(mut self, enabled: bool) -> Self {
735 self.no_tls = enabled;
736 if enabled {
737 self.encrypt = false;
738 }
739 self
740 }
741
742 #[cfg(feature = "always-encrypted")]
759 #[must_use]
760 pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
761 self.column_encryption = Some(std::sync::Arc::new(config));
762 self
763 }
764
765 #[must_use]
767 pub fn with_host(mut self, host: &str) -> Self {
768 self.host = host.to_string();
769 self
770 }
771
772 #[must_use]
774 pub fn with_port(mut self, port: u16) -> Self {
775 self.port = port;
776 self
777 }
778
779 #[must_use]
781 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
782 self.redirect = redirect;
783 self
784 }
785
786 #[must_use]
788 pub fn max_redirects(mut self, max: u8) -> Self {
789 self.redirect.max_redirects = max;
790 self
791 }
792
793 #[must_use]
795 pub fn retry(mut self, retry: RetryPolicy) -> Self {
796 self.retry = retry;
797 self
798 }
799
800 #[must_use]
802 pub fn max_retries(mut self, max: u32) -> Self {
803 self.retry.max_retries = max;
804 self
805 }
806
807 #[must_use]
809 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
810 self.connect_timeout = timeouts.connect_timeout;
812 self.command_timeout = timeouts.command_timeout;
813 self.timeouts = timeouts;
814 self
815 }
816
817 #[must_use]
819 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
820 self.application_intent = intent;
821 self
822 }
823
824 #[must_use]
829 pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
830 self.workstation_id = Some(id.into());
831 self
832 }
833
834 #[must_use]
838 pub fn language(mut self, lang: impl Into<String>) -> Self {
839 self.language = Some(lang.into());
840 self
841 }
842}
843
844#[cfg(test)]
845#[allow(clippy::unwrap_used)]
846mod tests {
847 use super::*;
848
849 #[test]
850 fn test_connection_string_parsing() {
851 let config = Config::from_connection_string(
852 "Server=localhost;Database=test;User Id=sa;Password=secret;",
853 )
854 .unwrap();
855
856 assert_eq!(config.host, "localhost");
857 assert_eq!(config.database, Some("test".to_string()));
858 }
859
860 #[test]
861 fn test_connection_string_with_port() {
862 let config =
863 Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
864
865 assert_eq!(config.host, "localhost");
866 assert_eq!(config.port, 1434);
867 }
868
869 #[test]
870 fn test_connection_string_with_instance() {
871 let config =
872 Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
873
874 assert_eq!(config.host, "localhost");
875 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
876 }
877
878 #[test]
879 fn test_connection_string_dot_instance() {
880 let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
882
883 assert_eq!(config.host, ".");
884 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
885 }
886
887 #[test]
888 fn test_connection_string_local_instance() {
889 let config =
891 Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
892
893 assert_eq!(config.host, "(local)");
894 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
895 }
896
897 #[test]
898 fn test_redirect_config_defaults() {
899 let config = RedirectConfig::default();
900 assert_eq!(config.max_redirects, 2);
901 assert!(config.follow_redirects);
902 }
903
904 #[test]
905 fn test_redirect_config_builder() {
906 let config = RedirectConfig::new()
907 .max_redirects(5)
908 .follow_redirects(false);
909 assert_eq!(config.max_redirects, 5);
910 assert!(!config.follow_redirects);
911 }
912
913 #[test]
914 fn test_redirect_config_no_follow() {
915 let config = RedirectConfig::no_follow();
916 assert_eq!(config.max_redirects, 0);
917 assert!(!config.follow_redirects);
918 }
919
920 #[test]
921 fn test_config_redirect_builder() {
922 let config = Config::new().max_redirects(3);
923 assert_eq!(config.redirect.max_redirects, 3);
924
925 let config2 = Config::new().redirect(RedirectConfig::no_follow());
926 assert!(!config2.redirect.follow_redirects);
927 }
928
929 #[test]
930 fn test_retry_policy_defaults() {
931 let policy = RetryPolicy::default();
932 assert_eq!(policy.max_retries, 3);
933 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
934 assert_eq!(policy.max_backoff, Duration::from_secs(30));
935 assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
936 assert!(policy.jitter);
937 }
938
939 #[test]
940 fn test_retry_policy_builder() {
941 let policy = RetryPolicy::new()
942 .max_retries(5)
943 .initial_backoff(Duration::from_millis(200))
944 .max_backoff(Duration::from_secs(60))
945 .backoff_multiplier(3.0)
946 .jitter(false);
947
948 assert_eq!(policy.max_retries, 5);
949 assert_eq!(policy.initial_backoff, Duration::from_millis(200));
950 assert_eq!(policy.max_backoff, Duration::from_secs(60));
951 assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
952 assert!(!policy.jitter);
953 }
954
955 #[test]
956 fn test_retry_policy_no_retry() {
957 let policy = RetryPolicy::no_retry();
958 assert_eq!(policy.max_retries, 0);
959 assert!(!policy.should_retry(0));
960 }
961
962 #[test]
963 fn test_retry_policy_should_retry() {
964 let policy = RetryPolicy::new().max_retries(3);
965 assert!(policy.should_retry(0));
966 assert!(policy.should_retry(1));
967 assert!(policy.should_retry(2));
968 assert!(!policy.should_retry(3));
969 assert!(!policy.should_retry(4));
970 }
971
972 #[test]
973 fn test_retry_policy_backoff_calculation() {
974 let policy = RetryPolicy::new()
975 .initial_backoff(Duration::from_millis(100))
976 .backoff_multiplier(2.0)
977 .max_backoff(Duration::from_secs(10))
978 .jitter(false);
979
980 assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
981 assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
982 assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
983 assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
984 }
985
986 #[test]
987 fn test_retry_policy_backoff_capped() {
988 let policy = RetryPolicy::new()
989 .initial_backoff(Duration::from_secs(1))
990 .backoff_multiplier(10.0)
991 .max_backoff(Duration::from_secs(5))
992 .jitter(false);
993
994 assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
996 }
997
998 #[test]
999 fn test_config_retry_builder() {
1000 let config = Config::new().max_retries(5);
1001 assert_eq!(config.retry.max_retries, 5);
1002
1003 let config2 = Config::new().retry(RetryPolicy::no_retry());
1004 assert_eq!(config2.retry.max_retries, 0);
1005 }
1006
1007 #[test]
1008 fn test_timeout_config_defaults() {
1009 let config = TimeoutConfig::default();
1010 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1011 assert_eq!(config.tls_timeout, Duration::from_secs(10));
1012 assert_eq!(config.login_timeout, Duration::from_secs(30));
1013 assert_eq!(config.command_timeout, Duration::from_secs(30));
1014 assert_eq!(config.idle_timeout, Duration::from_secs(300));
1015 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1016 }
1017
1018 #[test]
1019 fn test_timeout_config_builder() {
1020 let config = TimeoutConfig::new()
1021 .connect_timeout(Duration::from_secs(5))
1022 .tls_timeout(Duration::from_secs(3))
1023 .login_timeout(Duration::from_secs(10))
1024 .command_timeout(Duration::from_secs(60))
1025 .idle_timeout(Duration::from_secs(600))
1026 .keepalive_interval(Some(Duration::from_secs(60)));
1027
1028 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1029 assert_eq!(config.tls_timeout, Duration::from_secs(3));
1030 assert_eq!(config.login_timeout, Duration::from_secs(10));
1031 assert_eq!(config.command_timeout, Duration::from_secs(60));
1032 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1033 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1034 }
1035
1036 #[test]
1037 fn test_timeout_config_no_keepalive() {
1038 let config = TimeoutConfig::new().no_keepalive();
1039 assert_eq!(config.keepalive_interval, None);
1040 }
1041
1042 #[test]
1043 fn test_timeout_config_total_connect() {
1044 let config = TimeoutConfig::new()
1045 .connect_timeout(Duration::from_secs(5))
1046 .tls_timeout(Duration::from_secs(3))
1047 .login_timeout(Duration::from_secs(10));
1048
1049 assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1051 }
1052
1053 #[test]
1054 fn test_config_timeouts_builder() {
1055 let timeouts = TimeoutConfig::new()
1056 .connect_timeout(Duration::from_secs(5))
1057 .command_timeout(Duration::from_secs(60));
1058
1059 let config = Config::new().timeouts(timeouts);
1060 assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1061 assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1062 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1064 assert_eq!(config.command_timeout, Duration::from_secs(60));
1065 }
1066
1067 #[test]
1068 fn test_tds_version_default() {
1069 let config = Config::default();
1070 assert_eq!(config.tds_version, TdsVersion::V7_4);
1071 assert!(!config.strict_mode);
1072 }
1073
1074 #[test]
1075 fn test_tds_version_builder() {
1076 let config = Config::new().tds_version(TdsVersion::V7_3A);
1077 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1078 assert!(!config.strict_mode);
1079
1080 let config = Config::new().tds_version(TdsVersion::V7_3B);
1081 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1082 assert!(!config.strict_mode);
1083
1084 let config = Config::new().tds_version(TdsVersion::V8_0);
1086 assert_eq!(config.tds_version, TdsVersion::V8_0);
1087 assert!(config.strict_mode);
1088 }
1089
1090 #[test]
1091 fn test_strict_mode_sets_tds_8() {
1092 let config = Config::new().strict_mode(true);
1093 assert!(config.strict_mode);
1094 assert_eq!(config.tds_version, TdsVersion::V8_0);
1095 }
1096
1097 #[test]
1098 fn test_connection_string_tds_version() {
1099 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1101 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1102
1103 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1105 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1106
1107 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1109 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1110
1111 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1113 assert_eq!(config.tds_version, TdsVersion::V7_4);
1114
1115 let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1117 assert_eq!(config.tds_version, TdsVersion::V8_0);
1118 assert!(config.strict_mode);
1119
1120 let config =
1122 Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1123 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1124 }
1125
1126 #[test]
1127 fn test_connection_string_invalid_tds_version() {
1128 let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1129 assert!(result.is_err());
1130
1131 let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1132 assert!(result.is_err());
1133 }
1134
1135 #[test]
1136 fn test_connection_string_no_tls() {
1137 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1139 assert!(config.no_tls);
1140 assert!(!config.encrypt);
1141 assert!(!config.strict_mode);
1142
1143 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1145 assert!(config.no_tls);
1146
1147 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1149 assert!(!config.no_tls);
1150 assert!(config.encrypt);
1151
1152 let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1154 assert!(!config.no_tls);
1155 assert!(config.encrypt);
1156 assert!(config.strict_mode);
1157
1158 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1160 assert!(config.encrypt);
1161 assert!(!config.no_tls);
1162
1163 let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1165 assert!(!config.encrypt);
1166 assert!(!config.no_tls);
1167 }
1168
1169 #[test]
1170 fn test_no_tls_builder() {
1171 let config = Config::new().no_tls(true);
1173 assert!(config.no_tls);
1174 assert!(!config.encrypt);
1175
1176 let config = Config::new().no_tls(true).no_tls(false);
1178 assert!(!config.no_tls);
1179 }
1180
1181 #[test]
1182 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1183 fn test_connection_string_integrated_security() {
1184 let config =
1186 Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1187 assert_eq!(
1188 config.credentials.method_name(),
1189 "Integrated Authentication"
1190 );
1191
1192 let config =
1194 Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1195 assert_eq!(
1196 config.credentials.method_name(),
1197 "Integrated Authentication"
1198 );
1199
1200 let config =
1202 Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1203 assert_eq!(
1204 config.credentials.method_name(),
1205 "Integrated Authentication"
1206 );
1207
1208 let config =
1210 Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1211 assert_eq!(
1212 config.credentials.method_name(),
1213 "Integrated Authentication"
1214 );
1215
1216 let config =
1218 Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1219 assert_eq!(
1220 config.credentials.method_name(),
1221 "Integrated Authentication"
1222 );
1223 }
1224
1225 #[test]
1226 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1227 fn test_connection_string_integrated_security_without_feature() {
1228 let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1230 assert!(result.is_err());
1231 let err = result.unwrap_err().to_string();
1232 assert!(err.contains("integrated-auth"));
1233 }
1234
1235 #[test]
1240 fn test_parse_conn_bool_all_values() {
1241 assert!(parse_conn_bool("test", "true").unwrap());
1242 assert!(parse_conn_bool("test", "True").unwrap());
1243 assert!(parse_conn_bool("test", "TRUE").unwrap());
1244 assert!(parse_conn_bool("test", "yes").unwrap());
1245 assert!(parse_conn_bool("test", "Yes").unwrap());
1246 assert!(parse_conn_bool("test", "1").unwrap());
1247
1248 assert!(!parse_conn_bool("test", "false").unwrap());
1249 assert!(!parse_conn_bool("test", "False").unwrap());
1250 assert!(!parse_conn_bool("test", "FALSE").unwrap());
1251 assert!(!parse_conn_bool("test", "no").unwrap());
1252 assert!(!parse_conn_bool("test", "No").unwrap());
1253 assert!(!parse_conn_bool("test", "0").unwrap());
1254
1255 assert!(parse_conn_bool("test", "banana").is_err());
1257 assert!(parse_conn_bool("test", "tru").is_err());
1258 assert!(parse_conn_bool("test", "").is_err());
1259 }
1260
1261 #[test]
1262 fn test_boolean_validation_trust_server_certificate() {
1263 let config =
1265 Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1266 .unwrap();
1267 assert!(config.trust_server_certificate);
1268
1269 let config =
1270 Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1271 assert!(!config.trust_server_certificate);
1272
1273 let result =
1275 Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1276 assert!(result.is_err());
1277 assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1278 }
1279
1280 #[test]
1281 fn test_boolean_validation_mars() {
1282 let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1283 assert!(config.mars);
1284
1285 let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1287 assert!(result.is_err());
1288 }
1289
1290 #[test]
1291 fn test_quoted_value_semicolon() {
1292 let config = Config::from_connection_string(
1294 r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1295 )
1296 .unwrap();
1297 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1298 assert_eq!(password.as_ref(), "my;complex;pass");
1299 } else {
1300 unreachable!("expected SqlServer credentials");
1301 }
1302 }
1303
1304 #[test]
1305 fn test_quoted_value_single_quotes() {
1306 let config =
1307 Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1308 .unwrap();
1309 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1310 assert_eq!(password.as_ref(), "my;pass");
1311 } else {
1312 unreachable!("expected SqlServer credentials");
1313 }
1314 }
1315
1316 #[test]
1317 fn test_quoted_value_escaped_double_quotes() {
1318 let config = Config::from_connection_string(
1320 r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1321 )
1322 .unwrap();
1323 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1324 assert_eq!(password.as_ref(), r#"has "quotes""#);
1325 } else {
1326 unreachable!("expected SqlServer credentials");
1327 }
1328 }
1329
1330 #[test]
1331 fn test_quoted_value_escaped_single_quotes() {
1332 let config =
1333 Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1334 .unwrap();
1335 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1336 assert_eq!(password.as_ref(), "it's complex");
1337 } else {
1338 unreachable!("expected SqlServer credentials");
1339 }
1340 }
1341
1342 #[test]
1343 fn test_quoted_value_unterminated() {
1344 let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1345 assert!(result.is_err());
1346 assert!(result.unwrap_err().to_string().contains("unterminated"));
1347 }
1348
1349 #[test]
1350 fn test_tcp_prefix_stripped() {
1351 let config = Config::from_connection_string(
1353 "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1354 )
1355 .unwrap();
1356 assert_eq!(config.host, "myserver.database.windows.net");
1357 assert_eq!(config.port, 1433);
1358 }
1359
1360 #[test]
1361 fn test_tcp_prefix_mixed_case() {
1362 let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1364 assert_eq!(config.host, "myhost");
1365
1366 let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1367 assert_eq!(config.host, "myhost");
1368 }
1369
1370 #[test]
1371 fn test_tcp_prefix_with_instance() {
1372 let config =
1373 Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1374 assert_eq!(config.host, "myhost");
1375 assert_eq!(config.instance, Some("INST".to_string()));
1376 }
1377
1378 #[test]
1379 fn test_np_prefix_rejected() {
1380 let result =
1381 Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1382 assert!(result.is_err());
1383 assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1384
1385 let result =
1387 Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1388 assert!(result.is_err());
1389 }
1390
1391 #[test]
1392 fn test_lpc_prefix_rejected() {
1393 let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1394 assert!(result.is_err());
1395 assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1396 }
1397
1398 #[test]
1399 fn test_server_alias_addr() {
1400 let config = Config::from_connection_string("Addr=myhost;").unwrap();
1401 assert_eq!(config.host, "myhost");
1402 }
1403
1404 #[test]
1405 fn test_server_alias_address() {
1406 let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1407 assert_eq!(config.host, "myhost");
1408 assert_eq!(config.port, 1434);
1409 }
1410
1411 #[test]
1412 fn test_server_alias_network_address() {
1413 let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1414 assert_eq!(config.host, "myhost");
1415 }
1416
1417 #[test]
1418 fn test_timeout_alias() {
1419 let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1420 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1421 }
1422
1423 #[test]
1424 fn test_application_intent_readonly() {
1425 let config =
1426 Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1427 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1428 }
1429
1430 #[test]
1431 fn test_application_intent_readwrite() {
1432 let config =
1433 Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1434 .unwrap();
1435 assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1436 }
1437
1438 #[test]
1439 fn test_application_intent_invalid() {
1440 let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1441 assert!(result.is_err());
1442 assert!(
1443 result
1444 .unwrap_err()
1445 .to_string()
1446 .contains("ApplicationIntent")
1447 );
1448 }
1449
1450 #[test]
1451 fn test_workstation_id() {
1452 let config =
1453 Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1454 assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1455 }
1456
1457 #[test]
1458 fn test_wsid_alias() {
1459 let config =
1460 Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1461 assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1462 }
1463
1464 #[test]
1465 fn test_language() {
1466 let config =
1467 Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1468 assert_eq!(config.language, Some("us_english".to_string()));
1469 }
1470
1471 #[test]
1472 fn test_current_language_alias() {
1473 let config =
1474 Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1475 assert_eq!(config.language, Some("Deutsch".to_string()));
1476 }
1477
1478 #[test]
1479 fn test_connect_retry_count() {
1480 let config =
1481 Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1482 assert_eq!(config.retry.max_retries, 5);
1483 }
1484
1485 #[test]
1486 fn test_connect_retry_interval() {
1487 let config =
1488 Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1489 assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1490 }
1491
1492 #[test]
1493 fn test_pool_keywords_accepted_without_error() {
1494 let result = Config::from_connection_string(
1496 "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1497 );
1498 assert!(result.is_ok());
1499 }
1500
1501 #[test]
1502 fn test_known_unsupported_keywords_accepted() {
1503 let result = Config::from_connection_string(
1505 "Server=localhost;Failover Partner=backup;MultiSubnetFailover=true;",
1506 );
1507 assert!(result.is_ok());
1508 }
1509
1510 #[test]
1511 fn test_application_intent_builder() {
1512 let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1513 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1514 }
1515
1516 #[test]
1517 fn test_workstation_id_builder() {
1518 let config = Config::new().workstation_id("MY-PC");
1519 assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1520 }
1521
1522 #[test]
1523 fn test_language_builder() {
1524 let config = Config::new().language("us_english");
1525 assert_eq!(config.language, Some("us_english".to_string()));
1526 }
1527
1528 #[test]
1529 fn test_empty_values_become_none() {
1530 let config =
1532 Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
1533 assert_eq!(config.database, None);
1534 assert_eq!(config.language, None);
1535 }
1536}