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 pub multi_subnet_failover: bool,
268
269 pub send_string_parameters_as_unicode: bool,
284
285 #[cfg(feature = "always-encrypted")]
297 pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
298}
299
300impl Default for Config {
301 fn default() -> Self {
302 let timeouts = TimeoutConfig::default();
303 Self {
304 host: "localhost".to_string(),
305 port: 1433,
306 database: None,
307 credentials: Credentials::sql_server("", ""),
308 #[cfg(feature = "tls")]
309 tls: TlsConfig::default(),
310 application_name: "mssql-client".to_string(),
311 connect_timeout: timeouts.connect_timeout,
312 command_timeout: timeouts.command_timeout,
313 packet_size: 4096,
314 strict_mode: false,
315 trust_server_certificate: false,
316 instance: None,
317 mars: false,
318 encrypt: true, no_tls: false, redirect: RedirectConfig::default(),
321 retry: RetryPolicy::default(),
322 timeouts,
323 tds_version: TdsVersion::V7_4, application_intent: ApplicationIntent::default(),
325 workstation_id: None,
326 language: None,
327 multi_subnet_failover: false,
328 send_string_parameters_as_unicode: true,
329 #[cfg(feature = "always-encrypted")]
330 column_encryption: None,
331 }
332 }
333}
334
335impl Config {
336 #[must_use]
338 pub fn new() -> Self {
339 Self::default()
340 }
341
342 pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
353 let mut config = Self::default();
354 let pairs = split_connection_string(conn_str)?;
355
356 for (key, value) in &pairs {
357 let key = key.trim().to_lowercase();
358 let value = value.trim();
359
360 match key.as_str() {
361 "server" | "data source" | "addr" | "address" | "network address" | "host" => {
363 let lower_value = value.to_lowercase();
367 let server_value = if lower_value.starts_with("tcp:") {
368 &value[4..]
369 } else if lower_value.starts_with("np:") {
370 return Err(crate::error::Error::Config(
371 "Named Pipes connections (np:) are not supported. Use TCP connections instead."
372 .into(),
373 ));
374 } else if lower_value.starts_with("lpc:") {
375 return Err(crate::error::Error::Config(
376 "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
377 .into(),
378 ));
379 } else {
380 value
381 };
382
383 if let Some((host, port_or_instance)) = server_value.split_once(',') {
385 config.host = host.to_string();
386 config.port = port_or_instance.trim().parse().map_err(|_| {
387 crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
388 })?;
389 } else if let Some((host, instance)) = server_value.split_once('\\') {
390 config.host = host.to_string();
391 config.instance = non_empty(instance);
392 } else {
393 config.host = server_value.to_string();
394 }
395 }
396 "port" => {
397 config.port = value.parse().map_err(|_| {
398 crate::error::Error::Config(format!("invalid port: {value}"))
399 })?;
400 }
401 "database" | "initial catalog" => {
403 config.database = non_empty(value);
404 }
405 "user id" | "uid" | "user" => {
407 if let Credentials::SqlServer { password, .. } = &config.credentials {
408 config.credentials =
409 Credentials::sql_server(value.to_string(), password.clone());
410 }
411 }
412 "password" | "pwd" => {
413 if let Credentials::SqlServer { username, .. } = &config.credentials {
414 config.credentials =
415 Credentials::sql_server(username.clone(), value.to_string());
416 }
417 }
418 "application name" | "app" => {
420 config.application_name = value.to_string();
421 }
422 "applicationintent" | "application intent" => {
423 config.application_intent = match value.to_lowercase().as_str() {
424 "readonly" => ApplicationIntent::ReadOnly,
425 "readwrite" => ApplicationIntent::ReadWrite,
426 _ => {
427 return Err(crate::error::Error::Config(format!(
428 "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
429 )));
430 }
431 };
432 }
433 "workstation id" | "wsid" => {
434 config.workstation_id = non_empty(value);
435 }
436 "current language" | "language" => {
437 config.language = non_empty(value);
438 }
439 "connect timeout" | "connection timeout" | "timeout" => {
441 let secs: u64 = value.parse().map_err(|_| {
442 crate::error::Error::Config(format!("invalid timeout: {value}"))
443 })?;
444 config.connect_timeout = Duration::from_secs(secs);
445 }
446 "command timeout" => {
447 let secs: u64 = value.parse().map_err(|_| {
448 crate::error::Error::Config(format!("invalid timeout: {value}"))
449 })?;
450 config.command_timeout = Duration::from_secs(secs);
451 }
452 "trustservercertificate" | "trust server certificate" => {
454 config.trust_server_certificate = parse_conn_bool(&key, value)?;
455 }
456 "encrypt" => {
457 if value.eq_ignore_ascii_case("strict") {
465 config.strict_mode = true;
466 config.encrypt = true;
467 config.no_tls = false;
468 } else if value.eq_ignore_ascii_case("mandatory") {
469 config.encrypt = true;
470 config.no_tls = false;
471 } else if value.eq_ignore_ascii_case("optional") {
472 config.encrypt = false;
473 config.no_tls = false;
474 } else if value.eq_ignore_ascii_case("no_tls") {
475 config.no_tls = true;
476 config.encrypt = false;
477 } else {
478 let enabled = parse_conn_bool(&key, value)?;
480 config.encrypt = enabled;
481 config.no_tls = false;
482 }
483 }
484 "integrated security" | "trusted_connection" => {
485 let enabled =
487 value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
488 if enabled {
489 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
490 {
491 config.credentials = Credentials::Integrated;
492 }
493 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
494 {
495 return Err(crate::error::Error::Config(
496 "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
497 or 'sspi-auth' (Windows) feature to be enabled"
498 .into(),
499 ));
500 }
501 }
502 }
503 "column encryption setting" | "columnencryptionsetting" => {
505 #[cfg(feature = "always-encrypted")]
506 if value.eq_ignore_ascii_case("enabled") {
507 config.column_encryption = Some(std::sync::Arc::new(
508 crate::encryption::EncryptionConfig::new(),
509 ));
510 }
511 #[cfg(not(feature = "always-encrypted"))]
512 if value.eq_ignore_ascii_case("enabled") {
513 return Err(crate::error::Error::Config(
514 "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
515 Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
516 .to_string(),
517 ));
518 }
519 }
520 "multipleactiveresultsets" | "mars" => {
522 config.mars = parse_conn_bool(&key, value)?;
523 }
524 "packet size" => {
525 config.packet_size = value.parse().map_err(|_| {
526 crate::error::Error::Config(format!("invalid packet size: {value}"))
527 })?;
528 }
529 "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
530 config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
531 crate::error::Error::Config(format!(
532 "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
533 ))
534 })?;
535 if config.tds_version.is_tds_8() {
536 config.strict_mode = true;
537 }
538 }
539 "connectretrycount" | "connect retry count" => {
541 config.retry.max_retries = value.parse().map_err(|_| {
542 crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
543 })?;
544 }
545 "connectretryinterval" | "connect retry interval" => {
546 let secs: u64 = value.parse().map_err(|_| {
547 crate::error::Error::Config(format!(
548 "invalid ConnectRetryInterval: '{value}'"
549 ))
550 })?;
551 config.retry.initial_backoff = Duration::from_secs(secs);
552 }
553 "max pool size"
555 | "min pool size"
556 | "pooling"
557 | "connection lifetime"
558 | "load balance timeout" => {
559 tracing::info!(
560 key = key.as_str(),
561 value = value,
562 "connection string keyword '{}' is recognized but pool settings \
563 must be configured via PoolConfig, not the connection string",
564 key,
565 );
566 }
567 "multisubnetfailover" | "multi subnet failover" => {
569 config.multi_subnet_failover = parse_conn_bool(&key, value)?;
570 }
571 "sendstringparametersasunicode" | "send string parameters as unicode" => {
573 config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
574 }
575 "failover partner"
577 | "persist security info"
578 | "persistsecurityinfo"
579 | "enlist"
580 | "replication"
581 | "transaction binding"
582 | "type system version"
583 | "user instance"
584 | "attachdbfilename"
585 | "extended properties"
586 | "initial file name"
587 | "context connection"
588 | "network library"
589 | "network"
590 | "net"
591 | "asynchronous processing"
592 | "async"
593 | "transparentnetworkipresolution"
594 | "poolblockingperiod"
595 | "authentication"
596 | "hostnameincertificate"
597 | "servercertificate" => {
598 tracing::info!(
599 key = key.as_str(),
600 value = value,
601 "connection string keyword '{}' is recognized but not supported by this driver",
602 key,
603 );
604 }
605 _ => {
606 tracing::debug!(
607 key = key.as_str(),
608 value = value,
609 "ignoring unknown connection string option"
610 );
611 }
612 }
613 }
614
615 Ok(config)
616 }
617
618 #[must_use]
620 pub fn host(mut self, host: impl Into<String>) -> Self {
621 self.host = host.into();
622 self
623 }
624
625 #[must_use]
627 pub fn port(mut self, port: u16) -> Self {
628 self.port = port;
629 self
630 }
631
632 #[must_use]
634 pub fn database(mut self, database: impl Into<String>) -> Self {
635 self.database = Some(database.into());
636 self
637 }
638
639 #[must_use]
641 pub fn credentials(mut self, credentials: Credentials) -> Self {
642 self.credentials = credentials;
643 self
644 }
645
646 #[must_use]
648 pub fn application_name(mut self, name: impl Into<String>) -> Self {
649 self.application_name = name.into();
650 self
651 }
652
653 #[must_use]
655 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
656 self.connect_timeout = timeout;
657 self
658 }
659
660 #[must_use]
662 pub fn trust_server_certificate(mut self, trust: bool) -> Self {
663 self.trust_server_certificate = trust;
664 #[cfg(feature = "tls")]
665 {
666 self.tls = self.tls.trust_server_certificate(trust);
667 }
668 self
669 }
670
671 #[must_use]
673 pub fn strict_mode(mut self, enabled: bool) -> Self {
674 self.strict_mode = enabled;
675 #[cfg(feature = "tls")]
676 {
677 self.tls = self.tls.strict_mode(enabled);
678 }
679 if enabled {
680 self.tds_version = TdsVersion::V8_0;
681 }
682 self
683 }
684
685 #[must_use]
709 pub fn tds_version(mut self, version: TdsVersion) -> Self {
710 self.tds_version = version;
711 if version.is_tds_8() {
713 self.strict_mode = true;
714 #[cfg(feature = "tls")]
715 {
716 self.tls = self.tls.strict_mode(true);
717 }
718 }
719 self
720 }
721
722 #[must_use]
730 pub fn encrypt(mut self, enabled: bool) -> Self {
731 self.encrypt = enabled;
732 self
733 }
734
735 #[must_use]
770 pub fn no_tls(mut self, enabled: bool) -> Self {
771 self.no_tls = enabled;
772 if enabled {
773 self.encrypt = false;
774 }
775 self
776 }
777
778 #[cfg(feature = "always-encrypted")]
795 #[must_use]
796 pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
797 self.column_encryption = Some(std::sync::Arc::new(config));
798 self
799 }
800
801 #[must_use]
803 pub fn with_host(mut self, host: &str) -> Self {
804 self.host = host.to_string();
805 self
806 }
807
808 #[must_use]
810 pub fn with_port(mut self, port: u16) -> Self {
811 self.port = port;
812 self
813 }
814
815 #[must_use]
817 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
818 self.redirect = redirect;
819 self
820 }
821
822 #[must_use]
824 pub fn max_redirects(mut self, max: u8) -> Self {
825 self.redirect.max_redirects = max;
826 self
827 }
828
829 #[must_use]
831 pub fn retry(mut self, retry: RetryPolicy) -> Self {
832 self.retry = retry;
833 self
834 }
835
836 #[must_use]
838 pub fn max_retries(mut self, max: u32) -> Self {
839 self.retry.max_retries = max;
840 self
841 }
842
843 #[must_use]
845 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
846 self.connect_timeout = timeouts.connect_timeout;
848 self.command_timeout = timeouts.command_timeout;
849 self.timeouts = timeouts;
850 self
851 }
852
853 #[must_use]
855 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
856 self.application_intent = intent;
857 self
858 }
859
860 #[must_use]
865 pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
866 self.workstation_id = Some(id.into());
867 self
868 }
869
870 #[must_use]
874 pub fn language(mut self, lang: impl Into<String>) -> Self {
875 self.language = Some(lang.into());
876 self
877 }
878
879 #[must_use]
884 pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
885 self.multi_subnet_failover = enabled;
886 self
887 }
888
889 #[must_use]
897 pub fn send_string_parameters_as_unicode(mut self, enabled: bool) -> Self {
898 self.send_string_parameters_as_unicode = enabled;
899 self
900 }
901}
902
903#[cfg(test)]
904#[allow(clippy::unwrap_used)]
905mod tests {
906 use super::*;
907
908 #[test]
909 fn test_connection_string_parsing() {
910 let config = Config::from_connection_string(
911 "Server=localhost;Database=test;User Id=sa;Password=secret;",
912 )
913 .unwrap();
914
915 assert_eq!(config.host, "localhost");
916 assert_eq!(config.database, Some("test".to_string()));
917 }
918
919 #[test]
920 fn test_connection_string_with_port() {
921 let config =
922 Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
923
924 assert_eq!(config.host, "localhost");
925 assert_eq!(config.port, 1434);
926 }
927
928 #[test]
929 fn test_connection_string_with_instance() {
930 let config =
931 Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
932
933 assert_eq!(config.host, "localhost");
934 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
935 }
936
937 #[test]
938 fn test_connection_string_dot_instance() {
939 let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
941
942 assert_eq!(config.host, ".");
943 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
944 }
945
946 #[test]
947 fn test_connection_string_local_instance() {
948 let config =
950 Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
951
952 assert_eq!(config.host, "(local)");
953 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
954 }
955
956 #[test]
957 fn test_redirect_config_defaults() {
958 let config = RedirectConfig::default();
959 assert_eq!(config.max_redirects, 2);
960 assert!(config.follow_redirects);
961 }
962
963 #[test]
964 fn test_redirect_config_builder() {
965 let config = RedirectConfig::new()
966 .max_redirects(5)
967 .follow_redirects(false);
968 assert_eq!(config.max_redirects, 5);
969 assert!(!config.follow_redirects);
970 }
971
972 #[test]
973 fn test_redirect_config_no_follow() {
974 let config = RedirectConfig::no_follow();
975 assert_eq!(config.max_redirects, 0);
976 assert!(!config.follow_redirects);
977 }
978
979 #[test]
980 fn test_config_redirect_builder() {
981 let config = Config::new().max_redirects(3);
982 assert_eq!(config.redirect.max_redirects, 3);
983
984 let config2 = Config::new().redirect(RedirectConfig::no_follow());
985 assert!(!config2.redirect.follow_redirects);
986 }
987
988 #[test]
989 fn test_retry_policy_defaults() {
990 let policy = RetryPolicy::default();
991 assert_eq!(policy.max_retries, 3);
992 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
993 assert_eq!(policy.max_backoff, Duration::from_secs(30));
994 assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
995 assert!(policy.jitter);
996 }
997
998 #[test]
999 fn test_retry_policy_builder() {
1000 let policy = RetryPolicy::new()
1001 .max_retries(5)
1002 .initial_backoff(Duration::from_millis(200))
1003 .max_backoff(Duration::from_secs(60))
1004 .backoff_multiplier(3.0)
1005 .jitter(false);
1006
1007 assert_eq!(policy.max_retries, 5);
1008 assert_eq!(policy.initial_backoff, Duration::from_millis(200));
1009 assert_eq!(policy.max_backoff, Duration::from_secs(60));
1010 assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
1011 assert!(!policy.jitter);
1012 }
1013
1014 #[test]
1015 fn test_retry_policy_no_retry() {
1016 let policy = RetryPolicy::no_retry();
1017 assert_eq!(policy.max_retries, 0);
1018 assert!(!policy.should_retry(0));
1019 }
1020
1021 #[test]
1022 fn test_retry_policy_should_retry() {
1023 let policy = RetryPolicy::new().max_retries(3);
1024 assert!(policy.should_retry(0));
1025 assert!(policy.should_retry(1));
1026 assert!(policy.should_retry(2));
1027 assert!(!policy.should_retry(3));
1028 assert!(!policy.should_retry(4));
1029 }
1030
1031 #[test]
1032 fn test_retry_policy_backoff_calculation() {
1033 let policy = RetryPolicy::new()
1034 .initial_backoff(Duration::from_millis(100))
1035 .backoff_multiplier(2.0)
1036 .max_backoff(Duration::from_secs(10))
1037 .jitter(false);
1038
1039 assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
1040 assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
1041 assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
1042 assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
1043 }
1044
1045 #[test]
1046 fn test_retry_policy_backoff_capped() {
1047 let policy = RetryPolicy::new()
1048 .initial_backoff(Duration::from_secs(1))
1049 .backoff_multiplier(10.0)
1050 .max_backoff(Duration::from_secs(5))
1051 .jitter(false);
1052
1053 assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
1055 }
1056
1057 #[test]
1058 fn test_config_retry_builder() {
1059 let config = Config::new().max_retries(5);
1060 assert_eq!(config.retry.max_retries, 5);
1061
1062 let config2 = Config::new().retry(RetryPolicy::no_retry());
1063 assert_eq!(config2.retry.max_retries, 0);
1064 }
1065
1066 #[test]
1067 fn test_timeout_config_defaults() {
1068 let config = TimeoutConfig::default();
1069 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1070 assert_eq!(config.tls_timeout, Duration::from_secs(10));
1071 assert_eq!(config.login_timeout, Duration::from_secs(30));
1072 assert_eq!(config.command_timeout, Duration::from_secs(30));
1073 assert_eq!(config.idle_timeout, Duration::from_secs(300));
1074 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1075 }
1076
1077 #[test]
1078 fn test_timeout_config_builder() {
1079 let config = TimeoutConfig::new()
1080 .connect_timeout(Duration::from_secs(5))
1081 .tls_timeout(Duration::from_secs(3))
1082 .login_timeout(Duration::from_secs(10))
1083 .command_timeout(Duration::from_secs(60))
1084 .idle_timeout(Duration::from_secs(600))
1085 .keepalive_interval(Some(Duration::from_secs(60)));
1086
1087 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1088 assert_eq!(config.tls_timeout, Duration::from_secs(3));
1089 assert_eq!(config.login_timeout, Duration::from_secs(10));
1090 assert_eq!(config.command_timeout, Duration::from_secs(60));
1091 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1092 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1093 }
1094
1095 #[test]
1096 fn test_timeout_config_no_keepalive() {
1097 let config = TimeoutConfig::new().no_keepalive();
1098 assert_eq!(config.keepalive_interval, None);
1099 }
1100
1101 #[test]
1102 fn test_timeout_config_total_connect() {
1103 let config = TimeoutConfig::new()
1104 .connect_timeout(Duration::from_secs(5))
1105 .tls_timeout(Duration::from_secs(3))
1106 .login_timeout(Duration::from_secs(10));
1107
1108 assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1110 }
1111
1112 #[test]
1113 fn test_config_timeouts_builder() {
1114 let timeouts = TimeoutConfig::new()
1115 .connect_timeout(Duration::from_secs(5))
1116 .command_timeout(Duration::from_secs(60));
1117
1118 let config = Config::new().timeouts(timeouts);
1119 assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1120 assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1121 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1123 assert_eq!(config.command_timeout, Duration::from_secs(60));
1124 }
1125
1126 #[test]
1127 fn test_tds_version_default() {
1128 let config = Config::default();
1129 assert_eq!(config.tds_version, TdsVersion::V7_4);
1130 assert!(!config.strict_mode);
1131 }
1132
1133 #[test]
1134 fn test_tds_version_builder() {
1135 let config = Config::new().tds_version(TdsVersion::V7_3A);
1136 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1137 assert!(!config.strict_mode);
1138
1139 let config = Config::new().tds_version(TdsVersion::V7_3B);
1140 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1141 assert!(!config.strict_mode);
1142
1143 let config = Config::new().tds_version(TdsVersion::V8_0);
1145 assert_eq!(config.tds_version, TdsVersion::V8_0);
1146 assert!(config.strict_mode);
1147 }
1148
1149 #[test]
1150 fn test_strict_mode_sets_tds_8() {
1151 let config = Config::new().strict_mode(true);
1152 assert!(config.strict_mode);
1153 assert_eq!(config.tds_version, TdsVersion::V8_0);
1154 }
1155
1156 #[test]
1157 fn test_connection_string_tds_version() {
1158 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1160 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1161
1162 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1164 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1165
1166 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1168 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1169
1170 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1172 assert_eq!(config.tds_version, TdsVersion::V7_4);
1173
1174 let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1176 assert_eq!(config.tds_version, TdsVersion::V8_0);
1177 assert!(config.strict_mode);
1178
1179 let config =
1181 Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1182 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1183 }
1184
1185 #[test]
1186 fn test_connection_string_invalid_tds_version() {
1187 let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1188 assert!(result.is_err());
1189
1190 let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1191 assert!(result.is_err());
1192 }
1193
1194 #[test]
1195 fn test_connection_string_no_tls() {
1196 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1198 assert!(config.no_tls);
1199 assert!(!config.encrypt);
1200 assert!(!config.strict_mode);
1201
1202 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1204 assert!(config.no_tls);
1205
1206 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1208 assert!(!config.no_tls);
1209 assert!(config.encrypt);
1210
1211 let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1213 assert!(!config.no_tls);
1214 assert!(config.encrypt);
1215 assert!(config.strict_mode);
1216
1217 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1219 assert!(config.encrypt);
1220 assert!(!config.no_tls);
1221
1222 let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1224 assert!(!config.encrypt);
1225 assert!(!config.no_tls);
1226 }
1227
1228 #[test]
1229 fn test_no_tls_builder() {
1230 let config = Config::new().no_tls(true);
1232 assert!(config.no_tls);
1233 assert!(!config.encrypt);
1234
1235 let config = Config::new().no_tls(true).no_tls(false);
1237 assert!(!config.no_tls);
1238 }
1239
1240 #[test]
1241 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1242 fn test_connection_string_integrated_security() {
1243 let config =
1245 Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1246 assert_eq!(
1247 config.credentials.method_name(),
1248 "Integrated Authentication"
1249 );
1250
1251 let config =
1253 Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1254 assert_eq!(
1255 config.credentials.method_name(),
1256 "Integrated Authentication"
1257 );
1258
1259 let config =
1261 Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1262 assert_eq!(
1263 config.credentials.method_name(),
1264 "Integrated Authentication"
1265 );
1266
1267 let config =
1269 Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1270 assert_eq!(
1271 config.credentials.method_name(),
1272 "Integrated Authentication"
1273 );
1274
1275 let config =
1277 Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1278 assert_eq!(
1279 config.credentials.method_name(),
1280 "Integrated Authentication"
1281 );
1282 }
1283
1284 #[test]
1285 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1286 fn test_connection_string_integrated_security_without_feature() {
1287 let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1289 assert!(result.is_err());
1290 let err = result.unwrap_err().to_string();
1291 assert!(err.contains("integrated-auth"));
1292 }
1293
1294 #[test]
1299 fn test_parse_conn_bool_all_values() {
1300 assert!(parse_conn_bool("test", "true").unwrap());
1301 assert!(parse_conn_bool("test", "True").unwrap());
1302 assert!(parse_conn_bool("test", "TRUE").unwrap());
1303 assert!(parse_conn_bool("test", "yes").unwrap());
1304 assert!(parse_conn_bool("test", "Yes").unwrap());
1305 assert!(parse_conn_bool("test", "1").unwrap());
1306
1307 assert!(!parse_conn_bool("test", "false").unwrap());
1308 assert!(!parse_conn_bool("test", "False").unwrap());
1309 assert!(!parse_conn_bool("test", "FALSE").unwrap());
1310 assert!(!parse_conn_bool("test", "no").unwrap());
1311 assert!(!parse_conn_bool("test", "No").unwrap());
1312 assert!(!parse_conn_bool("test", "0").unwrap());
1313
1314 assert!(parse_conn_bool("test", "banana").is_err());
1316 assert!(parse_conn_bool("test", "tru").is_err());
1317 assert!(parse_conn_bool("test", "").is_err());
1318 }
1319
1320 #[test]
1321 fn test_boolean_validation_trust_server_certificate() {
1322 let config =
1324 Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1325 .unwrap();
1326 assert!(config.trust_server_certificate);
1327
1328 let config =
1329 Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1330 assert!(!config.trust_server_certificate);
1331
1332 let result =
1334 Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1335 assert!(result.is_err());
1336 assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1337 }
1338
1339 #[test]
1340 fn test_boolean_validation_mars() {
1341 let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1342 assert!(config.mars);
1343
1344 let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1346 assert!(result.is_err());
1347 }
1348
1349 #[test]
1350 fn test_quoted_value_semicolon() {
1351 let config = Config::from_connection_string(
1353 r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1354 )
1355 .unwrap();
1356 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1357 assert_eq!(password.as_ref(), "my;complex;pass");
1358 } else {
1359 unreachable!("expected SqlServer credentials");
1360 }
1361 }
1362
1363 #[test]
1364 fn test_quoted_value_single_quotes() {
1365 let config =
1366 Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1367 .unwrap();
1368 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1369 assert_eq!(password.as_ref(), "my;pass");
1370 } else {
1371 unreachable!("expected SqlServer credentials");
1372 }
1373 }
1374
1375 #[test]
1376 fn test_quoted_value_escaped_double_quotes() {
1377 let config = Config::from_connection_string(
1379 r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1380 )
1381 .unwrap();
1382 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1383 assert_eq!(password.as_ref(), r#"has "quotes""#);
1384 } else {
1385 unreachable!("expected SqlServer credentials");
1386 }
1387 }
1388
1389 #[test]
1390 fn test_quoted_value_escaped_single_quotes() {
1391 let config =
1392 Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1393 .unwrap();
1394 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1395 assert_eq!(password.as_ref(), "it's complex");
1396 } else {
1397 unreachable!("expected SqlServer credentials");
1398 }
1399 }
1400
1401 #[test]
1402 fn test_quoted_value_unterminated() {
1403 let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1404 assert!(result.is_err());
1405 assert!(result.unwrap_err().to_string().contains("unterminated"));
1406 }
1407
1408 #[test]
1409 fn test_tcp_prefix_stripped() {
1410 let config = Config::from_connection_string(
1412 "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1413 )
1414 .unwrap();
1415 assert_eq!(config.host, "myserver.database.windows.net");
1416 assert_eq!(config.port, 1433);
1417 }
1418
1419 #[test]
1420 fn test_tcp_prefix_mixed_case() {
1421 let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1423 assert_eq!(config.host, "myhost");
1424
1425 let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1426 assert_eq!(config.host, "myhost");
1427 }
1428
1429 #[test]
1430 fn test_tcp_prefix_with_instance() {
1431 let config =
1432 Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1433 assert_eq!(config.host, "myhost");
1434 assert_eq!(config.instance, Some("INST".to_string()));
1435 }
1436
1437 #[test]
1438 fn test_np_prefix_rejected() {
1439 let result =
1440 Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1441 assert!(result.is_err());
1442 assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1443
1444 let result =
1446 Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1447 assert!(result.is_err());
1448 }
1449
1450 #[test]
1451 fn test_lpc_prefix_rejected() {
1452 let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1453 assert!(result.is_err());
1454 assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1455 }
1456
1457 #[test]
1458 fn test_server_alias_addr() {
1459 let config = Config::from_connection_string("Addr=myhost;").unwrap();
1460 assert_eq!(config.host, "myhost");
1461 }
1462
1463 #[test]
1464 fn test_server_alias_address() {
1465 let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1466 assert_eq!(config.host, "myhost");
1467 assert_eq!(config.port, 1434);
1468 }
1469
1470 #[test]
1471 fn test_server_alias_network_address() {
1472 let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1473 assert_eq!(config.host, "myhost");
1474 }
1475
1476 #[test]
1477 fn test_timeout_alias() {
1478 let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1479 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1480 }
1481
1482 #[test]
1483 fn test_application_intent_readonly() {
1484 let config =
1485 Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1486 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1487 }
1488
1489 #[test]
1490 fn test_application_intent_readwrite() {
1491 let config =
1492 Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1493 .unwrap();
1494 assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1495 }
1496
1497 #[test]
1498 fn test_application_intent_invalid() {
1499 let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1500 assert!(result.is_err());
1501 assert!(
1502 result
1503 .unwrap_err()
1504 .to_string()
1505 .contains("ApplicationIntent")
1506 );
1507 }
1508
1509 #[test]
1510 fn test_workstation_id() {
1511 let config =
1512 Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1513 assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1514 }
1515
1516 #[test]
1517 fn test_wsid_alias() {
1518 let config =
1519 Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1520 assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1521 }
1522
1523 #[test]
1524 fn test_language() {
1525 let config =
1526 Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1527 assert_eq!(config.language, Some("us_english".to_string()));
1528 }
1529
1530 #[test]
1531 fn test_current_language_alias() {
1532 let config =
1533 Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1534 assert_eq!(config.language, Some("Deutsch".to_string()));
1535 }
1536
1537 #[test]
1538 fn test_connect_retry_count() {
1539 let config =
1540 Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1541 assert_eq!(config.retry.max_retries, 5);
1542 }
1543
1544 #[test]
1545 fn test_connect_retry_interval() {
1546 let config =
1547 Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1548 assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1549 }
1550
1551 #[test]
1552 fn test_pool_keywords_accepted_without_error() {
1553 let result = Config::from_connection_string(
1555 "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1556 );
1557 assert!(result.is_ok());
1558 }
1559
1560 #[test]
1561 fn test_known_unsupported_keywords_accepted() {
1562 let result = Config::from_connection_string(
1564 "Server=localhost;Failover Partner=backup;Persist Security Info=false;",
1565 );
1566 assert!(result.is_ok());
1567 }
1568
1569 #[test]
1570 fn test_multi_subnet_failover_connection_string() {
1571 let config =
1572 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=true;").unwrap();
1573 assert!(config.multi_subnet_failover);
1574
1575 let config =
1577 Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1578 .unwrap();
1579 assert!(config.multi_subnet_failover);
1580
1581 let config =
1583 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1584 .unwrap();
1585 assert!(!config.multi_subnet_failover);
1586
1587 let config = Config::from_connection_string("Server=localhost;").unwrap();
1589 assert!(!config.multi_subnet_failover);
1590 }
1591
1592 #[test]
1593 fn test_multi_subnet_failover_builder() {
1594 let config = Config::new().multi_subnet_failover(true);
1595 assert!(config.multi_subnet_failover);
1596
1597 let config = Config::new().multi_subnet_failover(false);
1598 assert!(!config.multi_subnet_failover);
1599 }
1600
1601 #[test]
1602 fn test_multi_subnet_failover_invalid_value() {
1603 let result = Config::from_connection_string("Server=localhost;MultiSubnetFailover=banana;");
1604 assert!(result.is_err());
1605 }
1606
1607 #[test]
1608 fn test_application_intent_builder() {
1609 let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1610 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1611 }
1612
1613 #[test]
1614 fn test_workstation_id_builder() {
1615 let config = Config::new().workstation_id("MY-PC");
1616 assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1617 }
1618
1619 #[test]
1620 fn test_language_builder() {
1621 let config = Config::new().language("us_english");
1622 assert_eq!(config.language, Some("us_english".to_string()));
1623 }
1624
1625 #[test]
1626 fn test_send_string_parameters_as_unicode_connection_string() {
1627 let config =
1628 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=false;")
1629 .unwrap();
1630 assert!(!config.send_string_parameters_as_unicode);
1631
1632 let config = Config::from_connection_string(
1634 "Server=localhost;Send String Parameters As Unicode=false;",
1635 )
1636 .unwrap();
1637 assert!(!config.send_string_parameters_as_unicode);
1638
1639 let config =
1641 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1642 .unwrap();
1643 assert!(config.send_string_parameters_as_unicode);
1644
1645 let config = Config::from_connection_string("Server=localhost;").unwrap();
1647 assert!(config.send_string_parameters_as_unicode);
1648 }
1649
1650 #[test]
1651 fn test_send_string_parameters_as_unicode_builder() {
1652 let config = Config::new().send_string_parameters_as_unicode(false);
1653 assert!(!config.send_string_parameters_as_unicode);
1654
1655 let config = Config::new().send_string_parameters_as_unicode(true);
1656 assert!(config.send_string_parameters_as_unicode);
1657 }
1658
1659 #[test]
1660 fn test_send_string_parameters_as_unicode_invalid_value() {
1661 let result = Config::from_connection_string(
1662 "Server=localhost;SendStringParametersAsUnicode=banana;",
1663 );
1664 assert!(result.is_err());
1665 }
1666
1667 #[test]
1668 fn test_empty_values_become_none() {
1669 let config =
1671 Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
1672 assert_eq!(config.database, None);
1673 assert_eq!(config.language, None);
1674 }
1675}