Skip to main content

exarrow_rs/connection/
params.rs

1//! Connection parameter parsing and validation.
2//!
3//! This module handles parsing connection strings and building connection
4//! parameters with validation.
5
6use crate::error::ConnectionError;
7use std::collections::HashMap;
8use std::fmt;
9use std::str::FromStr;
10use std::time::Duration;
11
12/// Connection parameters for establishing a database connection.
13#[derive(Clone)]
14pub struct ConnectionParams {
15    /// Database host address
16    pub host: String,
17
18    /// Database port (default: 8563)
19    pub port: u16,
20
21    /// Username for authentication
22    pub username: String,
23
24    /// Password for authentication (stored securely)
25    password: String,
26
27    /// Optional schema to use after connection
28    pub schema: Option<String>,
29
30    /// Connection timeout
31    pub connection_timeout: Duration,
32
33    /// Query execution timeout
34    pub query_timeout: Duration,
35
36    /// Idle connection timeout
37    pub idle_timeout: Duration,
38
39    /// Enable TLS/SSL encryption
40    pub use_tls: bool,
41
42    /// TLS certificate validation mode
43    pub validate_server_certificate: bool,
44
45    /// Client name for session identification
46    pub client_name: String,
47
48    /// Client version
49    pub client_version: String,
50
51    /// Additional connection attributes
52    pub attributes: HashMap<String, String>,
53}
54
55impl ConnectionParams {
56    /// Get the password (for internal use only, never logged).
57    pub(crate) fn password(&self) -> &str {
58        &self.password
59    }
60
61    /// Create a new ConnectionBuilder.
62    pub fn builder() -> ConnectionBuilder {
63        ConnectionBuilder::new()
64    }
65}
66
67impl FromStr for ConnectionParams {
68    type Err = ConnectionError;
69
70    /// Parse a connection string in the format:
71    /// `exasol://username[:password]@host[:port][/schema][?param=value&...]`
72    ///
73    /// # Examples
74    ///
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        // Parse the connection string
77        let url = s.trim();
78
79        // Check for exasol:// prefix
80        if !url.starts_with("exasol://") {
81            return Err(ConnectionError::ParseError(
82                "Connection string must start with 'exasol://'".to_string(),
83            ));
84        }
85
86        let url = &url[9..]; // Skip "exasol://"
87
88        // Split into main part and query string
89        let (main_part, query_string) = match url.split_once('?') {
90            Some((main, query)) => (main, Some(query)),
91            None => (url, None),
92        };
93
94        // Parse query parameters
95        let mut params = parse_query_params(query_string)?;
96
97        // Split main part into auth@host/schema
98        let (auth_part, host_part) = match main_part.rfind('@') {
99            Some(pos) => {
100                let auth = &main_part[..pos];
101                let host = &main_part[pos + 1..];
102                (Some(auth), host)
103            }
104            None => (None, main_part),
105        };
106
107        // Parse authentication
108        let (username, password) = if let Some(auth) = auth_part {
109            parse_auth(auth)?
110        } else {
111            // Check query params for username/password
112            let username = params
113                .remove("user")
114                .or_else(|| params.remove("username"))
115                .ok_or_else(|| ConnectionError::ParseError("Username is required".to_string()))?;
116            let password = params
117                .remove("password")
118                .or_else(|| params.remove("pass"))
119                .unwrap_or_default();
120            (username, password)
121        };
122
123        // Parse host and schema
124        let (host_port, schema) = match host_part.split_once('/') {
125            Some((host, schema)) => {
126                let schema = if schema.is_empty() {
127                    None
128                } else {
129                    Some(schema.to_string())
130                };
131                (host, schema)
132            }
133            None => (host_part, None),
134        };
135
136        // Parse host and port
137        let (host, port) = parse_host_port(host_port)?;
138
139        // Build connection params
140        let mut builder = ConnectionBuilder::new()
141            .host(&host)
142            .port(port)
143            .username(&username)
144            .password(&password);
145
146        if let Some(schema) = schema {
147            builder = builder.schema(&schema);
148        }
149
150        // Apply query parameters
151        builder = apply_query_params(builder, params)?;
152
153        builder.build()
154    }
155}
156
157// Prevent password from being displayed in debug or display output
158impl fmt::Debug for ConnectionParams {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("ConnectionParams")
161            .field("host", &self.host)
162            .field("port", &self.port)
163            .field("username", &self.username)
164            .field("password", &"<redacted>")
165            .field("schema", &self.schema)
166            .field("connection_timeout", &self.connection_timeout)
167            .field("query_timeout", &self.query_timeout)
168            .field("idle_timeout", &self.idle_timeout)
169            .field("use_tls", &self.use_tls)
170            .field(
171                "validate_server_certificate",
172                &self.validate_server_certificate,
173            )
174            .field("client_name", &self.client_name)
175            .field("client_version", &self.client_version)
176            .field("attributes", &self.attributes)
177            .finish()
178    }
179}
180
181impl fmt::Display for ConnectionParams {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(
184            f,
185            "ConnectionParams {{ host: {}, port: {}, username: {}, schema: {:?}, use_tls: {} }}",
186            self.host, self.port, self.username, self.schema, self.use_tls
187        )
188    }
189}
190
191/// Builder for constructing ConnectionParams with validation.
192#[derive(Debug, Clone)]
193pub struct ConnectionBuilder {
194    host: Option<String>,
195    port: Option<u16>,
196    username: Option<String>,
197    password: Option<String>,
198    schema: Option<String>,
199    connection_timeout: Option<Duration>,
200    query_timeout: Option<Duration>,
201    idle_timeout: Option<Duration>,
202    use_tls: Option<bool>,
203    validate_server_certificate: Option<bool>,
204    client_name: Option<String>,
205    client_version: Option<String>,
206    attributes: HashMap<String, String>,
207}
208
209impl ConnectionBuilder {
210    /// Create a new ConnectionBuilder with default values.
211    pub fn new() -> Self {
212        Self {
213            host: None,
214            port: None,
215            username: None,
216            password: None,
217            schema: None,
218            connection_timeout: None,
219            query_timeout: None,
220            idle_timeout: None,
221            use_tls: None,
222            validate_server_certificate: None,
223            client_name: None,
224            client_version: None,
225            attributes: HashMap::new(),
226        }
227    }
228
229    /// Set the database host.
230    pub fn host(mut self, host: &str) -> Self {
231        self.host = Some(host.to_string());
232        self
233    }
234
235    /// Set the database port.
236    pub fn port(mut self, port: u16) -> Self {
237        self.port = Some(port);
238        self
239    }
240
241    /// Set the username.
242    pub fn username(mut self, username: &str) -> Self {
243        self.username = Some(username.to_string());
244        self
245    }
246
247    /// Set the password.
248    pub fn password(mut self, password: &str) -> Self {
249        self.password = Some(password.to_string());
250        self
251    }
252
253    /// Set the default schema.
254    pub fn schema(mut self, schema: &str) -> Self {
255        self.schema = Some(schema.to_string());
256        self
257    }
258
259    /// Set the connection timeout.
260    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
261        self.connection_timeout = Some(timeout);
262        self
263    }
264
265    /// Set the query execution timeout.
266    pub fn query_timeout(mut self, timeout: Duration) -> Self {
267        self.query_timeout = Some(timeout);
268        self
269    }
270
271    /// Set the idle connection timeout.
272    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
273        self.idle_timeout = Some(timeout);
274        self
275    }
276
277    /// Enable or disable TLS/SSL.
278    pub fn use_tls(mut self, use_tls: bool) -> Self {
279        self.use_tls = Some(use_tls);
280        self
281    }
282
283    /// Enable or disable server certificate validation.
284    pub fn validate_server_certificate(mut self, validate: bool) -> Self {
285        self.validate_server_certificate = Some(validate);
286        self
287    }
288
289    /// Set the client name.
290    pub fn client_name(mut self, name: &str) -> Self {
291        self.client_name = Some(name.to_string());
292        self
293    }
294
295    /// Set the client version.
296    pub fn client_version(mut self, version: &str) -> Self {
297        self.client_version = Some(version.to_string());
298        self
299    }
300
301    /// Add a custom connection attribute.
302    pub fn attribute(mut self, key: &str, value: &str) -> Self {
303        self.attributes.insert(key.to_string(), value.to_string());
304        self
305    }
306
307    /// Build the ConnectionParams with validation.
308    pub fn build(self) -> Result<ConnectionParams, ConnectionError> {
309        // Validate required fields
310        let host = self.host.ok_or_else(|| ConnectionError::InvalidParameter {
311            parameter: "host".to_string(),
312            message: "Host is required".to_string(),
313        })?;
314
315        let username = self
316            .username
317            .ok_or_else(|| ConnectionError::InvalidParameter {
318                parameter: "username".to_string(),
319                message: "Username is required".to_string(),
320            })?;
321
322        // Validate host is not empty
323        if host.is_empty() {
324            return Err(ConnectionError::InvalidParameter {
325                parameter: "host".to_string(),
326                message: "Host cannot be empty".to_string(),
327            });
328        }
329
330        // Validate username is not empty
331        if username.is_empty() {
332            return Err(ConnectionError::InvalidParameter {
333                parameter: "username".to_string(),
334                message: "Username cannot be empty".to_string(),
335            });
336        }
337
338        let port = self.port.unwrap_or(8563);
339
340        // Validate port range
341        if port == 0 {
342            return Err(ConnectionError::InvalidParameter {
343                parameter: "port".to_string(),
344                message: "Port must be greater than 0".to_string(),
345            });
346        }
347
348        // Validate timeouts
349        let connection_timeout = self.connection_timeout.unwrap_or(Duration::from_secs(30));
350        let query_timeout = self.query_timeout.unwrap_or(Duration::from_secs(300));
351        let idle_timeout = self.idle_timeout.unwrap_or(Duration::from_secs(600));
352
353        if connection_timeout.as_secs() > 300 {
354            return Err(ConnectionError::InvalidParameter {
355                parameter: "connection_timeout".to_string(),
356                message: "Connection timeout cannot exceed 300 seconds".to_string(),
357            });
358        }
359
360        Ok(ConnectionParams {
361            host,
362            port,
363            username,
364            password: self.password.unwrap_or_default(),
365            schema: self.schema,
366            connection_timeout,
367            query_timeout,
368            idle_timeout,
369            use_tls: self.use_tls.unwrap_or(false),
370            validate_server_certificate: self.validate_server_certificate.unwrap_or(true),
371            client_name: self.client_name.unwrap_or_else(|| "exarrow-rs".to_string()),
372            client_version: self
373                .client_version
374                .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
375            attributes: self.attributes,
376        })
377    }
378}
379
380impl Default for ConnectionBuilder {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386/// Parse query parameters from URL query string.
387fn parse_query_params(query: Option<&str>) -> Result<HashMap<String, String>, ConnectionError> {
388    let mut params = HashMap::new();
389
390    if let Some(query) = query {
391        for pair in query.split('&') {
392            if pair.is_empty() {
393                continue;
394            }
395
396            let (key, value) = match pair.split_once('=') {
397                Some((k, v)) => (k, v),
398                None => {
399                    return Err(ConnectionError::ParseError(format!(
400                        "Invalid query parameter format: {}",
401                        pair
402                    )));
403                }
404            };
405
406            // URL decode the values
407            let key = urlencoding::decode(key)
408                .map_err(|e| ConnectionError::ParseError(format!("Failed to decode key: {}", e)))?
409                .into_owned();
410            let value = urlencoding::decode(value)
411                .map_err(|e| ConnectionError::ParseError(format!("Failed to decode value: {}", e)))?
412                .into_owned();
413
414            params.insert(key, value);
415        }
416    }
417
418    Ok(params)
419}
420
421/// Parse authentication part (username:password).
422fn parse_auth(auth: &str) -> Result<(String, String), ConnectionError> {
423    match auth.split_once(':') {
424        Some((user, pass)) => {
425            let user = urlencoding::decode(user)
426                .map_err(|e| {
427                    ConnectionError::ParseError(format!("Failed to decode username: {}", e))
428                })?
429                .into_owned();
430            let pass = urlencoding::decode(pass)
431                .map_err(|e| {
432                    ConnectionError::ParseError(format!("Failed to decode password: {}", e))
433                })?
434                .into_owned();
435            Ok((user, pass))
436        }
437        None => {
438            let user = urlencoding::decode(auth)
439                .map_err(|e| {
440                    ConnectionError::ParseError(format!("Failed to decode username: {}", e))
441                })?
442                .into_owned();
443            Ok((user, String::new()))
444        }
445    }
446}
447
448/// Parse host and port.
449fn parse_host_port(host_port: &str) -> Result<(String, u16), ConnectionError> {
450    // Check for IPv6 address format [host]:port
451    if host_port.starts_with('[') {
452        if let Some(close_bracket) = host_port.find(']') {
453            let host = host_port[1..close_bracket].to_string();
454            let port_part = &host_port[close_bracket + 1..];
455
456            let port = if let Some(stripped) = port_part.strip_prefix(':') {
457                stripped.parse().map_err(|_| {
458                    ConnectionError::ParseError(format!("Invalid port: {}", port_part))
459                })?
460            } else {
461                8563
462            };
463
464            return Ok((host, port));
465        }
466    }
467
468    // Regular host:port or just host
469    match host_port.rsplit_once(':') {
470        Some((host, port_str)) => {
471            let port = port_str
472                .parse()
473                .map_err(|_| ConnectionError::ParseError(format!("Invalid port: {}", port_str)))?;
474            Ok((host.to_string(), port))
475        }
476        None => Ok((host_port.to_string(), 8563)),
477    }
478}
479
480/// Apply query parameters to builder.
481fn apply_query_params(
482    mut builder: ConnectionBuilder,
483    params: HashMap<String, String>,
484) -> Result<ConnectionBuilder, ConnectionError> {
485    for (key, value) in params {
486        match key.as_str() {
487            "timeout" | "connection_timeout" => {
488                let secs: u64 = value
489                    .parse()
490                    .map_err(|_| ConnectionError::InvalidParameter {
491                        parameter: key.clone(),
492                        message: format!("Invalid timeout value: {}", value),
493                    })?;
494                builder = builder.connection_timeout(Duration::from_secs(secs));
495            }
496            "query_timeout" => {
497                let secs: u64 = value
498                    .parse()
499                    .map_err(|_| ConnectionError::InvalidParameter {
500                        parameter: key.clone(),
501                        message: format!("Invalid timeout value: {}", value),
502                    })?;
503                builder = builder.query_timeout(Duration::from_secs(secs));
504            }
505            "idle_timeout" => {
506                let secs: u64 = value
507                    .parse()
508                    .map_err(|_| ConnectionError::InvalidParameter {
509                        parameter: key.clone(),
510                        message: format!("Invalid timeout value: {}", value),
511                    })?;
512                builder = builder.idle_timeout(Duration::from_secs(secs));
513            }
514            "tls" | "use_tls" | "ssl" => {
515                let use_tls = parse_bool(&value)?;
516                builder = builder.use_tls(use_tls);
517            }
518            "validate_certificate" | "verify_certificate" | "validateservercertificate" => {
519                let validate = parse_bool(&value)?;
520                builder = builder.validate_server_certificate(validate);
521            }
522            "client_name" => {
523                builder = builder.client_name(&value);
524            }
525            "client_version" => {
526                builder = builder.client_version(&value);
527            }
528            _ => {
529                // Store as custom attribute
530                builder = builder.attribute(&key, &value);
531            }
532        }
533    }
534
535    Ok(builder)
536}
537
538/// Parse boolean value from string.
539fn parse_bool(s: &str) -> Result<bool, ConnectionError> {
540    match s.to_lowercase().as_str() {
541        "true" | "1" | "yes" | "on" => Ok(true),
542        "false" | "0" | "no" | "off" => Ok(false),
543        _ => Err(ConnectionError::InvalidParameter {
544            parameter: "boolean".to_string(),
545            message: format!("Invalid boolean value: {}", s),
546        }),
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    #[test]
555    fn test_builder_minimal() {
556        let params = ConnectionBuilder::new()
557            .host("localhost")
558            .username("test")
559            .build()
560            .unwrap();
561
562        assert_eq!(params.host, "localhost");
563        assert_eq!(params.port, 8563);
564        assert_eq!(params.username, "test");
565        assert_eq!(params.password(), "");
566    }
567
568    #[test]
569    fn test_builder_full() {
570        let params = ConnectionBuilder::new()
571            .host("db.example.com")
572            .port(9000)
573            .username("admin")
574            .password("secret")
575            .schema("MY_SCHEMA")
576            .connection_timeout(Duration::from_secs(20))
577            .query_timeout(Duration::from_secs(60))
578            .use_tls(true)
579            .client_name("test-client")
580            .attribute("custom", "value")
581            .build()
582            .unwrap();
583
584        assert_eq!(params.host, "db.example.com");
585        assert_eq!(params.port, 9000);
586        assert_eq!(params.username, "admin");
587        assert_eq!(params.password(), "secret");
588        assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
589        assert_eq!(params.connection_timeout, Duration::from_secs(20));
590        assert_eq!(params.query_timeout, Duration::from_secs(60));
591        assert!(params.use_tls);
592        assert_eq!(params.client_name, "test-client");
593        assert_eq!(params.attributes.get("custom"), Some(&"value".to_string()));
594    }
595
596    #[test]
597    fn test_builder_validation_missing_host() {
598        let result = ConnectionBuilder::new().username("test").build();
599
600        assert!(result.is_err());
601        assert!(matches!(
602            result.unwrap_err(),
603            ConnectionError::InvalidParameter { parameter, .. } if parameter == "host"
604        ));
605    }
606
607    #[test]
608    fn test_builder_validation_empty_host() {
609        let result = ConnectionBuilder::new().host("").username("test").build();
610
611        assert!(result.is_err());
612    }
613
614    #[test]
615    fn test_builder_validation_timeout() {
616        let result = ConnectionBuilder::new()
617            .host("localhost")
618            .username("test")
619            .connection_timeout(Duration::from_secs(400))
620            .build();
621
622        assert!(result.is_err());
623    }
624
625    #[test]
626    fn test_parse_basic() {
627        let params = ConnectionParams::from_str("exasol://user@localhost").unwrap();
628
629        assert_eq!(params.host, "localhost");
630        assert_eq!(params.port, 8563);
631        assert_eq!(params.username, "user");
632    }
633
634    #[test]
635    fn test_parse_with_port() {
636        let params = ConnectionParams::from_str("exasol://user@localhost:9000").unwrap();
637
638        assert_eq!(params.host, "localhost");
639        assert_eq!(params.port, 9000);
640    }
641
642    #[test]
643    fn test_parse_with_password() {
644        let params = ConnectionParams::from_str("exasol://user:pass@localhost").unwrap();
645
646        assert_eq!(params.username, "user");
647        assert_eq!(params.password(), "pass");
648    }
649
650    #[test]
651    fn test_parse_with_schema() {
652        let params = ConnectionParams::from_str("exasol://user@localhost/MY_SCHEMA").unwrap();
653
654        assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
655    }
656
657    #[test]
658    fn test_parse_with_query_params() {
659        let params = ConnectionParams::from_str(
660            "exasol://user@localhost?timeout=20&tls=true&client_name=test",
661        )
662        .unwrap();
663
664        assert_eq!(params.connection_timeout, Duration::from_secs(20));
665        assert!(params.use_tls);
666        assert_eq!(params.client_name, "test");
667    }
668
669    #[test]
670    fn test_parse_full_url() {
671        let params = ConnectionParams::from_str(
672            "exasol://admin:secret@db.example.com:9000/PROD?timeout=30&tls=true",
673        )
674        .unwrap();
675
676        assert_eq!(params.host, "db.example.com");
677        assert_eq!(params.port, 9000);
678        assert_eq!(params.username, "admin");
679        assert_eq!(params.password(), "secret");
680        assert_eq!(params.schema, Some("PROD".to_string()));
681        assert_eq!(params.connection_timeout, Duration::from_secs(30));
682        assert!(params.use_tls);
683    }
684
685    #[test]
686    fn test_parse_url_encoded() {
687        let params = ConnectionParams::from_str("exasol://user%40test:p%40ss@localhost").unwrap();
688
689        assert_eq!(params.username, "user@test");
690        assert_eq!(params.password(), "p@ss");
691    }
692
693    #[test]
694    fn test_parse_ipv6() {
695        let params = ConnectionParams::from_str("exasol://user@[::1]:8563").unwrap();
696
697        assert_eq!(params.host, "::1");
698        assert_eq!(params.port, 8563);
699    }
700
701    #[test]
702    fn test_parse_invalid_scheme() {
703        let result = ConnectionParams::from_str("postgres://user@localhost");
704        assert!(result.is_err());
705    }
706
707    #[test]
708    fn test_parse_missing_username() {
709        let result = ConnectionParams::from_str("exasol://localhost");
710        assert!(result.is_err());
711    }
712
713    #[test]
714    fn test_display_no_password_leak() {
715        let params = ConnectionBuilder::new()
716            .host("localhost")
717            .username("admin")
718            .password("super_secret")
719            .build()
720            .unwrap();
721
722        let display = format!("{}", params);
723        assert!(!display.contains("super_secret"));
724        assert!(display.contains("localhost"));
725        assert!(display.contains("admin"));
726    }
727
728    #[test]
729    fn test_debug_no_password_leak() {
730        let params = ConnectionBuilder::new()
731            .host("localhost")
732            .username("admin")
733            .password("super_secret")
734            .build()
735            .unwrap();
736
737        let debug = format!("{:?}", params);
738        // Debug output should not contain the password
739        assert!(!debug.contains("super_secret"));
740    }
741
742    // ============================================================
743    // Builder validation tests
744    // ============================================================
745
746    #[test]
747    fn test_builder_validation_missing_username() {
748        let result = ConnectionBuilder::new().host("localhost").build();
749
750        assert!(result.is_err());
751        assert!(matches!(
752            result.unwrap_err(),
753            ConnectionError::InvalidParameter { parameter, .. } if parameter == "username"
754        ));
755    }
756
757    #[test]
758    fn test_builder_validation_empty_username() {
759        let result = ConnectionBuilder::new()
760            .host("localhost")
761            .username("")
762            .build();
763
764        assert!(result.is_err());
765        assert!(matches!(
766            result.unwrap_err(),
767            ConnectionError::InvalidParameter { parameter, message }
768                if parameter == "username" && message.contains("empty")
769        ));
770    }
771
772    #[test]
773    fn test_builder_validation_port_zero() {
774        let result = ConnectionBuilder::new()
775            .host("localhost")
776            .username("test")
777            .port(0)
778            .build();
779
780        assert!(result.is_err());
781        assert!(matches!(
782            result.unwrap_err(),
783            ConnectionError::InvalidParameter { parameter, message }
784                if parameter == "port" && message.contains("greater than 0")
785        ));
786    }
787
788    #[test]
789    fn test_builder_default() {
790        let builder = ConnectionBuilder::default();
791        let result = builder.host("localhost").username("user").build().unwrap();
792        assert_eq!(result.host, "localhost");
793    }
794
795    #[test]
796    fn test_connection_params_builder_method() {
797        let builder = ConnectionParams::builder();
798        let params = builder.host("localhost").username("user").build().unwrap();
799        assert_eq!(params.host, "localhost");
800    }
801
802    #[test]
803    fn test_builder_idle_timeout() {
804        let params = ConnectionBuilder::new()
805            .host("localhost")
806            .username("test")
807            .idle_timeout(Duration::from_secs(120))
808            .build()
809            .unwrap();
810
811        assert_eq!(params.idle_timeout, Duration::from_secs(120));
812    }
813
814    #[test]
815    fn test_builder_validate_server_certificate() {
816        let params = ConnectionBuilder::new()
817            .host("localhost")
818            .username("test")
819            .validate_server_certificate(false)
820            .build()
821            .unwrap();
822
823        assert!(!params.validate_server_certificate);
824    }
825
826    #[test]
827    fn test_builder_client_version() {
828        let params = ConnectionBuilder::new()
829            .host("localhost")
830            .username("test")
831            .client_version("1.2.3")
832            .build()
833            .unwrap();
834
835        assert_eq!(params.client_version, "1.2.3");
836    }
837
838    #[test]
839    fn test_builder_default_values() {
840        let params = ConnectionBuilder::new()
841            .host("localhost")
842            .username("test")
843            .build()
844            .unwrap();
845
846        assert_eq!(params.connection_timeout, Duration::from_secs(30));
847        assert_eq!(params.query_timeout, Duration::from_secs(300));
848        assert_eq!(params.idle_timeout, Duration::from_secs(600));
849        assert!(!params.use_tls);
850        assert!(params.validate_server_certificate);
851        assert_eq!(params.client_name, "exarrow-rs");
852    }
853
854    // ============================================================
855    // Query parameter parsing tests
856    // ============================================================
857
858    #[test]
859    fn test_parse_query_param_without_equals() {
860        let result = ConnectionParams::from_str("exasol://user@localhost?invalid_param");
861
862        assert!(result.is_err());
863        assert!(matches!(
864            result.unwrap_err(),
865            ConnectionError::ParseError(msg) if msg.contains("Invalid query parameter format")
866        ));
867    }
868
869    #[test]
870    fn test_parse_query_param_empty_pairs() {
871        // Empty pairs between && should be skipped
872        let params =
873            ConnectionParams::from_str("exasol://user@localhost?timeout=10&&tls=true").unwrap();
874
875        assert_eq!(params.connection_timeout, Duration::from_secs(10));
876        assert!(params.use_tls);
877    }
878
879    #[test]
880    fn test_parse_query_timeout() {
881        let params =
882            ConnectionParams::from_str("exasol://user@localhost?query_timeout=60").unwrap();
883
884        assert_eq!(params.query_timeout, Duration::from_secs(60));
885    }
886
887    #[test]
888    fn test_parse_idle_timeout() {
889        let params =
890            ConnectionParams::from_str("exasol://user@localhost?idle_timeout=120").unwrap();
891
892        assert_eq!(params.idle_timeout, Duration::from_secs(120));
893    }
894
895    #[test]
896    fn test_parse_connection_timeout_param() {
897        let params =
898            ConnectionParams::from_str("exasol://user@localhost?connection_timeout=15").unwrap();
899
900        assert_eq!(params.connection_timeout, Duration::from_secs(15));
901    }
902
903    #[test]
904    fn test_parse_invalid_timeout_value() {
905        let result = ConnectionParams::from_str("exasol://user@localhost?timeout=not_a_number");
906
907        assert!(result.is_err());
908        assert!(matches!(
909            result.unwrap_err(),
910            ConnectionError::InvalidParameter { parameter, message }
911                if parameter == "timeout" && message.contains("Invalid timeout value")
912        ));
913    }
914
915    #[test]
916    fn test_parse_invalid_query_timeout_value() {
917        let result =
918            ConnectionParams::from_str("exasol://user@localhost?query_timeout=not_a_number");
919
920        assert!(result.is_err());
921        assert!(matches!(
922            result.unwrap_err(),
923            ConnectionError::InvalidParameter { parameter, .. } if parameter == "query_timeout"
924        ));
925    }
926
927    #[test]
928    fn test_parse_invalid_idle_timeout_value() {
929        let result =
930            ConnectionParams::from_str("exasol://user@localhost?idle_timeout=not_a_number");
931
932        assert!(result.is_err());
933        assert!(matches!(
934            result.unwrap_err(),
935            ConnectionError::InvalidParameter { parameter, .. } if parameter == "idle_timeout"
936        ));
937    }
938
939    // ============================================================
940    // TLS/SSL parameter tests
941    // ============================================================
942
943    #[test]
944    fn test_parse_ssl_param() {
945        let params = ConnectionParams::from_str("exasol://user@localhost?ssl=true").unwrap();
946
947        assert!(params.use_tls);
948    }
949
950    #[test]
951    fn test_parse_use_tls_param() {
952        let params = ConnectionParams::from_str("exasol://user@localhost?use_tls=1").unwrap();
953
954        assert!(params.use_tls);
955    }
956
957    #[test]
958    fn test_parse_validate_certificate_param() {
959        let params =
960            ConnectionParams::from_str("exasol://user@localhost?validate_certificate=false")
961                .unwrap();
962
963        assert!(!params.validate_server_certificate);
964    }
965
966    #[test]
967    fn test_parse_verify_certificate_param() {
968        let params =
969            ConnectionParams::from_str("exasol://user@localhost?verify_certificate=0").unwrap();
970
971        assert!(!params.validate_server_certificate);
972    }
973
974    #[test]
975    fn test_parse_validateservercertificate_param() {
976        let params =
977            ConnectionParams::from_str("exasol://user@localhost?validateservercertificate=no")
978                .unwrap();
979
980        assert!(!params.validate_server_certificate);
981    }
982
983    // ============================================================
984    // Boolean parsing tests
985    // ============================================================
986
987    #[test]
988    fn test_parse_bool_yes() {
989        let params = ConnectionParams::from_str("exasol://user@localhost?tls=yes").unwrap();
990        assert!(params.use_tls);
991    }
992
993    #[test]
994    fn test_parse_bool_no() {
995        let params = ConnectionParams::from_str("exasol://user@localhost?tls=no").unwrap();
996        assert!(!params.use_tls);
997    }
998
999    #[test]
1000    fn test_parse_bool_on() {
1001        let params = ConnectionParams::from_str("exasol://user@localhost?tls=on").unwrap();
1002        assert!(params.use_tls);
1003    }
1004
1005    #[test]
1006    fn test_parse_bool_off() {
1007        let params = ConnectionParams::from_str("exasol://user@localhost?tls=off").unwrap();
1008        assert!(!params.use_tls);
1009    }
1010
1011    #[test]
1012    fn test_parse_bool_one() {
1013        let params = ConnectionParams::from_str("exasol://user@localhost?tls=1").unwrap();
1014        assert!(params.use_tls);
1015    }
1016
1017    #[test]
1018    fn test_parse_bool_zero() {
1019        let params = ConnectionParams::from_str("exasol://user@localhost?tls=0").unwrap();
1020        assert!(!params.use_tls);
1021    }
1022
1023    #[test]
1024    fn test_parse_bool_case_insensitive() {
1025        let params = ConnectionParams::from_str("exasol://user@localhost?tls=TRUE").unwrap();
1026        assert!(params.use_tls);
1027
1028        let params = ConnectionParams::from_str("exasol://user@localhost?tls=FALSE").unwrap();
1029        assert!(!params.use_tls);
1030    }
1031
1032    #[test]
1033    fn test_parse_bool_invalid() {
1034        let result = ConnectionParams::from_str("exasol://user@localhost?tls=maybe");
1035
1036        assert!(result.is_err());
1037        assert!(matches!(
1038            result.unwrap_err(),
1039            ConnectionError::InvalidParameter { parameter, message }
1040                if parameter == "boolean" && message.contains("Invalid boolean value")
1041        ));
1042    }
1043
1044    // ============================================================
1045    // Client info parameter tests
1046    // ============================================================
1047
1048    #[test]
1049    fn test_parse_client_version_param() {
1050        let params =
1051            ConnectionParams::from_str("exasol://user@localhost?client_version=2.0.0").unwrap();
1052
1053        assert_eq!(params.client_version, "2.0.0");
1054    }
1055
1056    #[test]
1057    fn test_parse_custom_attribute_param() {
1058        let params =
1059            ConnectionParams::from_str("exasol://user@localhost?custom_key=custom_value").unwrap();
1060
1061        assert_eq!(
1062            params.attributes.get("custom_key"),
1063            Some(&"custom_value".to_string())
1064        );
1065    }
1066
1067    // ============================================================
1068    // Authentication from query params tests
1069    // ============================================================
1070
1071    #[test]
1072    fn test_parse_username_from_query_user() {
1073        let params = ConnectionParams::from_str("exasol://localhost?user=testuser").unwrap();
1074
1075        assert_eq!(params.username, "testuser");
1076    }
1077
1078    #[test]
1079    fn test_parse_username_from_query_username() {
1080        let params = ConnectionParams::from_str("exasol://localhost?username=testuser").unwrap();
1081
1082        assert_eq!(params.username, "testuser");
1083    }
1084
1085    #[test]
1086    fn test_parse_password_from_query_password() {
1087        let params =
1088            ConnectionParams::from_str("exasol://localhost?user=testuser&password=secret").unwrap();
1089
1090        assert_eq!(params.password(), "secret");
1091    }
1092
1093    #[test]
1094    fn test_parse_password_from_query_pass() {
1095        let params =
1096            ConnectionParams::from_str("exasol://localhost?user=testuser&pass=secret").unwrap();
1097
1098        assert_eq!(params.password(), "secret");
1099    }
1100
1101    #[test]
1102    fn test_parse_auth_from_query_no_password() {
1103        let params = ConnectionParams::from_str("exasol://localhost?user=testuser").unwrap();
1104
1105        assert_eq!(params.username, "testuser");
1106        assert_eq!(params.password(), "");
1107    }
1108
1109    // ============================================================
1110    // IPv6 tests
1111    // ============================================================
1112
1113    #[test]
1114    fn test_parse_ipv6_without_port() {
1115        let params = ConnectionParams::from_str("exasol://user@[::1]").unwrap();
1116
1117        assert_eq!(params.host, "::1");
1118        assert_eq!(params.port, 8563);
1119    }
1120
1121    #[test]
1122    fn test_parse_ipv6_full_address() {
1123        let params = ConnectionParams::from_str("exasol://user@[2001:db8::1]:9000/schema").unwrap();
1124
1125        assert_eq!(params.host, "2001:db8::1");
1126        assert_eq!(params.port, 9000);
1127        assert_eq!(params.schema, Some("schema".to_string()));
1128    }
1129
1130    // ============================================================
1131    // Schema edge cases
1132    // ============================================================
1133
1134    #[test]
1135    fn test_parse_empty_schema_path() {
1136        let params = ConnectionParams::from_str("exasol://user@localhost/").unwrap();
1137
1138        assert_eq!(params.schema, None);
1139    }
1140
1141    #[test]
1142    fn test_parse_schema_with_query_params() {
1143        let params =
1144            ConnectionParams::from_str("exasol://user@localhost/MY_SCHEMA?tls=true").unwrap();
1145
1146        assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
1147        assert!(params.use_tls);
1148    }
1149
1150    // ============================================================
1151    // Port parsing edge cases
1152    // ============================================================
1153
1154    #[test]
1155    fn test_parse_invalid_port() {
1156        let result = ConnectionParams::from_str("exasol://user@localhost:not_a_port");
1157
1158        assert!(result.is_err());
1159        assert!(matches!(
1160            result.unwrap_err(),
1161            ConnectionError::ParseError(msg) if msg.contains("Invalid port")
1162        ));
1163    }
1164
1165    #[test]
1166    fn test_parse_ipv6_invalid_port() {
1167        let result = ConnectionParams::from_str("exasol://user@[::1]:invalid");
1168
1169        assert!(result.is_err());
1170        assert!(matches!(
1171            result.unwrap_err(),
1172            ConnectionError::ParseError(msg) if msg.contains("Invalid port")
1173        ));
1174    }
1175
1176    // ============================================================
1177    // URL encoding edge cases
1178    // ============================================================
1179
1180    #[test]
1181    fn test_parse_url_encoded_query_params() {
1182        let params =
1183            ConnectionParams::from_str("exasol://user@localhost?client_name=my%20client").unwrap();
1184
1185        assert_eq!(params.client_name, "my client");
1186    }
1187
1188    #[test]
1189    fn test_parse_auth_without_password() {
1190        let params = ConnectionParams::from_str("exasol://testuser@localhost").unwrap();
1191
1192        assert_eq!(params.username, "testuser");
1193        assert_eq!(params.password(), "");
1194    }
1195
1196    // ============================================================
1197    // Whitespace handling tests
1198    // ============================================================
1199
1200    #[test]
1201    fn test_parse_url_with_whitespace_trim() {
1202        let params = ConnectionParams::from_str("  exasol://user@localhost  ").unwrap();
1203
1204        assert_eq!(params.host, "localhost");
1205        assert_eq!(params.username, "user");
1206    }
1207}