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 max_response_size: usize,
283
284 pub packet_size: u16,
286
287 pub strict_mode: bool,
289
290 pub trust_server_certificate: bool,
292
293 pub instance: Option<String>,
295
296 pub mars: bool,
298
299 pub encrypt: bool,
303
304 pub no_tls: bool,
323
324 pub redirect: RedirectConfig,
326
327 pub retry: RetryPolicy,
329
330 pub timeouts: TimeoutConfig,
332
333 pub tds_version: TdsVersion,
346
347 pub application_intent: ApplicationIntent,
353
354 pub workstation_id: Option<String>,
361
362 pub language: Option<String>,
368
369 pub multi_subnet_failover: bool,
380
381 pub send_string_parameters_as_unicode: bool,
396
397 #[cfg(feature = "always-encrypted")]
409 pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
410}
411
412impl Default for Config {
413 fn default() -> Self {
414 let timeouts = TimeoutConfig::default();
415 Self {
416 host: "localhost".to_string(),
417 port: 1433,
418 database: None,
419 credentials: Credentials::sql_server("", ""),
420 #[cfg(feature = "tls")]
421 tls: TlsConfig::default(),
422 application_name: "mssql-client".to_string(),
423 connect_timeout: timeouts.connect_timeout,
424 command_timeout: timeouts.command_timeout,
425 max_response_size: 0,
426 packet_size: 4096,
427 strict_mode: false,
428 trust_server_certificate: false,
429 instance: None,
430 mars: false,
431 encrypt: true, no_tls: false, redirect: RedirectConfig::default(),
434 retry: RetryPolicy::default(),
435 timeouts,
436 tds_version: TdsVersion::V7_4, application_intent: ApplicationIntent::default(),
438 workstation_id: None,
439 language: None,
440 multi_subnet_failover: false,
441 send_string_parameters_as_unicode: true,
442 #[cfg(feature = "always-encrypted")]
443 column_encryption: None,
444 }
445 }
446}
447
448impl Config {
449 #[must_use]
451 pub fn new() -> Self {
452 Self::default()
453 }
454
455 pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
466 let mut config = Self::default();
467 let pairs = split_connection_string(conn_str)?;
468
469 for (key, value) in &pairs {
470 let key = key.trim().to_lowercase();
471 let value = value.trim();
472
473 match key.as_str() {
474 "server" | "data source" | "addr" | "address" | "network address" | "host" => {
476 let lower_value = value.to_lowercase();
480 let server_value = if lower_value.starts_with("tcp:") {
481 &value[4..]
482 } else if lower_value.starts_with("np:") {
483 return Err(crate::error::Error::Config(
484 "Named Pipes connections (np:) are not supported. Use TCP connections instead."
485 .into(),
486 ));
487 } else if lower_value.starts_with("lpc:") {
488 return Err(crate::error::Error::Config(
489 "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
490 .into(),
491 ));
492 } else {
493 value
494 };
495
496 if let Some((host, port_or_instance)) = server_value.split_once(',') {
498 config.host = host.to_string();
499 config.port = port_or_instance.trim().parse().map_err(|_| {
500 crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
501 })?;
502 } else if let Some((host, instance)) = server_value.split_once('\\') {
503 config.host = host.to_string();
504 config.instance = non_empty(instance);
505 } else {
506 config.host = server_value.to_string();
507 }
508 }
509 "port" => {
510 config.port = value.parse().map_err(|_| {
511 crate::error::Error::Config(format!("invalid port: {value}"))
512 })?;
513 }
514 "database" | "initial catalog" => {
516 config.database = non_empty(value);
517 }
518 "user id" | "uid" | "user" => {
520 if let Credentials::SqlServer { password, .. } = &config.credentials {
521 config.credentials =
522 Credentials::sql_server(value.to_string(), password.clone());
523 }
524 }
525 "password" | "pwd" => {
526 if let Credentials::SqlServer { username, .. } = &config.credentials {
527 config.credentials =
528 Credentials::sql_server(username.clone(), value.to_string());
529 }
530 }
531 "application name" | "app" => {
533 config.application_name = value.to_string();
534 }
535 "applicationintent" | "application intent" => {
536 config.application_intent = match value.to_lowercase().as_str() {
537 "readonly" => ApplicationIntent::ReadOnly,
538 "readwrite" => ApplicationIntent::ReadWrite,
539 _ => {
540 return Err(crate::error::Error::Config(format!(
541 "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
542 )));
543 }
544 };
545 }
546 "workstation id" | "wsid" => {
547 config.workstation_id = non_empty(value);
548 }
549 "current language" | "language" => {
550 config.language = non_empty(value);
551 }
552 "connect timeout" | "connection timeout" | "timeout" => {
554 let secs: u64 = value.parse().map_err(|_| {
555 crate::error::Error::Config(format!("invalid timeout: {value}"))
556 })?;
557 config.connect_timeout = Duration::from_secs(secs);
558 }
559 "command timeout" => {
560 let secs: u64 = value.parse().map_err(|_| {
561 crate::error::Error::Config(format!("invalid timeout: {value}"))
562 })?;
563 config.command_timeout = Duration::from_secs(secs);
564 }
565 "trustservercertificate" | "trust server certificate" => {
567 config.trust_server_certificate = parse_conn_bool(&key, value)?;
568 }
569 "encrypt" => {
570 if value.eq_ignore_ascii_case("strict") {
578 config.strict_mode = true;
579 config.encrypt = true;
580 config.no_tls = false;
581 } else if value.eq_ignore_ascii_case("mandatory") {
582 config.encrypt = true;
583 config.no_tls = false;
584 } else if value.eq_ignore_ascii_case("optional") {
585 config.encrypt = false;
586 config.no_tls = false;
587 } else if value.eq_ignore_ascii_case("no_tls") {
588 config.no_tls = true;
589 config.encrypt = false;
590 } else {
591 let enabled = parse_conn_bool(&key, value)?;
593 config.encrypt = enabled;
594 config.no_tls = false;
595 }
596 }
597 "integrated security" | "trusted_connection" => {
598 let enabled =
600 value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
601 if enabled {
602 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
603 {
604 config.credentials = Credentials::Integrated;
605 }
606 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
607 {
608 return Err(crate::error::Error::Config(
609 "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
610 or 'sspi-auth' (Windows) feature to be enabled"
611 .into(),
612 ));
613 }
614 }
615 }
616 "column encryption setting" | "columnencryptionsetting" => {
618 #[cfg(feature = "always-encrypted")]
619 if value.eq_ignore_ascii_case("enabled") {
620 config.column_encryption = Some(std::sync::Arc::new(
621 crate::encryption::EncryptionConfig::new(),
622 ));
623 }
624 #[cfg(not(feature = "always-encrypted"))]
625 if value.eq_ignore_ascii_case("enabled") {
626 return Err(crate::error::Error::Config(
627 "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
628 Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
629 .to_string(),
630 ));
631 }
632 }
633 "multipleactiveresultsets" | "mars" => {
635 config.mars = parse_conn_bool(&key, value)?;
636 }
637 "packet size" => {
638 config.packet_size = value.parse().map_err(|_| {
639 crate::error::Error::Config(format!("invalid packet size: {value}"))
640 })?;
641 }
642 "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
643 config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
644 crate::error::Error::Config(format!(
645 "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
646 ))
647 })?;
648 if config.tds_version.is_tds_8() {
649 config.strict_mode = true;
650 }
651 }
652 "connectretrycount" | "connect retry count" => {
654 config.retry.max_retries = value.parse().map_err(|_| {
655 crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
656 })?;
657 }
658 "connectretryinterval" | "connect retry interval" => {
659 let secs: u64 = value.parse().map_err(|_| {
660 crate::error::Error::Config(format!(
661 "invalid ConnectRetryInterval: '{value}'"
662 ))
663 })?;
664 config.retry.initial_backoff = Duration::from_secs(secs);
665 }
666 "max pool size"
668 | "min pool size"
669 | "pooling"
670 | "connection lifetime"
671 | "load balance timeout" => {
672 tracing::info!(
673 key = key.as_str(),
674 value = value,
675 "connection string keyword '{}' is recognized but pool settings \
676 must be configured via PoolConfig, not the connection string",
677 key,
678 );
679 }
680 "multisubnetfailover" | "multi subnet failover" => {
682 config.multi_subnet_failover = parse_conn_bool(&key, value)?;
683 }
684 "sendstringparametersasunicode" | "send string parameters as unicode" => {
686 config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
687 }
688 "failover partner"
690 | "persist security info"
691 | "persistsecurityinfo"
692 | "enlist"
693 | "replication"
694 | "transaction binding"
695 | "type system version"
696 | "user instance"
697 | "attachdbfilename"
698 | "extended properties"
699 | "initial file name"
700 | "context connection"
701 | "network library"
702 | "network"
703 | "net"
704 | "asynchronous processing"
705 | "async"
706 | "transparentnetworkipresolution"
707 | "poolblockingperiod"
708 | "authentication"
709 | "hostnameincertificate"
710 | "servercertificate" => {
711 tracing::info!(
712 key = key.as_str(),
713 value = value,
714 "connection string keyword '{}' is recognized but not supported by this driver",
715 key,
716 );
717 }
718 _ => {
719 tracing::debug!(
720 key = key.as_str(),
721 value = value,
722 "ignoring unknown connection string option"
723 );
724 }
725 }
726 }
727
728 Ok(config)
729 }
730
731 #[must_use]
733 pub fn host(mut self, host: impl Into<String>) -> Self {
734 self.host = host.into();
735 self
736 }
737
738 #[must_use]
740 pub fn port(mut self, port: u16) -> Self {
741 self.port = port;
742 self
743 }
744
745 #[must_use]
747 pub fn database(mut self, database: impl Into<String>) -> Self {
748 self.database = Some(database.into());
749 self
750 }
751
752 #[must_use]
754 pub fn credentials(mut self, credentials: Credentials) -> Self {
755 self.credentials = credentials;
756 self
757 }
758
759 #[must_use]
761 pub fn application_name(mut self, name: impl Into<String>) -> Self {
762 self.application_name = name.into();
763 self
764 }
765
766 #[must_use]
768 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
769 self.connect_timeout = timeout;
770 self
771 }
772
773 #[must_use]
775 pub fn trust_server_certificate(mut self, trust: bool) -> Self {
776 self.trust_server_certificate = trust;
777 #[cfg(feature = "tls")]
778 {
779 self.tls = self.tls.trust_server_certificate(trust);
780 }
781 self
782 }
783
784 #[must_use]
786 pub fn strict_mode(mut self, enabled: bool) -> Self {
787 self.strict_mode = enabled;
788 #[cfg(feature = "tls")]
789 {
790 self.tls = self.tls.strict_mode(enabled);
791 }
792 if enabled {
793 self.tds_version = TdsVersion::V8_0;
794 }
795 self
796 }
797
798 #[must_use]
822 pub fn tds_version(mut self, version: TdsVersion) -> Self {
823 self.tds_version = version;
824 if version.is_tds_8() {
826 self.strict_mode = true;
827 #[cfg(feature = "tls")]
828 {
829 self.tls = self.tls.strict_mode(true);
830 }
831 }
832 self
833 }
834
835 #[must_use]
843 pub fn encrypt(mut self, enabled: bool) -> Self {
844 self.encrypt = enabled;
845 self
846 }
847
848 #[must_use]
889 pub fn no_tls(mut self, enabled: bool) -> Self {
890 self.no_tls = enabled;
891 if enabled {
892 self.encrypt = false;
893 }
894 self
895 }
896
897 #[cfg(feature = "always-encrypted")]
919 #[must_use]
920 pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
921 self.column_encryption = Some(std::sync::Arc::new(config));
922 self
923 }
924
925 #[must_use]
927 pub fn with_host(mut self, host: &str) -> Self {
928 self.host = host.to_string();
929 self
930 }
931
932 #[must_use]
934 pub fn with_port(mut self, port: u16) -> Self {
935 self.port = port;
936 self
937 }
938
939 #[must_use]
941 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
942 self.redirect = redirect;
943 self
944 }
945
946 #[must_use]
948 pub fn max_redirects(mut self, max: u8) -> Self {
949 self.redirect.max_redirects = max;
950 self
951 }
952
953 #[must_use]
955 pub fn retry(mut self, retry: RetryPolicy) -> Self {
956 self.retry = retry;
957 self
958 }
959
960 #[must_use]
962 pub fn max_retries(mut self, max: u32) -> Self {
963 self.retry.max_retries = max;
964 self
965 }
966
967 #[must_use]
969 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
970 self.connect_timeout = timeouts.connect_timeout;
972 self.command_timeout = timeouts.command_timeout;
973 self.timeouts = timeouts;
974 self
975 }
976
977 #[must_use]
979 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
980 self.application_intent = intent;
981 self
982 }
983
984 #[must_use]
989 pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
990 self.workstation_id = Some(id.into());
991 self
992 }
993
994 #[must_use]
998 pub fn language(mut self, lang: impl Into<String>) -> Self {
999 self.language = Some(lang.into());
1000 self
1001 }
1002
1003 #[must_use]
1008 pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
1009 self.multi_subnet_failover = enabled;
1010 self
1011 }
1012
1013 #[must_use]
1021 pub fn send_string_parameters_as_unicode(mut self, enabled: bool) -> Self {
1022 self.send_string_parameters_as_unicode = enabled;
1023 self
1024 }
1025}
1026
1027#[cfg(test)]
1028#[allow(clippy::unwrap_used)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1033 fn test_connection_string_parsing() {
1034 let config = Config::from_connection_string(
1035 "Server=localhost;Database=test;User Id=sa;Password=secret;",
1036 )
1037 .unwrap();
1038
1039 assert_eq!(config.host, "localhost");
1040 assert_eq!(config.database, Some("test".to_string()));
1041 }
1042
1043 #[test]
1044 fn test_connection_string_with_port() {
1045 let config =
1046 Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
1047
1048 assert_eq!(config.host, "localhost");
1049 assert_eq!(config.port, 1434);
1050 }
1051
1052 #[test]
1053 fn test_connection_string_with_instance() {
1054 let config =
1055 Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
1056
1057 assert_eq!(config.host, "localhost");
1058 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1059 }
1060
1061 #[test]
1062 fn test_connection_string_dot_instance() {
1063 let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
1065
1066 assert_eq!(config.host, ".");
1067 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1068 }
1069
1070 #[test]
1071 fn test_connection_string_local_instance() {
1072 let config =
1074 Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
1075
1076 assert_eq!(config.host, "(local)");
1077 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1078 }
1079
1080 #[test]
1081 fn test_redirect_config_defaults() {
1082 let config = RedirectConfig::default();
1083 assert_eq!(config.max_redirects, 2);
1084 assert!(config.follow_redirects);
1085 }
1086
1087 #[test]
1088 fn test_redirect_config_builder() {
1089 let config = RedirectConfig::new()
1090 .max_redirects(5)
1091 .follow_redirects(false);
1092 assert_eq!(config.max_redirects, 5);
1093 assert!(!config.follow_redirects);
1094 }
1095
1096 #[test]
1097 fn test_redirect_config_no_follow() {
1098 let config = RedirectConfig::no_follow();
1099 assert_eq!(config.max_redirects, 0);
1100 assert!(!config.follow_redirects);
1101 }
1102
1103 #[test]
1104 fn test_config_redirect_builder() {
1105 let config = Config::new().max_redirects(3);
1106 assert_eq!(config.redirect.max_redirects, 3);
1107
1108 let config2 = Config::new().redirect(RedirectConfig::no_follow());
1109 assert!(!config2.redirect.follow_redirects);
1110 }
1111
1112 #[test]
1113 fn test_retry_policy_defaults() {
1114 let policy = RetryPolicy::default();
1115 assert_eq!(policy.max_retries, 3);
1116 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
1117 assert_eq!(policy.max_backoff, Duration::from_secs(30));
1118 assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
1119 assert!(policy.jitter);
1120 }
1121
1122 #[test]
1123 fn test_retry_policy_builder() {
1124 let policy = RetryPolicy::new()
1125 .max_retries(5)
1126 .initial_backoff(Duration::from_millis(200))
1127 .max_backoff(Duration::from_secs(60))
1128 .backoff_multiplier(3.0)
1129 .jitter(false);
1130
1131 assert_eq!(policy.max_retries, 5);
1132 assert_eq!(policy.initial_backoff, Duration::from_millis(200));
1133 assert_eq!(policy.max_backoff, Duration::from_secs(60));
1134 assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
1135 assert!(!policy.jitter);
1136 }
1137
1138 #[test]
1139 fn test_retry_policy_no_retry() {
1140 let policy = RetryPolicy::no_retry();
1141 assert_eq!(policy.max_retries, 0);
1142 assert!(!policy.should_retry(0));
1143 }
1144
1145 #[test]
1146 fn test_retry_policy_should_retry() {
1147 let policy = RetryPolicy::new().max_retries(3);
1148 assert!(policy.should_retry(0));
1149 assert!(policy.should_retry(1));
1150 assert!(policy.should_retry(2));
1151 assert!(!policy.should_retry(3));
1152 assert!(!policy.should_retry(4));
1153 }
1154
1155 #[test]
1156 fn test_retry_policy_backoff_calculation() {
1157 let policy = RetryPolicy::new()
1158 .initial_backoff(Duration::from_millis(100))
1159 .backoff_multiplier(2.0)
1160 .max_backoff(Duration::from_secs(10))
1161 .jitter(false);
1162
1163 assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
1164 assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
1165 assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
1166 assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
1167 }
1168
1169 #[test]
1170 fn test_retry_policy_backoff_capped() {
1171 let policy = RetryPolicy::new()
1172 .initial_backoff(Duration::from_secs(1))
1173 .backoff_multiplier(10.0)
1174 .max_backoff(Duration::from_secs(5))
1175 .jitter(false);
1176
1177 assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
1179 }
1180
1181 #[test]
1182 fn test_config_retry_builder() {
1183 let config = Config::new().max_retries(5);
1184 assert_eq!(config.retry.max_retries, 5);
1185
1186 let config2 = Config::new().retry(RetryPolicy::no_retry());
1187 assert_eq!(config2.retry.max_retries, 0);
1188 }
1189
1190 #[test]
1191 fn test_timeout_config_defaults() {
1192 let config = TimeoutConfig::default();
1193 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1194 assert_eq!(config.tls_timeout, Duration::from_secs(10));
1195 assert_eq!(config.login_timeout, Duration::from_secs(30));
1196 assert_eq!(config.command_timeout, Duration::from_secs(30));
1197 assert_eq!(config.idle_timeout, Duration::from_secs(300));
1198 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1199 }
1200
1201 #[test]
1202 fn test_timeout_config_builder() {
1203 let config = TimeoutConfig::new()
1204 .connect_timeout(Duration::from_secs(5))
1205 .tls_timeout(Duration::from_secs(3))
1206 .login_timeout(Duration::from_secs(10))
1207 .command_timeout(Duration::from_secs(60))
1208 .idle_timeout(Duration::from_secs(600))
1209 .keepalive_interval(Some(Duration::from_secs(60)));
1210
1211 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1212 assert_eq!(config.tls_timeout, Duration::from_secs(3));
1213 assert_eq!(config.login_timeout, Duration::from_secs(10));
1214 assert_eq!(config.command_timeout, Duration::from_secs(60));
1215 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1216 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1217 }
1218
1219 #[test]
1220 fn test_timeout_config_no_keepalive() {
1221 let config = TimeoutConfig::new().no_keepalive();
1222 assert_eq!(config.keepalive_interval, None);
1223 }
1224
1225 #[test]
1226 fn test_timeout_config_total_connect() {
1227 let config = TimeoutConfig::new()
1228 .connect_timeout(Duration::from_secs(5))
1229 .tls_timeout(Duration::from_secs(3))
1230 .login_timeout(Duration::from_secs(10));
1231
1232 assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1234 }
1235
1236 #[test]
1237 fn test_config_timeouts_builder() {
1238 let timeouts = TimeoutConfig::new()
1239 .connect_timeout(Duration::from_secs(5))
1240 .command_timeout(Duration::from_secs(60));
1241
1242 let config = Config::new().timeouts(timeouts);
1243 assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1244 assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1245 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1247 assert_eq!(config.command_timeout, Duration::from_secs(60));
1248 }
1249
1250 #[test]
1251 fn test_tds_version_default() {
1252 let config = Config::default();
1253 assert_eq!(config.tds_version, TdsVersion::V7_4);
1254 assert!(!config.strict_mode);
1255 }
1256
1257 #[test]
1258 fn test_tds_version_builder() {
1259 let config = Config::new().tds_version(TdsVersion::V7_3A);
1260 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1261 assert!(!config.strict_mode);
1262
1263 let config = Config::new().tds_version(TdsVersion::V7_3B);
1264 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1265 assert!(!config.strict_mode);
1266
1267 let config = Config::new().tds_version(TdsVersion::V8_0);
1269 assert_eq!(config.tds_version, TdsVersion::V8_0);
1270 assert!(config.strict_mode);
1271 }
1272
1273 #[test]
1274 fn test_strict_mode_sets_tds_8() {
1275 let config = Config::new().strict_mode(true);
1276 assert!(config.strict_mode);
1277 assert_eq!(config.tds_version, TdsVersion::V8_0);
1278 }
1279
1280 #[test]
1281 fn test_connection_string_tds_version() {
1282 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1284 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1285
1286 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1288 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1289
1290 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1292 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1293
1294 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1296 assert_eq!(config.tds_version, TdsVersion::V7_4);
1297
1298 let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1300 assert_eq!(config.tds_version, TdsVersion::V8_0);
1301 assert!(config.strict_mode);
1302
1303 let config =
1305 Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1306 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1307 }
1308
1309 #[test]
1310 fn test_connection_string_invalid_tds_version() {
1311 let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1312 assert!(result.is_err());
1313
1314 let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1315 assert!(result.is_err());
1316 }
1317
1318 #[test]
1319 fn test_connection_string_no_tls() {
1320 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1322 assert!(config.no_tls);
1323 assert!(!config.encrypt);
1324 assert!(!config.strict_mode);
1325
1326 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1328 assert!(config.no_tls);
1329
1330 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1332 assert!(!config.no_tls);
1333 assert!(config.encrypt);
1334
1335 let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1337 assert!(!config.no_tls);
1338 assert!(config.encrypt);
1339 assert!(config.strict_mode);
1340
1341 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1343 assert!(config.encrypt);
1344 assert!(!config.no_tls);
1345
1346 let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1348 assert!(!config.encrypt);
1349 assert!(!config.no_tls);
1350 }
1351
1352 #[test]
1353 fn test_no_tls_builder() {
1354 let config = Config::new().no_tls(true);
1356 assert!(config.no_tls);
1357 assert!(!config.encrypt);
1358
1359 let config = Config::new().no_tls(true).no_tls(false);
1361 assert!(!config.no_tls);
1362 }
1363
1364 #[test]
1365 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1366 fn test_connection_string_integrated_security() {
1367 let config =
1369 Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1370 assert_eq!(
1371 config.credentials.method_name(),
1372 "Integrated Authentication"
1373 );
1374
1375 let config =
1377 Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1378 assert_eq!(
1379 config.credentials.method_name(),
1380 "Integrated Authentication"
1381 );
1382
1383 let config =
1385 Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1386 assert_eq!(
1387 config.credentials.method_name(),
1388 "Integrated Authentication"
1389 );
1390
1391 let config =
1393 Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1394 assert_eq!(
1395 config.credentials.method_name(),
1396 "Integrated Authentication"
1397 );
1398
1399 let config =
1401 Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1402 assert_eq!(
1403 config.credentials.method_name(),
1404 "Integrated Authentication"
1405 );
1406 }
1407
1408 #[test]
1409 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1410 fn test_connection_string_integrated_security_without_feature() {
1411 let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1413 assert!(result.is_err());
1414 let err = result.unwrap_err().to_string();
1415 assert!(err.contains("integrated-auth"));
1416 }
1417
1418 #[test]
1423 fn test_parse_conn_bool_all_values() {
1424 assert!(parse_conn_bool("test", "true").unwrap());
1425 assert!(parse_conn_bool("test", "True").unwrap());
1426 assert!(parse_conn_bool("test", "TRUE").unwrap());
1427 assert!(parse_conn_bool("test", "yes").unwrap());
1428 assert!(parse_conn_bool("test", "Yes").unwrap());
1429 assert!(parse_conn_bool("test", "1").unwrap());
1430
1431 assert!(!parse_conn_bool("test", "false").unwrap());
1432 assert!(!parse_conn_bool("test", "False").unwrap());
1433 assert!(!parse_conn_bool("test", "FALSE").unwrap());
1434 assert!(!parse_conn_bool("test", "no").unwrap());
1435 assert!(!parse_conn_bool("test", "No").unwrap());
1436 assert!(!parse_conn_bool("test", "0").unwrap());
1437
1438 assert!(parse_conn_bool("test", "banana").is_err());
1440 assert!(parse_conn_bool("test", "tru").is_err());
1441 assert!(parse_conn_bool("test", "").is_err());
1442 }
1443
1444 #[test]
1445 fn test_boolean_validation_trust_server_certificate() {
1446 let config =
1448 Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1449 .unwrap();
1450 assert!(config.trust_server_certificate);
1451
1452 let config =
1453 Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1454 assert!(!config.trust_server_certificate);
1455
1456 let result =
1458 Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1459 assert!(result.is_err());
1460 assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1461 }
1462
1463 #[test]
1464 fn test_boolean_validation_mars() {
1465 let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1466 assert!(config.mars);
1467
1468 let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1470 assert!(result.is_err());
1471 }
1472
1473 #[test]
1474 fn test_quoted_value_semicolon() {
1475 let config = Config::from_connection_string(
1477 r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1478 )
1479 .unwrap();
1480 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1481 assert_eq!(password.as_ref(), "my;complex;pass");
1482 } else {
1483 unreachable!("expected SqlServer credentials");
1484 }
1485 }
1486
1487 #[test]
1488 fn test_quoted_value_single_quotes() {
1489 let config =
1490 Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1491 .unwrap();
1492 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1493 assert_eq!(password.as_ref(), "my;pass");
1494 } else {
1495 unreachable!("expected SqlServer credentials");
1496 }
1497 }
1498
1499 #[test]
1500 fn test_quoted_value_escaped_double_quotes() {
1501 let config = Config::from_connection_string(
1503 r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1504 )
1505 .unwrap();
1506 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1507 assert_eq!(password.as_ref(), r#"has "quotes""#);
1508 } else {
1509 unreachable!("expected SqlServer credentials");
1510 }
1511 }
1512
1513 #[test]
1514 fn test_quoted_value_escaped_single_quotes() {
1515 let config =
1516 Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1517 .unwrap();
1518 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1519 assert_eq!(password.as_ref(), "it's complex");
1520 } else {
1521 unreachable!("expected SqlServer credentials");
1522 }
1523 }
1524
1525 #[test]
1526 fn test_quoted_value_unterminated() {
1527 let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1528 assert!(result.is_err());
1529 assert!(result.unwrap_err().to_string().contains("unterminated"));
1530 }
1531
1532 #[test]
1533 fn test_tcp_prefix_stripped() {
1534 let config = Config::from_connection_string(
1536 "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1537 )
1538 .unwrap();
1539 assert_eq!(config.host, "myserver.database.windows.net");
1540 assert_eq!(config.port, 1433);
1541 }
1542
1543 #[test]
1544 fn test_tcp_prefix_mixed_case() {
1545 let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1547 assert_eq!(config.host, "myhost");
1548
1549 let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1550 assert_eq!(config.host, "myhost");
1551 }
1552
1553 #[test]
1554 fn test_tcp_prefix_with_instance() {
1555 let config =
1556 Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1557 assert_eq!(config.host, "myhost");
1558 assert_eq!(config.instance, Some("INST".to_string()));
1559 }
1560
1561 #[test]
1562 fn test_np_prefix_rejected() {
1563 let result =
1564 Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1565 assert!(result.is_err());
1566 assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1567
1568 let result =
1570 Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1571 assert!(result.is_err());
1572 }
1573
1574 #[test]
1575 fn test_lpc_prefix_rejected() {
1576 let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1577 assert!(result.is_err());
1578 assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1579 }
1580
1581 #[test]
1582 fn test_server_alias_addr() {
1583 let config = Config::from_connection_string("Addr=myhost;").unwrap();
1584 assert_eq!(config.host, "myhost");
1585 }
1586
1587 #[test]
1588 fn test_server_alias_address() {
1589 let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1590 assert_eq!(config.host, "myhost");
1591 assert_eq!(config.port, 1434);
1592 }
1593
1594 #[test]
1595 fn test_server_alias_network_address() {
1596 let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1597 assert_eq!(config.host, "myhost");
1598 }
1599
1600 #[test]
1601 fn test_timeout_alias() {
1602 let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1603 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1604 }
1605
1606 #[test]
1607 fn test_application_intent_readonly() {
1608 let config =
1609 Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1610 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1611 }
1612
1613 #[test]
1614 fn test_application_intent_readwrite() {
1615 let config =
1616 Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1617 .unwrap();
1618 assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1619 }
1620
1621 #[test]
1622 fn test_application_intent_invalid() {
1623 let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1624 assert!(result.is_err());
1625 assert!(
1626 result
1627 .unwrap_err()
1628 .to_string()
1629 .contains("ApplicationIntent")
1630 );
1631 }
1632
1633 #[test]
1634 fn test_workstation_id() {
1635 let config =
1636 Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1637 assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1638 }
1639
1640 #[test]
1641 fn test_wsid_alias() {
1642 let config =
1643 Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1644 assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1645 }
1646
1647 #[test]
1648 fn test_language() {
1649 let config =
1650 Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1651 assert_eq!(config.language, Some("us_english".to_string()));
1652 }
1653
1654 #[test]
1655 fn test_current_language_alias() {
1656 let config =
1657 Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1658 assert_eq!(config.language, Some("Deutsch".to_string()));
1659 }
1660
1661 #[test]
1662 fn test_connect_retry_count() {
1663 let config =
1664 Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1665 assert_eq!(config.retry.max_retries, 5);
1666 }
1667
1668 #[test]
1669 fn test_connect_retry_interval() {
1670 let config =
1671 Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1672 assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1673 }
1674
1675 #[test]
1676 fn test_pool_keywords_accepted_without_error() {
1677 let result = Config::from_connection_string(
1679 "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1680 );
1681 assert!(result.is_ok());
1682 }
1683
1684 #[test]
1685 fn test_known_unsupported_keywords_accepted() {
1686 let result = Config::from_connection_string(
1688 "Server=localhost;Failover Partner=backup;Persist Security Info=false;",
1689 );
1690 assert!(result.is_ok());
1691 }
1692
1693 #[test]
1694 fn test_multi_subnet_failover_connection_string() {
1695 let config =
1696 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=true;").unwrap();
1697 assert!(config.multi_subnet_failover);
1698
1699 let config =
1701 Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1702 .unwrap();
1703 assert!(config.multi_subnet_failover);
1704
1705 let config =
1707 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1708 .unwrap();
1709 assert!(!config.multi_subnet_failover);
1710
1711 let config = Config::from_connection_string("Server=localhost;").unwrap();
1713 assert!(!config.multi_subnet_failover);
1714 }
1715
1716 #[test]
1717 fn test_multi_subnet_failover_builder() {
1718 let config = Config::new().multi_subnet_failover(true);
1719 assert!(config.multi_subnet_failover);
1720
1721 let config = Config::new().multi_subnet_failover(false);
1722 assert!(!config.multi_subnet_failover);
1723 }
1724
1725 #[test]
1726 fn test_multi_subnet_failover_invalid_value() {
1727 let result = Config::from_connection_string("Server=localhost;MultiSubnetFailover=banana;");
1728 assert!(result.is_err());
1729 }
1730
1731 #[test]
1732 fn test_application_intent_builder() {
1733 let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1734 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1735 }
1736
1737 #[test]
1738 fn test_workstation_id_builder() {
1739 let config = Config::new().workstation_id("MY-PC");
1740 assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1741 }
1742
1743 #[test]
1744 fn test_language_builder() {
1745 let config = Config::new().language("us_english");
1746 assert_eq!(config.language, Some("us_english".to_string()));
1747 }
1748
1749 #[test]
1750 fn test_send_string_parameters_as_unicode_connection_string() {
1751 let config =
1752 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=false;")
1753 .unwrap();
1754 assert!(!config.send_string_parameters_as_unicode);
1755
1756 let config = Config::from_connection_string(
1758 "Server=localhost;Send String Parameters As Unicode=false;",
1759 )
1760 .unwrap();
1761 assert!(!config.send_string_parameters_as_unicode);
1762
1763 let config =
1765 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1766 .unwrap();
1767 assert!(config.send_string_parameters_as_unicode);
1768
1769 let config = Config::from_connection_string("Server=localhost;").unwrap();
1771 assert!(config.send_string_parameters_as_unicode);
1772 }
1773
1774 #[test]
1775 fn test_send_string_parameters_as_unicode_builder() {
1776 let config = Config::new().send_string_parameters_as_unicode(false);
1777 assert!(!config.send_string_parameters_as_unicode);
1778
1779 let config = Config::new().send_string_parameters_as_unicode(true);
1780 assert!(config.send_string_parameters_as_unicode);
1781 }
1782
1783 #[test]
1784 fn test_send_string_parameters_as_unicode_invalid_value() {
1785 let result = Config::from_connection_string(
1786 "Server=localhost;SendStringParametersAsUnicode=banana;",
1787 );
1788 assert!(result.is_err());
1789 }
1790
1791 #[test]
1792 fn test_empty_values_become_none() {
1793 let config =
1795 Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
1796 assert_eq!(config.database, None);
1797 assert_eq!(config.language, None);
1798 }
1799}