1mod types;
109pub use types::*;
110
111use std::time::Duration;
112
113use mssql_auth::Credentials;
114#[cfg(feature = "tls")]
115use mssql_tls::TlsConfig;
116use tds_protocol::version::TdsVersion;
117
118fn parse_conn_bool(key: &str, value: &str) -> Result<bool, crate::error::Error> {
124 match value.to_lowercase().as_str() {
125 "true" | "yes" | "1" => Ok(true),
126 "false" | "no" | "0" => Ok(false),
127 _ => Err(crate::error::Error::Config(format!(
128 "invalid boolean value for '{key}': '{value}' (expected true/false/yes/no/1/0)"
129 ))),
130 }
131}
132
133fn split_connection_string(conn_str: &str) -> Result<Vec<(String, String)>, crate::error::Error> {
142 let mut pairs = Vec::new();
143 let chars: Vec<char> = conn_str.chars().collect();
144 let len = chars.len();
145 let mut i = 0;
146
147 while i < len {
148 while i < len && (chars[i] == ';' || chars[i].is_whitespace()) {
150 i += 1;
151 }
152 if i >= len {
153 break;
154 }
155
156 let key_start = i;
158 while i < len && chars[i] != '=' {
159 i += 1;
160 }
161 if i >= len {
162 let remaining = chars[key_start..].iter().collect::<String>();
164 if remaining.trim().is_empty() {
165 break;
166 }
167 return Err(crate::error::Error::Config(format!(
168 "invalid key-value pair (missing '='): '{remaining}'"
169 )));
170 }
171 let key: String = chars[key_start..i].iter().collect();
172 i += 1; while i < len && chars[i].is_whitespace() {
177 i += 1;
178 }
179
180 let value = if i < len && (chars[i] == '"' || chars[i] == '\'') {
181 let quote_char = chars[i];
183 i += 1; let mut val = String::new();
185 loop {
186 if i >= len {
187 return Err(crate::error::Error::Config(format!(
188 "unterminated quoted value for key '{}'",
189 key.trim()
190 )));
191 }
192 if chars[i] == quote_char {
193 if i + 1 < len && chars[i + 1] == quote_char {
195 val.push(quote_char);
196 i += 2;
197 } else {
198 i += 1; break;
200 }
201 } else {
202 val.push(chars[i]);
203 i += 1;
204 }
205 }
206 while i < len && chars[i] != ';' {
208 i += 1;
209 }
210 val
211 } else {
212 let val_start = i;
214 while i < len && chars[i] != ';' {
215 i += 1;
216 }
217 chars[val_start..i].iter().collect::<String>()
218 };
219
220 let key_trimmed = key.trim().to_string();
221 if !key_trimmed.is_empty() {
222 pairs.push((key_trimmed, value));
223 }
224 }
225
226 Ok(pairs)
227}
228
229fn non_empty(value: &str) -> Option<String> {
234 if value.is_empty() {
235 None
236 } else {
237 Some(value.to_string())
238 }
239}
240
241#[derive(Debug, Clone)]
247#[non_exhaustive]
248pub struct Config {
249 pub host: String,
251
252 pub port: u16,
254
255 pub database: Option<String>,
257
258 pub credentials: Credentials,
260
261 #[cfg(feature = "tls")]
263 pub tls: TlsConfig,
264
265 pub application_name: String,
267
268 pub connect_timeout: Duration,
270
271 pub command_timeout: Duration,
273
274 pub max_response_size: usize,
284
285 pub packet_size: u16,
287
288 pub strict_mode: bool,
290
291 pub trust_server_certificate: bool,
293
294 pub instance: Option<String>,
296
297 pub mars: bool,
299
300 pub encrypt: bool,
304
305 pub no_tls: bool,
324
325 pub redirect: RedirectConfig,
327
328 pub retry: RetryPolicy,
330
331 pub timeouts: TimeoutConfig,
333
334 pub tds_version: TdsVersion,
347
348 pub application_intent: ApplicationIntent,
354
355 pub workstation_id: Option<String>,
362
363 pub language: Option<String>,
369
370 pub multi_subnet_failover: bool,
381
382 pub send_string_parameters_as_unicode: bool,
397
398 pub statement_cache: bool,
414
415 #[cfg(feature = "always-encrypted")]
427 pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
428}
429
430impl Default for Config {
431 fn default() -> Self {
432 let timeouts = TimeoutConfig::default();
433 Self {
434 host: "localhost".to_string(),
435 port: 1433,
436 database: None,
437 credentials: Credentials::sql_server("", ""),
438 #[cfg(feature = "tls")]
439 tls: TlsConfig::default(),
440 application_name: "mssql-client".to_string(),
441 connect_timeout: timeouts.connect_timeout,
442 command_timeout: timeouts.command_timeout,
443 max_response_size: 0,
444 packet_size: 4096,
445 strict_mode: false,
446 trust_server_certificate: false,
447 instance: None,
448 mars: false,
449 encrypt: true, no_tls: false, redirect: RedirectConfig::default(),
452 retry: RetryPolicy::default(),
453 timeouts,
454 tds_version: TdsVersion::V7_4, application_intent: ApplicationIntent::default(),
456 workstation_id: None,
457 language: None,
458 multi_subnet_failover: false,
459 send_string_parameters_as_unicode: true,
460 statement_cache: false,
461 #[cfg(feature = "always-encrypted")]
462 column_encryption: None,
463 }
464 }
465}
466
467impl Config {
468 #[must_use]
470 pub fn new() -> Self {
471 Self::default()
472 }
473
474 pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
485 let mut config = Self::default();
486 let pairs = split_connection_string(conn_str)?;
487
488 let mut authentication: Option<String> = None;
492
493 for (key, value) in &pairs {
494 let key = key.trim().to_lowercase();
495 let value = value.trim();
496
497 match key.as_str() {
498 "server" | "data source" | "addr" | "address" | "network address" | "host" => {
500 let lower_value = value.to_lowercase();
504 let server_value = if lower_value.starts_with("tcp:") {
505 &value[4..]
506 } else if lower_value.starts_with("np:") {
507 return Err(crate::error::Error::Config(
508 "Named Pipes connections (np:) are not supported. Use TCP connections instead."
509 .into(),
510 ));
511 } else if lower_value.starts_with("lpc:") {
512 return Err(crate::error::Error::Config(
513 "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
514 .into(),
515 ));
516 } else {
517 value
518 };
519
520 if let Some((host, port_or_instance)) = server_value.split_once(',') {
522 config.host = host.to_string();
523 config.port = port_or_instance.trim().parse().map_err(|_| {
524 crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
525 })?;
526 } else if let Some((host, instance)) = server_value.split_once('\\') {
527 config.host = host.to_string();
528 config.instance = non_empty(instance);
529 } else {
530 config.host = server_value.to_string();
531 }
532 }
533 "port" => {
534 config.port = value.parse().map_err(|_| {
535 crate::error::Error::Config(format!("invalid port: {value}"))
536 })?;
537 }
538 "database" | "initial catalog" => {
540 config.database = non_empty(value);
541 }
542 "user id" | "uid" | "user" => {
544 if let Credentials::SqlServer { password, .. } = &config.credentials {
545 config.credentials =
546 Credentials::sql_server(value.to_string(), password.clone());
547 }
548 }
549 "password" | "pwd" => {
550 if let Credentials::SqlServer { username, .. } = &config.credentials {
551 config.credentials =
552 Credentials::sql_server(username.clone(), value.to_string());
553 }
554 }
555 "authentication" => {
559 authentication = non_empty(value).map(|v| v.to_lowercase().replace(' ', ""));
560 }
561 "application name" | "app" => {
563 config.application_name = value.to_string();
564 }
565 "applicationintent" | "application intent" => {
566 config.application_intent = match value.to_lowercase().as_str() {
567 "readonly" => ApplicationIntent::ReadOnly,
568 "readwrite" => ApplicationIntent::ReadWrite,
569 _ => {
570 return Err(crate::error::Error::Config(format!(
571 "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
572 )));
573 }
574 };
575 }
576 "workstation id" | "wsid" => {
577 config.workstation_id = non_empty(value);
578 }
579 "current language" | "language" => {
580 config.language = non_empty(value);
581 }
582 "connect timeout" | "connection timeout" | "timeout" => {
584 let secs: u64 = value.parse().map_err(|_| {
585 crate::error::Error::Config(format!("invalid timeout: {value}"))
586 })?;
587 config.connect_timeout = Duration::from_secs(secs);
588 }
589 "command timeout" => {
590 let secs: u64 = value.parse().map_err(|_| {
591 crate::error::Error::Config(format!("invalid timeout: {value}"))
592 })?;
593 config.command_timeout = Duration::from_secs(secs);
594 }
595 "trustservercertificate" | "trust server certificate" => {
597 config.trust_server_certificate = parse_conn_bool(&key, value)?;
598 }
599 "encrypt" => {
600 if value.eq_ignore_ascii_case("strict") {
608 config.strict_mode = true;
609 config.encrypt = true;
610 config.no_tls = false;
611 } else if value.eq_ignore_ascii_case("mandatory") {
612 config.encrypt = true;
613 config.no_tls = false;
614 } else if value.eq_ignore_ascii_case("optional") {
615 config.encrypt = false;
616 config.no_tls = false;
617 } else if value.eq_ignore_ascii_case("no_tls") {
618 config.no_tls = true;
619 config.encrypt = false;
620 } else {
621 let enabled = parse_conn_bool(&key, value)?;
623 config.encrypt = enabled;
624 config.no_tls = false;
625 }
626 }
627 "integrated security" | "trusted_connection" => {
628 let enabled =
630 value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
631 if enabled {
632 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
633 {
634 config.credentials = Credentials::Integrated;
635 }
636 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
637 {
638 return Err(crate::error::Error::Config(
639 "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
640 or 'sspi-auth' (Windows) feature to be enabled"
641 .into(),
642 ));
643 }
644 }
645 }
646 "column encryption setting" | "columnencryptionsetting" => {
648 #[cfg(feature = "always-encrypted")]
649 if value.eq_ignore_ascii_case("enabled") {
650 config.column_encryption = Some(std::sync::Arc::new(
651 crate::encryption::EncryptionConfig::new(),
652 ));
653 }
654 #[cfg(not(feature = "always-encrypted"))]
655 if value.eq_ignore_ascii_case("enabled") {
656 return Err(crate::error::Error::Config(
657 "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
658 Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
659 .to_string(),
660 ));
661 }
662 }
663 "multipleactiveresultsets" | "mars" => {
665 config.mars = parse_conn_bool(&key, value)?;
666 }
667 "packet size" => {
668 config.packet_size = value.parse().map_err(|_| {
669 crate::error::Error::Config(format!("invalid packet size: {value}"))
670 })?;
671 }
672 "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
673 config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
674 crate::error::Error::Config(format!(
675 "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
676 ))
677 })?;
678 if config.tds_version.is_tds_8() {
679 config.strict_mode = true;
680 }
681 }
682 "connectretrycount" | "connect retry count" => {
684 config.retry.max_retries = value.parse().map_err(|_| {
685 crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
686 })?;
687 }
688 "connectretryinterval" | "connect retry interval" => {
689 let secs: u64 = value.parse().map_err(|_| {
690 crate::error::Error::Config(format!(
691 "invalid ConnectRetryInterval: '{value}'"
692 ))
693 })?;
694 config.retry.initial_backoff = Duration::from_secs(secs);
695 }
696 "max pool size"
698 | "min pool size"
699 | "pooling"
700 | "connection lifetime"
701 | "load balance timeout" => {
702 tracing::info!(
703 key = key.as_str(),
704 value = value,
705 "connection string keyword '{}' is recognized but pool settings \
706 must be configured via PoolConfig, not the connection string",
707 key,
708 );
709 }
710 "multisubnetfailover" | "multi subnet failover" => {
712 config.multi_subnet_failover = parse_conn_bool(&key, value)?;
713 }
714 "sendstringparametersasunicode" | "send string parameters as unicode" => {
716 config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
717 }
718 "statement cache" | "statementcache" => {
720 config.statement_cache = parse_conn_bool(&key, value)?;
721 }
722 "failover partner"
724 | "persist security info"
725 | "persistsecurityinfo"
726 | "enlist"
727 | "replication"
728 | "transaction binding"
729 | "type system version"
730 | "user instance"
731 | "attachdbfilename"
732 | "extended properties"
733 | "initial file name"
734 | "context connection"
735 | "network library"
736 | "network"
737 | "net"
738 | "asynchronous processing"
739 | "async"
740 | "transparentnetworkipresolution"
741 | "poolblockingperiod"
742 | "hostnameincertificate"
743 | "servercertificate" => {
744 tracing::info!(
745 key = key.as_str(),
746 value = value,
747 "connection string keyword '{}' is recognized but not supported by this driver",
748 key,
749 );
750 }
751 _ => {
752 tracing::debug!(
753 key = key.as_str(),
754 value = value,
755 "ignoring unknown connection string option"
756 );
757 }
758 }
759 }
760
761 if let Some(method) = authentication {
762 config.apply_authentication_keyword(&method)?;
763 }
764
765 Ok(config)
766 }
767
768 fn apply_authentication_keyword(&mut self, method: &str) -> Result<(), crate::error::Error> {
774 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
777 if matches!(self.credentials, Credentials::Integrated) {
778 return Err(crate::error::Error::Config(
779 "the Authentication keyword cannot be combined with Integrated Security".into(),
780 ));
781 }
782
783 match method {
784 "sqlpassword" => {}
787 "activedirectorymanagedidentity" | "activedirectorymsi" => {
788 #[cfg(not(feature = "azure-identity"))]
789 return Err(crate::error::Error::Config(format!(
790 "Authentication={method} requires the 'azure-identity' feature. \
791 Enable it in your Cargo.toml: \
792 mssql-client = {{ features = [\"azure-identity\"] }}"
793 )));
794 #[cfg(feature = "azure-identity")]
795 {
796 let client_id = match &self.credentials {
798 Credentials::SqlServer { username, .. } if !username.is_empty() => {
799 Some(username.clone())
800 }
801 _ => None,
802 };
803 self.credentials = Credentials::AzureManagedIdentity { client_id };
804 }
805 }
806 "activedirectoryserviceprincipal" => {
807 #[cfg(not(feature = "azure-identity"))]
808 return Err(crate::error::Error::Config(format!(
809 "Authentication={method} requires the 'azure-identity' feature. \
810 Enable it in your Cargo.toml: \
811 mssql-client = {{ features = [\"azure-identity\"] }}"
812 )));
813 #[cfg(feature = "azure-identity")]
814 {
815 let Credentials::SqlServer { username, password } = &self.credentials else {
816 return Err(crate::error::Error::Config(
817 "Authentication=ActiveDirectoryServicePrincipal requires \
818 User Id and Password (client id and secret)"
819 .into(),
820 ));
821 };
822 let Some((client_id, tenant_id)) = username.split_once('@') else {
826 return Err(crate::error::Error::Config(
827 "Authentication=ActiveDirectoryServicePrincipal requires \
828 User Id=<client-id>@<tenant-id> (the tenant id is needed \
829 for client-side token acquisition)"
830 .into(),
831 ));
832 };
833 if client_id.is_empty() || tenant_id.is_empty() {
834 return Err(crate::error::Error::Config(
835 "Authentication=ActiveDirectoryServicePrincipal: client id \
836 and tenant id must both be non-empty in \
837 User Id=<client-id>@<tenant-id>"
838 .into(),
839 ));
840 }
841 if password.is_empty() {
842 return Err(crate::error::Error::Config(
843 "Authentication=ActiveDirectoryServicePrincipal requires \
844 Password=<client secret>"
845 .into(),
846 ));
847 }
848 let (client_id, tenant_id) = (client_id.to_string(), tenant_id.to_string());
849 let client_secret = password.clone();
850 self.credentials = Credentials::AzureServicePrincipal {
851 tenant_id: tenant_id.into(),
852 client_id: client_id.into(),
853 client_secret,
854 };
855 }
856 }
857 "activedirectorydefault" => {
858 #[cfg(not(feature = "azure-identity"))]
859 return Err(crate::error::Error::Config(format!(
860 "Authentication={method} requires the 'azure-identity' feature. \
861 Enable it in your Cargo.toml: \
862 mssql-client = {{ features = [\"azure-identity\"] }}"
863 )));
864 #[cfg(feature = "azure-identity")]
865 {
866 self.credentials = Credentials::AzureDefault;
869 }
870 }
871 "activedirectorypassword"
872 | "activedirectoryintegrated"
873 | "activedirectoryinteractive"
874 | "activedirectorydevicecodeflow" => {
875 return Err(crate::error::Error::Config(format!(
876 "Authentication value '{method}' is not supported: azure_identity does \
877 not ship the interactive / username-password / device-code credentials. \
878 Acquire the token yourself and pass it via Credentials::azure_token. \
879 Built-in Authentication values: SqlPassword, \
880 ActiveDirectoryServicePrincipal, ActiveDirectoryManagedIdentity \
881 (alias ActiveDirectoryMSI), ActiveDirectoryDefault"
882 )));
883 }
884 other => {
885 return Err(crate::error::Error::Config(format!(
886 "invalid Authentication value: '{other}'. Supported values: \
887 SqlPassword, ActiveDirectoryServicePrincipal, \
888 ActiveDirectoryManagedIdentity (alias ActiveDirectoryMSI), \
889 ActiveDirectoryDefault"
890 )));
891 }
892 }
893
894 Ok(())
895 }
896
897 #[must_use]
899 pub fn host(mut self, host: impl Into<String>) -> Self {
900 self.host = host.into();
901 self
902 }
903
904 #[must_use]
906 pub fn port(mut self, port: u16) -> Self {
907 self.port = port;
908 self
909 }
910
911 #[must_use]
913 pub fn database(mut self, database: impl Into<String>) -> Self {
914 self.database = Some(database.into());
915 self
916 }
917
918 #[must_use]
920 pub fn credentials(mut self, credentials: Credentials) -> Self {
921 self.credentials = credentials;
922 self
923 }
924
925 #[must_use]
927 pub fn application_name(mut self, name: impl Into<String>) -> Self {
928 self.application_name = name.into();
929 self
930 }
931
932 #[must_use]
934 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
935 self.connect_timeout = timeout;
936 self
937 }
938
939 #[must_use]
941 pub fn trust_server_certificate(mut self, trust: bool) -> Self {
942 self.trust_server_certificate = trust;
943 #[cfg(feature = "tls")]
944 {
945 self.tls = self.tls.trust_server_certificate(trust);
946 }
947 self
948 }
949
950 #[must_use]
952 pub fn strict_mode(mut self, enabled: bool) -> Self {
953 self.strict_mode = enabled;
954 #[cfg(feature = "tls")]
955 {
956 self.tls = self.tls.strict_mode(enabled);
957 }
958 if enabled {
959 self.tds_version = TdsVersion::V8_0;
960 }
961 self
962 }
963
964 #[must_use]
988 pub fn tds_version(mut self, version: TdsVersion) -> Self {
989 self.tds_version = version;
990 if version.is_tds_8() {
992 self.strict_mode = true;
993 #[cfg(feature = "tls")]
994 {
995 self.tls = self.tls.strict_mode(true);
996 }
997 }
998 self
999 }
1000
1001 #[must_use]
1009 pub fn encrypt(mut self, enabled: bool) -> Self {
1010 self.encrypt = enabled;
1011 self
1012 }
1013
1014 #[must_use]
1055 pub fn no_tls(mut self, enabled: bool) -> Self {
1056 self.no_tls = enabled;
1057 if enabled {
1058 self.encrypt = false;
1059 }
1060 self
1061 }
1062
1063 #[cfg(feature = "always-encrypted")]
1085 #[must_use]
1086 pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
1087 self.column_encryption = Some(std::sync::Arc::new(config));
1088 self
1089 }
1090
1091 #[must_use]
1093 pub fn with_host(mut self, host: &str) -> Self {
1094 self.host = host.to_string();
1095 self
1096 }
1097
1098 #[must_use]
1100 pub fn with_port(mut self, port: u16) -> Self {
1101 self.port = port;
1102 self
1103 }
1104
1105 #[must_use]
1109 pub fn with_statement_cache(mut self, enabled: bool) -> Self {
1110 self.statement_cache = enabled;
1111 self
1112 }
1113
1114 #[must_use]
1116 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
1117 self.redirect = redirect;
1118 self
1119 }
1120
1121 #[must_use]
1123 pub fn max_redirects(mut self, max: u8) -> Self {
1124 self.redirect.max_redirects = max;
1125 self
1126 }
1127
1128 #[must_use]
1130 pub fn retry(mut self, retry: RetryPolicy) -> Self {
1131 self.retry = retry;
1132 self
1133 }
1134
1135 #[must_use]
1137 pub fn max_retries(mut self, max: u32) -> Self {
1138 self.retry.max_retries = max;
1139 self
1140 }
1141
1142 #[must_use]
1144 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
1145 self.connect_timeout = timeouts.connect_timeout;
1147 self.command_timeout = timeouts.command_timeout;
1148 self.timeouts = timeouts;
1149 self
1150 }
1151
1152 #[must_use]
1154 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
1155 self.application_intent = intent;
1156 self
1157 }
1158
1159 #[must_use]
1164 pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
1165 self.workstation_id = Some(id.into());
1166 self
1167 }
1168
1169 #[must_use]
1173 pub fn language(mut self, lang: impl Into<String>) -> Self {
1174 self.language = Some(lang.into());
1175 self
1176 }
1177
1178 #[must_use]
1183 pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
1184 self.multi_subnet_failover = enabled;
1185 self
1186 }
1187
1188 #[must_use]
1196 pub fn send_string_parameters_as_unicode(mut self, enabled: bool) -> Self {
1197 self.send_string_parameters_as_unicode = enabled;
1198 self
1199 }
1200}
1201
1202#[cfg(test)]
1203#[allow(clippy::unwrap_used)]
1204mod tests {
1205 use super::*;
1206
1207 #[cfg(feature = "azure-identity")]
1208 #[test]
1209 fn test_authentication_active_directory_default() {
1210 let config = Config::from_connection_string(
1211 "Server=db.example.com;Database=app;Authentication=ActiveDirectoryDefault;",
1212 )
1213 .unwrap();
1214 assert!(matches!(
1215 config.credentials,
1216 mssql_auth::Credentials::AzureDefault
1217 ));
1218 let spaced = Config::from_connection_string(
1220 "Server=db.example.com;Authentication=Active Directory Default;",
1221 )
1222 .unwrap();
1223 assert!(matches!(
1224 spaced.credentials,
1225 mssql_auth::Credentials::AzureDefault
1226 ));
1227 }
1228
1229 #[test]
1230 fn test_connection_string_parsing() {
1231 let config = Config::from_connection_string(
1232 "Server=localhost;Database=test;User Id=sa;Password=secret;",
1233 )
1234 .unwrap();
1235
1236 assert_eq!(config.host, "localhost");
1237 assert_eq!(config.database, Some("test".to_string()));
1238 }
1239
1240 #[test]
1241 fn test_connection_string_with_port() {
1242 let config =
1243 Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
1244
1245 assert_eq!(config.host, "localhost");
1246 assert_eq!(config.port, 1434);
1247 }
1248
1249 #[test]
1250 fn test_connection_string_with_instance() {
1251 let config =
1252 Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
1253
1254 assert_eq!(config.host, "localhost");
1255 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1256 }
1257
1258 #[test]
1259 fn test_connection_string_dot_instance() {
1260 let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
1262
1263 assert_eq!(config.host, ".");
1264 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1265 }
1266
1267 #[test]
1268 fn test_connection_string_local_instance() {
1269 let config =
1271 Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
1272
1273 assert_eq!(config.host, "(local)");
1274 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1275 }
1276
1277 #[test]
1278 fn test_redirect_config_defaults() {
1279 let config = RedirectConfig::default();
1280 assert_eq!(config.max_redirects, 2);
1281 assert!(config.follow_redirects);
1282 }
1283
1284 #[test]
1285 fn test_redirect_config_builder() {
1286 let config = RedirectConfig::new()
1287 .max_redirects(5)
1288 .follow_redirects(false);
1289 assert_eq!(config.max_redirects, 5);
1290 assert!(!config.follow_redirects);
1291 }
1292
1293 #[test]
1294 fn test_redirect_config_no_follow() {
1295 let config = RedirectConfig::no_follow();
1296 assert_eq!(config.max_redirects, 0);
1297 assert!(!config.follow_redirects);
1298 }
1299
1300 #[test]
1301 fn test_config_redirect_builder() {
1302 let config = Config::new().max_redirects(3);
1303 assert_eq!(config.redirect.max_redirects, 3);
1304
1305 let config2 = Config::new().redirect(RedirectConfig::no_follow());
1306 assert!(!config2.redirect.follow_redirects);
1307 }
1308
1309 #[test]
1310 fn test_retry_policy_defaults() {
1311 let policy = RetryPolicy::default();
1312 assert_eq!(policy.max_retries, 3);
1313 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
1314 assert_eq!(policy.max_backoff, Duration::from_secs(30));
1315 assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
1316 assert!(policy.jitter);
1317 }
1318
1319 #[test]
1320 fn test_retry_policy_builder() {
1321 let policy = RetryPolicy::new()
1322 .max_retries(5)
1323 .initial_backoff(Duration::from_millis(200))
1324 .max_backoff(Duration::from_secs(60))
1325 .backoff_multiplier(3.0)
1326 .jitter(false);
1327
1328 assert_eq!(policy.max_retries, 5);
1329 assert_eq!(policy.initial_backoff, Duration::from_millis(200));
1330 assert_eq!(policy.max_backoff, Duration::from_secs(60));
1331 assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
1332 assert!(!policy.jitter);
1333 }
1334
1335 #[test]
1336 fn test_retry_policy_no_retry() {
1337 let policy = RetryPolicy::no_retry();
1338 assert_eq!(policy.max_retries, 0);
1339 assert!(!policy.should_retry(0));
1340 }
1341
1342 #[test]
1343 fn test_retry_policy_should_retry() {
1344 let policy = RetryPolicy::new().max_retries(3);
1345 assert!(policy.should_retry(0));
1346 assert!(policy.should_retry(1));
1347 assert!(policy.should_retry(2));
1348 assert!(!policy.should_retry(3));
1349 assert!(!policy.should_retry(4));
1350 }
1351
1352 #[test]
1353 fn test_retry_policy_backoff_calculation() {
1354 let policy = RetryPolicy::new()
1355 .initial_backoff(Duration::from_millis(100))
1356 .backoff_multiplier(2.0)
1357 .max_backoff(Duration::from_secs(10))
1358 .jitter(false);
1359
1360 assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
1361 assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
1362 assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
1363 assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
1364 }
1365
1366 #[test]
1367 fn test_retry_policy_backoff_capped() {
1368 let policy = RetryPolicy::new()
1369 .initial_backoff(Duration::from_secs(1))
1370 .backoff_multiplier(10.0)
1371 .max_backoff(Duration::from_secs(5))
1372 .jitter(false);
1373
1374 assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
1376 }
1377
1378 #[test]
1379 fn test_config_retry_builder() {
1380 let config = Config::new().max_retries(5);
1381 assert_eq!(config.retry.max_retries, 5);
1382
1383 let config2 = Config::new().retry(RetryPolicy::no_retry());
1384 assert_eq!(config2.retry.max_retries, 0);
1385 }
1386
1387 #[test]
1388 fn test_timeout_config_defaults() {
1389 let config = TimeoutConfig::default();
1390 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1391 assert_eq!(config.tls_timeout, Duration::from_secs(10));
1392 assert_eq!(config.login_timeout, Duration::from_secs(30));
1393 assert_eq!(config.command_timeout, Duration::from_secs(30));
1394 assert_eq!(config.idle_timeout, Duration::from_secs(300));
1395 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1396 }
1397
1398 #[test]
1399 fn test_timeout_config_builder() {
1400 let config = TimeoutConfig::new()
1401 .connect_timeout(Duration::from_secs(5))
1402 .tls_timeout(Duration::from_secs(3))
1403 .login_timeout(Duration::from_secs(10))
1404 .command_timeout(Duration::from_secs(60))
1405 .idle_timeout(Duration::from_secs(600))
1406 .keepalive_interval(Some(Duration::from_secs(60)));
1407
1408 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1409 assert_eq!(config.tls_timeout, Duration::from_secs(3));
1410 assert_eq!(config.login_timeout, Duration::from_secs(10));
1411 assert_eq!(config.command_timeout, Duration::from_secs(60));
1412 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1413 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1414 }
1415
1416 #[test]
1417 fn test_timeout_config_no_keepalive() {
1418 let config = TimeoutConfig::new().no_keepalive();
1419 assert_eq!(config.keepalive_interval, None);
1420 }
1421
1422 #[test]
1423 fn test_timeout_config_total_connect() {
1424 let config = TimeoutConfig::new()
1425 .connect_timeout(Duration::from_secs(5))
1426 .tls_timeout(Duration::from_secs(3))
1427 .login_timeout(Duration::from_secs(10));
1428
1429 assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1431 }
1432
1433 #[test]
1434 fn test_config_timeouts_builder() {
1435 let timeouts = TimeoutConfig::new()
1436 .connect_timeout(Duration::from_secs(5))
1437 .command_timeout(Duration::from_secs(60));
1438
1439 let config = Config::new().timeouts(timeouts);
1440 assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1441 assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1442 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1444 assert_eq!(config.command_timeout, Duration::from_secs(60));
1445 }
1446
1447 #[test]
1448 fn test_tds_version_default() {
1449 let config = Config::default();
1450 assert_eq!(config.tds_version, TdsVersion::V7_4);
1451 assert!(!config.strict_mode);
1452 }
1453
1454 #[test]
1455 fn test_tds_version_builder() {
1456 let config = Config::new().tds_version(TdsVersion::V7_3A);
1457 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1458 assert!(!config.strict_mode);
1459
1460 let config = Config::new().tds_version(TdsVersion::V7_3B);
1461 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1462 assert!(!config.strict_mode);
1463
1464 let config = Config::new().tds_version(TdsVersion::V8_0);
1466 assert_eq!(config.tds_version, TdsVersion::V8_0);
1467 assert!(config.strict_mode);
1468 }
1469
1470 #[test]
1471 fn test_strict_mode_sets_tds_8() {
1472 let config = Config::new().strict_mode(true);
1473 assert!(config.strict_mode);
1474 assert_eq!(config.tds_version, TdsVersion::V8_0);
1475 }
1476
1477 #[test]
1478 fn test_connection_string_tds_version() {
1479 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1481 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1482
1483 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1485 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1486
1487 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1489 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1490
1491 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1493 assert_eq!(config.tds_version, TdsVersion::V7_4);
1494
1495 let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1497 assert_eq!(config.tds_version, TdsVersion::V8_0);
1498 assert!(config.strict_mode);
1499
1500 let config =
1502 Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1503 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1504 }
1505
1506 #[test]
1507 fn test_connection_string_invalid_tds_version() {
1508 let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1509 assert!(result.is_err());
1510
1511 let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1512 assert!(result.is_err());
1513 }
1514
1515 #[test]
1516 fn test_connection_string_no_tls() {
1517 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1519 assert!(config.no_tls);
1520 assert!(!config.encrypt);
1521 assert!(!config.strict_mode);
1522
1523 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1525 assert!(config.no_tls);
1526
1527 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1529 assert!(!config.no_tls);
1530 assert!(config.encrypt);
1531
1532 let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1534 assert!(!config.no_tls);
1535 assert!(config.encrypt);
1536 assert!(config.strict_mode);
1537
1538 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1540 assert!(config.encrypt);
1541 assert!(!config.no_tls);
1542
1543 let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1545 assert!(!config.encrypt);
1546 assert!(!config.no_tls);
1547 }
1548
1549 #[test]
1550 fn test_no_tls_builder() {
1551 let config = Config::new().no_tls(true);
1553 assert!(config.no_tls);
1554 assert!(!config.encrypt);
1555
1556 let config = Config::new().no_tls(true).no_tls(false);
1558 assert!(!config.no_tls);
1559 }
1560
1561 #[test]
1562 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1563 fn test_connection_string_integrated_security() {
1564 let config =
1566 Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1567 assert_eq!(
1568 config.credentials.method_name(),
1569 "Integrated Authentication"
1570 );
1571
1572 let config =
1574 Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1575 assert_eq!(
1576 config.credentials.method_name(),
1577 "Integrated Authentication"
1578 );
1579
1580 let config =
1582 Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1583 assert_eq!(
1584 config.credentials.method_name(),
1585 "Integrated Authentication"
1586 );
1587
1588 let config =
1590 Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1591 assert_eq!(
1592 config.credentials.method_name(),
1593 "Integrated Authentication"
1594 );
1595
1596 let config =
1598 Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1599 assert_eq!(
1600 config.credentials.method_name(),
1601 "Integrated Authentication"
1602 );
1603 }
1604
1605 #[test]
1606 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1607 fn test_connection_string_integrated_security_without_feature() {
1608 let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1610 assert!(result.is_err());
1611 let err = result.unwrap_err().to_string();
1612 assert!(err.contains("integrated-auth"));
1613 }
1614
1615 #[test]
1620 fn test_parse_conn_bool_all_values() {
1621 assert!(parse_conn_bool("test", "true").unwrap());
1622 assert!(parse_conn_bool("test", "True").unwrap());
1623 assert!(parse_conn_bool("test", "TRUE").unwrap());
1624 assert!(parse_conn_bool("test", "yes").unwrap());
1625 assert!(parse_conn_bool("test", "Yes").unwrap());
1626 assert!(parse_conn_bool("test", "1").unwrap());
1627
1628 assert!(!parse_conn_bool("test", "false").unwrap());
1629 assert!(!parse_conn_bool("test", "False").unwrap());
1630 assert!(!parse_conn_bool("test", "FALSE").unwrap());
1631 assert!(!parse_conn_bool("test", "no").unwrap());
1632 assert!(!parse_conn_bool("test", "No").unwrap());
1633 assert!(!parse_conn_bool("test", "0").unwrap());
1634
1635 assert!(parse_conn_bool("test", "banana").is_err());
1637 assert!(parse_conn_bool("test", "tru").is_err());
1638 assert!(parse_conn_bool("test", "").is_err());
1639 }
1640
1641 #[test]
1642 fn test_boolean_validation_trust_server_certificate() {
1643 let config =
1645 Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1646 .unwrap();
1647 assert!(config.trust_server_certificate);
1648
1649 let config =
1650 Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1651 assert!(!config.trust_server_certificate);
1652
1653 let result =
1655 Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1656 assert!(result.is_err());
1657 assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1658 }
1659
1660 #[test]
1661 fn test_boolean_validation_mars() {
1662 let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1663 assert!(config.mars);
1664
1665 let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1667 assert!(result.is_err());
1668 }
1669
1670 #[test]
1671 fn test_quoted_value_semicolon() {
1672 let config = Config::from_connection_string(
1674 r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1675 )
1676 .unwrap();
1677 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1678 assert_eq!(password.as_ref(), "my;complex;pass");
1679 } else {
1680 unreachable!("expected SqlServer credentials");
1681 }
1682 }
1683
1684 #[test]
1685 fn test_quoted_value_single_quotes() {
1686 let config =
1687 Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1688 .unwrap();
1689 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1690 assert_eq!(password.as_ref(), "my;pass");
1691 } else {
1692 unreachable!("expected SqlServer credentials");
1693 }
1694 }
1695
1696 #[test]
1697 fn test_quoted_value_escaped_double_quotes() {
1698 let config = Config::from_connection_string(
1700 r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1701 )
1702 .unwrap();
1703 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1704 assert_eq!(password.as_ref(), r#"has "quotes""#);
1705 } else {
1706 unreachable!("expected SqlServer credentials");
1707 }
1708 }
1709
1710 #[test]
1711 fn test_quoted_value_escaped_single_quotes() {
1712 let config =
1713 Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1714 .unwrap();
1715 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1716 assert_eq!(password.as_ref(), "it's complex");
1717 } else {
1718 unreachable!("expected SqlServer credentials");
1719 }
1720 }
1721
1722 #[test]
1723 fn test_quoted_value_unterminated() {
1724 let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1725 assert!(result.is_err());
1726 assert!(result.unwrap_err().to_string().contains("unterminated"));
1727 }
1728
1729 #[test]
1730 fn test_tcp_prefix_stripped() {
1731 let config = Config::from_connection_string(
1733 "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1734 )
1735 .unwrap();
1736 assert_eq!(config.host, "myserver.database.windows.net");
1737 assert_eq!(config.port, 1433);
1738 }
1739
1740 #[test]
1741 fn test_tcp_prefix_mixed_case() {
1742 let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1744 assert_eq!(config.host, "myhost");
1745
1746 let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1747 assert_eq!(config.host, "myhost");
1748 }
1749
1750 #[test]
1751 fn test_tcp_prefix_with_instance() {
1752 let config =
1753 Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1754 assert_eq!(config.host, "myhost");
1755 assert_eq!(config.instance, Some("INST".to_string()));
1756 }
1757
1758 #[test]
1759 fn test_np_prefix_rejected() {
1760 let result =
1761 Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1762 assert!(result.is_err());
1763 assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1764
1765 let result =
1767 Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1768 assert!(result.is_err());
1769 }
1770
1771 #[test]
1772 fn test_lpc_prefix_rejected() {
1773 let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1774 assert!(result.is_err());
1775 assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1776 }
1777
1778 #[test]
1779 fn test_server_alias_addr() {
1780 let config = Config::from_connection_string("Addr=myhost;").unwrap();
1781 assert_eq!(config.host, "myhost");
1782 }
1783
1784 #[test]
1785 fn test_server_alias_address() {
1786 let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1787 assert_eq!(config.host, "myhost");
1788 assert_eq!(config.port, 1434);
1789 }
1790
1791 #[test]
1792 fn test_server_alias_network_address() {
1793 let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1794 assert_eq!(config.host, "myhost");
1795 }
1796
1797 #[test]
1798 fn test_timeout_alias() {
1799 let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1800 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1801 }
1802
1803 #[test]
1804 fn test_application_intent_readonly() {
1805 let config =
1806 Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1807 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1808 }
1809
1810 #[test]
1811 fn test_application_intent_readwrite() {
1812 let config =
1813 Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1814 .unwrap();
1815 assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1816 }
1817
1818 #[test]
1819 fn test_application_intent_invalid() {
1820 let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1821 assert!(result.is_err());
1822 assert!(
1823 result
1824 .unwrap_err()
1825 .to_string()
1826 .contains("ApplicationIntent")
1827 );
1828 }
1829
1830 #[test]
1831 fn test_workstation_id() {
1832 let config =
1833 Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1834 assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1835 }
1836
1837 #[test]
1838 fn test_wsid_alias() {
1839 let config =
1840 Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1841 assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1842 }
1843
1844 #[test]
1845 fn test_language() {
1846 let config =
1847 Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1848 assert_eq!(config.language, Some("us_english".to_string()));
1849 }
1850
1851 #[test]
1852 fn test_current_language_alias() {
1853 let config =
1854 Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1855 assert_eq!(config.language, Some("Deutsch".to_string()));
1856 }
1857
1858 #[test]
1859 fn test_connect_retry_count() {
1860 let config =
1861 Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1862 assert_eq!(config.retry.max_retries, 5);
1863 }
1864
1865 #[test]
1866 fn test_connect_retry_interval() {
1867 let config =
1868 Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1869 assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1870 }
1871
1872 #[test]
1873 fn test_pool_keywords_accepted_without_error() {
1874 let result = Config::from_connection_string(
1876 "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1877 );
1878 assert!(result.is_ok());
1879 }
1880
1881 #[test]
1882 fn test_known_unsupported_keywords_accepted() {
1883 let result = Config::from_connection_string(
1885 "Server=localhost;Failover Partner=backup;Persist Security Info=false;",
1886 );
1887 assert!(result.is_ok());
1888 }
1889
1890 #[test]
1891 fn test_multi_subnet_failover_connection_string() {
1892 let config =
1893 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=true;").unwrap();
1894 assert!(config.multi_subnet_failover);
1895
1896 let config =
1898 Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1899 .unwrap();
1900 assert!(config.multi_subnet_failover);
1901
1902 let config =
1904 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1905 .unwrap();
1906 assert!(!config.multi_subnet_failover);
1907
1908 let config = Config::from_connection_string("Server=localhost;").unwrap();
1910 assert!(!config.multi_subnet_failover);
1911 }
1912
1913 #[test]
1914 fn test_multi_subnet_failover_builder() {
1915 let config = Config::new().multi_subnet_failover(true);
1916 assert!(config.multi_subnet_failover);
1917
1918 let config = Config::new().multi_subnet_failover(false);
1919 assert!(!config.multi_subnet_failover);
1920 }
1921
1922 #[test]
1923 fn test_multi_subnet_failover_invalid_value() {
1924 let result = Config::from_connection_string("Server=localhost;MultiSubnetFailover=banana;");
1925 assert!(result.is_err());
1926 }
1927
1928 #[test]
1929 fn test_application_intent_builder() {
1930 let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1931 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1932 }
1933
1934 #[test]
1935 fn test_workstation_id_builder() {
1936 let config = Config::new().workstation_id("MY-PC");
1937 assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1938 }
1939
1940 #[test]
1941 fn test_language_builder() {
1942 let config = Config::new().language("us_english");
1943 assert_eq!(config.language, Some("us_english".to_string()));
1944 }
1945
1946 #[test]
1947 fn test_send_string_parameters_as_unicode_connection_string() {
1948 let config =
1949 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=false;")
1950 .unwrap();
1951 assert!(!config.send_string_parameters_as_unicode);
1952
1953 let config = Config::from_connection_string(
1955 "Server=localhost;Send String Parameters As Unicode=false;",
1956 )
1957 .unwrap();
1958 assert!(!config.send_string_parameters_as_unicode);
1959
1960 let config =
1962 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1963 .unwrap();
1964 assert!(config.send_string_parameters_as_unicode);
1965
1966 let config = Config::from_connection_string("Server=localhost;").unwrap();
1968 assert!(config.send_string_parameters_as_unicode);
1969 }
1970
1971 #[test]
1972 fn test_send_string_parameters_as_unicode_builder() {
1973 let config = Config::new().send_string_parameters_as_unicode(false);
1974 assert!(!config.send_string_parameters_as_unicode);
1975
1976 let config = Config::new().send_string_parameters_as_unicode(true);
1977 assert!(config.send_string_parameters_as_unicode);
1978 }
1979
1980 #[test]
1981 fn test_send_string_parameters_as_unicode_invalid_value() {
1982 let result = Config::from_connection_string(
1983 "Server=localhost;SendStringParametersAsUnicode=banana;",
1984 );
1985 assert!(result.is_err());
1986 }
1987
1988 #[test]
1989 fn test_statement_cache_default_off() {
1990 assert!(!Config::new().statement_cache);
1991 }
1992
1993 #[test]
1994 fn test_statement_cache_connection_string() {
1995 let config =
1996 Config::from_connection_string("Server=localhost;Statement Cache=true;").unwrap();
1997 assert!(config.statement_cache);
1998
1999 let config =
2000 Config::from_connection_string("Server=localhost;StatementCache=false;").unwrap();
2001 assert!(!config.statement_cache);
2002 }
2003
2004 #[test]
2005 fn test_statement_cache_builder() {
2006 assert!(Config::new().with_statement_cache(true).statement_cache);
2007 assert!(!Config::new().with_statement_cache(false).statement_cache);
2008 }
2009
2010 #[test]
2011 fn test_statement_cache_invalid_value() {
2012 let result = Config::from_connection_string("Server=localhost;Statement Cache=banana;");
2013 assert!(result.is_err());
2014 }
2015
2016 #[test]
2017 fn test_empty_values_become_none() {
2018 let config =
2020 Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
2021 assert_eq!(config.database, None);
2022 assert_eq!(config.language, None);
2023 }
2024}