1mod 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
117fn 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
132fn 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 while i < len && (chars[i] == ';' || chars[i].is_whitespace()) {
149 i += 1;
150 }
151 if i >= len {
152 break;
153 }
154
155 let key_start = i;
157 while i < len && chars[i] != '=' {
158 i += 1;
159 }
160 if i >= len {
161 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; while i < len && chars[i].is_whitespace() {
176 i += 1;
177 }
178
179 let value = if i < len && (chars[i] == '"' || chars[i] == '\'') {
180 let quote_char = chars[i];
182 i += 1; 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 if i + 1 < len && chars[i + 1] == quote_char {
194 val.push(quote_char);
195 i += 2;
196 } else {
197 i += 1; break;
199 }
200 } else {
201 val.push(chars[i]);
202 i += 1;
203 }
204 }
205 while i < len && chars[i] != ';' {
207 i += 1;
208 }
209 val
210 } else {
211 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
228fn non_empty(value: &str) -> Option<String> {
233 if value.is_empty() {
234 None
235 } else {
236 Some(value.to_string())
237 }
238}
239
240#[derive(Debug, Clone)]
246#[non_exhaustive]
247pub struct Config {
248 pub host: String,
250
251 pub port: u16,
253
254 pub database: Option<String>,
256
257 pub credentials: Credentials,
259
260 #[cfg(feature = "tls")]
262 pub tls: TlsConfig,
263
264 pub application_name: String,
266
267 pub connect_timeout: Duration,
269
270 pub command_timeout: Duration,
272
273 pub packet_size: u16,
275
276 pub strict_mode: bool,
278
279 pub trust_server_certificate: bool,
281
282 pub instance: Option<String>,
284
285 pub mars: bool,
287
288 pub encrypt: bool,
292
293 pub no_tls: bool,
312
313 pub redirect: RedirectConfig,
315
316 pub retry: RetryPolicy,
318
319 pub timeouts: TimeoutConfig,
321
322 pub tds_version: TdsVersion,
335
336 pub application_intent: ApplicationIntent,
342
343 pub workstation_id: Option<String>,
350
351 pub language: Option<String>,
357
358 pub multi_subnet_failover: bool,
369
370 pub send_string_parameters_as_unicode: bool,
385
386 #[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, no_tls: false, redirect: RedirectConfig::default(),
422 retry: RetryPolicy::default(),
423 timeouts,
424 tds_version: TdsVersion::V7_4, 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 #[must_use]
439 pub fn new() -> Self {
440 Self::default()
441 }
442
443 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" | "addr" | "address" | "network address" | "host" => {
464 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 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" | "initial catalog" => {
504 config.database = non_empty(value);
505 }
506 "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 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 "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 "trustservercertificate" | "trust server certificate" => {
555 config.trust_server_certificate = parse_conn_bool(&key, value)?;
556 }
557 "encrypt" => {
558 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 let enabled = parse_conn_bool(&key, value)?;
581 config.encrypt = enabled;
582 config.no_tls = false;
583 }
584 }
585 "integrated security" | "trusted_connection" => {
586 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 "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 "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 "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 "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" | "multi subnet failover" => {
670 config.multi_subnet_failover = parse_conn_bool(&key, value)?;
671 }
672 "sendstringparametersasunicode" | "send string parameters as unicode" => {
674 config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
675 }
676 "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 #[must_use]
721 pub fn host(mut self, host: impl Into<String>) -> Self {
722 self.host = host.into();
723 self
724 }
725
726 #[must_use]
728 pub fn port(mut self, port: u16) -> Self {
729 self.port = port;
730 self
731 }
732
733 #[must_use]
735 pub fn database(mut self, database: impl Into<String>) -> Self {
736 self.database = Some(database.into());
737 self
738 }
739
740 #[must_use]
742 pub fn credentials(mut self, credentials: Credentials) -> Self {
743 self.credentials = credentials;
744 self
745 }
746
747 #[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 #[must_use]
756 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
757 self.connect_timeout = timeout;
758 self
759 }
760
761 #[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 #[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 #[must_use]
810 pub fn tds_version(mut self, version: TdsVersion) -> Self {
811 self.tds_version = version;
812 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 #[must_use]
831 pub fn encrypt(mut self, enabled: bool) -> Self {
832 self.encrypt = enabled;
833 self
834 }
835
836 #[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 #[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 #[must_use]
915 pub fn with_host(mut self, host: &str) -> Self {
916 self.host = host.to_string();
917 self
918 }
919
920 #[must_use]
922 pub fn with_port(mut self, port: u16) -> Self {
923 self.port = port;
924 self
925 }
926
927 #[must_use]
929 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
930 self.redirect = redirect;
931 self
932 }
933
934 #[must_use]
936 pub fn max_redirects(mut self, max: u8) -> Self {
937 self.redirect.max_redirects = max;
938 self
939 }
940
941 #[must_use]
943 pub fn retry(mut self, retry: RetryPolicy) -> Self {
944 self.retry = retry;
945 self
946 }
947
948 #[must_use]
950 pub fn max_retries(mut self, max: u32) -> Self {
951 self.retry.max_retries = max;
952 self
953 }
954
955 #[must_use]
957 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
958 self.connect_timeout = timeouts.connect_timeout;
960 self.command_timeout = timeouts.command_timeout;
961 self.timeouts = timeouts;
962 self
963 }
964
965 #[must_use]
967 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
968 self.application_intent = intent;
969 self
970 }
971
972 #[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 #[must_use]
986 pub fn language(mut self, lang: impl Into<String>) -> Self {
987 self.language = Some(lang.into());
988 self
989 }
990
991 #[must_use]
996 pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
997 self.multi_subnet_failover = enabled;
998 self
999 }
1000
1001 #[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 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 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 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 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 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 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 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1272 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1273
1274 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1276 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1277
1278 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1280 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1281
1282 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1284 assert_eq!(config.tds_version, TdsVersion::V7_4);
1285
1286 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 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 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 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1316 assert!(config.no_tls);
1317
1318 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1320 assert!(!config.no_tls);
1321 assert!(config.encrypt);
1322
1323 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 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1331 assert!(config.encrypt);
1332 assert!(!config.no_tls);
1333
1334 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 let config = Config::new().no_tls(true);
1344 assert!(config.no_tls);
1345 assert!(!config.encrypt);
1346
1347 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 let config =
1689 Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1690 .unwrap();
1691 assert!(config.multi_subnet_failover);
1692
1693 let config =
1695 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1696 .unwrap();
1697 assert!(!config.multi_subnet_failover);
1698
1699 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 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 let config =
1753 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1754 .unwrap();
1755 assert!(config.send_string_parameters_as_unicode);
1756
1757 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 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}