Skip to main content

geode_client/
client.rs

1//! Geode client implementation supporting both QUIC and gRPC transports.
2//! Uses protobuf wire protocol with 4-byte big-endian length prefix for QUIC.
3
4use log::{debug, trace, warn};
5use quinn::{ClientConfig, Endpoint};
6use rustls::pki_types::{CertificateDer, ServerName as RustlsServerName};
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::net::{SocketAddr, ToSocketAddrs};
11use std::sync::Arc;
12use tokio::time::{Duration, Instant, timeout};
13
14use crate::dsn::{Dsn, Transport};
15use crate::error::{Error, Result};
16use crate::proto;
17use crate::types::Value;
18use crate::validate;
19
20const GEODE_ALPN: &[u8] = b"geode/1";
21/// Maximum size of a single protobuf response frame in bytes (8 MB).
22/// Prevents unbounded memory allocation from a malicious or buggy server (CWE-400).
23const MAX_PROTO_FRAME_BYTES: usize = 8 * 1024 * 1024;
24/// Maximum total rows to buffer across all pages for a single query.
25const DEFAULT_MAX_ROWS: usize = 1_000_000;
26/// Maximum number of pages to pull for a single query.
27const DEFAULT_MAX_PAGES: usize = 10_000;
28
29/// Redact the password from a DSN string for safe inclusion in logs and
30/// error messages.
31///
32/// Handles both the URL credential format (`quic://user:pass@host`) and the
33/// query-parameter format (`?password=xxx` / `?pass=xxx`), replacing the
34/// secret with `[REDACTED]`. DSNs without credentials are returned
35/// unchanged. Mirrors the Go reference client's `RedactDSN`.
36///
37/// # Example
38///
39/// ```
40/// use geode_client::redact_dsn;
41///
42/// let safe = redact_dsn("quic://user:secret@host:3141");
43/// assert!(safe.contains("[REDACTED]"));
44/// assert!(!safe.contains("secret"));
45/// ```
46pub fn redact_dsn(dsn: &str) -> String {
47    let mut result = dsn.to_string();
48
49    // Handle URL format: scheme://user:password@host:port
50    // Look for pattern user:password@ and redact the password
51    if let Some(scheme_end) = result.find("://") {
52        let after_scheme = scheme_end + 3;
53        if let Some(at_pos) = result[after_scheme..].find('@') {
54            let auth_section = &result[after_scheme..after_scheme + at_pos];
55            if let Some(colon_pos) = auth_section.find(':') {
56                // Found user:password pattern
57                let user = &auth_section[..colon_pos];
58                let rest_start = after_scheme + at_pos;
59                result = format!(
60                    "{}{}:{}{}",
61                    &result[..after_scheme],
62                    user,
63                    "[REDACTED]",
64                    &result[rest_start..]
65                );
66            }
67        }
68    }
69
70    // Handle query parameter format: host:port?password=xxx
71    // Redact password= and pass= parameters (only check once per pattern to avoid loops)
72    let patterns = ["password=", "pass="];
73    for pattern in patterns {
74        let lower = result.to_lowercase();
75        if let Some(start) = lower.find(pattern) {
76            let value_start = start + pattern.len();
77            // Find end of value (& or end of string)
78            let value_end = result[value_start..]
79                .find('&')
80                .map(|i| value_start + i)
81                .unwrap_or(result.len());
82
83            result = format!(
84                "{}[REDACTED]{}",
85                &result[..value_start],
86                &result[value_end..]
87            );
88        }
89    }
90
91    result
92}
93
94/// A column definition in a query result set.
95///
96/// Contains the column name and its GQL type as returned by the server.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Column {
99    /// The column name (or alias if specified in the query)
100    pub name: String,
101    /// The GQL type of the column (e.g., "INT", "STRING", "BOOL")
102    #[serde(rename = "type")]
103    pub col_type: String,
104}
105
106/// A page of query results.
107///
108/// Query results are returned in pages. Each page contains a slice of rows
109/// along with metadata about the result set.
110///
111/// # Example
112///
113/// ```ignore
114/// let (page, _) = conn.query("MATCH (n:Person) RETURN n.name, n.age").await?;
115///
116/// for row in &page.rows {
117///     let name = row.get("name").unwrap().as_string()?;
118///     let age = row.get("age").unwrap().as_int()?;
119///     println!("{}: {}", name, age);
120/// }
121///
122/// if !page.final_page {
123///     // More results available, would need to pull next page
124/// }
125/// ```
126#[derive(Debug, Clone)]
127pub struct Page {
128    /// Column definitions for the result set
129    pub columns: Vec<Column>,
130    /// Result rows, each row is a map of column name to value
131    pub rows: Vec<HashMap<String, Value>>,
132    /// Whether results are ordered (ORDER BY was used)
133    pub ordered: bool,
134    /// The keys used for ordering, if any
135    pub order_keys: Vec<String>,
136    /// Whether this is the final page of results
137    pub final_page: bool,
138}
139
140/// A named savepoint within a transaction.
141///
142/// Savepoints allow partial rollback within a transaction. They can be created
143/// and managed via server-side GQL commands.
144///
145/// # Example
146///
147/// ```ignore
148/// conn.begin().await?;
149/// conn.query("CREATE (n:Node {id: 1})").await?;
150///
151/// let sp = conn.savepoint("before_risky_op")?;
152/// match conn.query("CREATE (n:Node {id: 2})").await {
153///     Ok(_) => {},
154///     Err(_) => conn.rollback_to(&sp).await?,  // Undo only the second create
155/// }
156///
157/// conn.commit().await?;  // First node is saved
158/// ```
159#[derive(Debug, Clone)]
160pub struct Savepoint {
161    /// The savepoint name
162    pub name: String,
163}
164
165/// A prepared statement for efficient repeated query execution.
166///
167/// Prepared statements allow you to define a query once and execute it
168/// multiple times with different parameters. This can improve performance
169/// by allowing query plan caching on the server.
170///
171/// # Example
172///
173/// ```ignore
174/// let stmt = conn.prepare("MATCH (p:Person {id: $id}) RETURN p").await?;
175///
176/// for id in 1..=100 {
177///     let mut params = HashMap::new();
178///     params.insert("id".to_string(), Value::int(id));
179///     let (page, _) = stmt.execute(&mut conn, &params).await?;
180///     // Process results...
181/// }
182/// ```
183#[derive(Debug, Clone)]
184pub struct PreparedStatement {
185    /// The GQL query string
186    query: String,
187    /// Parameter names extracted from the query
188    param_names: Vec<String>,
189}
190
191impl PreparedStatement {
192    /// Create a new prepared statement.
193    ///
194    /// Extracts parameter names from the query (tokens starting with `$`).
195    pub fn new(query: impl Into<String>) -> Self {
196        let query = query.into();
197        let param_names = Self::extract_param_names(&query);
198        Self { query, param_names }
199    }
200
201    /// Extract parameter names from a query string.
202    fn extract_param_names(query: &str) -> Vec<String> {
203        let mut names = Vec::new();
204        let mut chars = query.chars().peekable();
205
206        while let Some(c) = chars.next() {
207            if c == '$' {
208                let mut name = String::new();
209                while let Some(&next) = chars.peek() {
210                    if next.is_ascii_alphanumeric() || next == '_' {
211                        name.push(chars.next().unwrap());
212                    } else {
213                        break;
214                    }
215                }
216                if !name.is_empty() && !names.contains(&name) {
217                    names.push(name);
218                }
219            }
220        }
221
222        names
223    }
224
225    /// Get the query string.
226    pub fn query(&self) -> &str {
227        &self.query
228    }
229
230    /// Get the parameter names expected by this statement.
231    pub fn param_names(&self) -> &[String] {
232        &self.param_names
233    }
234
235    /// Execute the prepared statement with the given parameters.
236    ///
237    /// # Arguments
238    ///
239    /// * `conn` - The connection to execute on
240    /// * `params` - Parameter values (must include all parameters in the query)
241    ///
242    /// # Returns
243    ///
244    /// A tuple of (`Page`, `Option<String>`) with results and optional warnings.
245    ///
246    /// # Errors
247    ///
248    /// Returns an error if required parameters are missing or if the query fails.
249    pub async fn execute(
250        &self,
251        conn: &mut Connection,
252        params: &HashMap<String, crate::types::Value>,
253    ) -> crate::error::Result<(Page, Option<String>)> {
254        // Validate all required parameters are provided
255        for name in &self.param_names {
256            if !params.contains_key(name) {
257                return Err(crate::error::Error::validation(format!(
258                    "Missing required parameter: {}",
259                    name
260                )));
261            }
262        }
263
264        conn.query_with_params(&self.query, params).await
265    }
266}
267
268/// An operation in a query execution plan.
269#[derive(Debug, Clone)]
270pub struct PlanOperation {
271    /// Operation type (e.g., "NodeScan", "Filter", "Projection")
272    pub op_type: String,
273    /// Human-readable description
274    pub description: String,
275    /// Estimated row count for this operation
276    pub estimated_rows: Option<u64>,
277    /// Child operations
278    pub children: Vec<PlanOperation>,
279}
280
281/// A query execution plan.
282///
283/// Shows how the database will execute a query without actually running it.
284/// Useful for query optimization and understanding performance characteristics.
285#[derive(Debug, Clone)]
286pub struct QueryPlan {
287    /// Root operations in the plan
288    pub operations: Vec<PlanOperation>,
289    /// Total estimated rows
290    pub estimated_rows: u64,
291    /// Raw plan from server (for advanced analysis)
292    pub raw: serde_json::Value,
293}
294
295/// Query execution profile with timing information.
296///
297/// Includes the execution plan plus actual runtime statistics.
298#[derive(Debug, Clone)]
299pub struct QueryProfile {
300    /// The execution plan
301    pub plan: QueryPlan,
302    /// Actual rows returned
303    pub actual_rows: u64,
304    /// Total execution time in milliseconds
305    pub execution_time_ms: f64,
306    /// Raw profile from server
307    pub raw: serde_json::Value,
308}
309
310/// A Geode database client supporting both QUIC and gRPC transports.
311///
312/// Use the builder pattern to configure the client, then call [`connect`](Client::connect)
313/// to establish a connection.
314///
315/// # Transport Selection
316///
317/// The transport is selected based on the DSN scheme:
318/// - `quic://` - QUIC transport (default)
319/// - `grpc://` - gRPC transport
320///
321/// # Example
322///
323/// ```no_run
324/// use geode_client::Client;
325///
326/// # async fn example() -> geode_client::Result<()> {
327/// // QUIC transport (legacy API)
328/// let client = Client::new("127.0.0.1", 3141)
329///     .skip_verify(true)  // Development only!
330///     .page_size(500)
331///     .client_name("my-app");
332///
333/// // Or use DSN with explicit transport
334/// let client = Client::from_dsn("quic://127.0.0.1:3141?insecure=true")?;
335/// let client = Client::from_dsn("grpc://127.0.0.1:50051")?;
336///
337/// let mut conn = client.connect().await?;
338/// let (page, _) = conn.query("RETURN 1 AS x").await?;
339/// conn.close()?;
340/// # Ok(())
341/// # }
342/// ```
343/// Client configuration for connecting to a Geode server.
344///
345/// The password field uses `SecretString` from the `secrecy` crate to ensure
346/// credentials are zeroized from memory on drop and not accidentally leaked
347/// in debug output or error messages.
348#[derive(Clone)]
349pub struct Client {
350    transport: Transport,
351    host: String,
352    port: u16,
353    tls_enabled: bool,
354    skip_verify: bool,
355    page_size: usize,
356    hello_name: String,
357    hello_ver: String,
358    conformance: String,
359    username: Option<String>,
360    /// Password stored using SecretString for secure memory handling (CWE-316).
361    /// Automatically zeroized on drop and redacted in Debug output.
362    password: Option<SecretString>,
363    /// Graph name to bind on connection (sent in HELLO)
364    graph: Option<String>,
365    /// Tenant ID for multi-tenant isolation (sent in HELLO when set)
366    tenant: Option<String>,
367    /// FLE role for field-level access control (sent in HELLO when set)
368    role: Option<String>,
369    /// Connection timeout in seconds (default: 10)
370    connect_timeout_secs: u64,
371    /// HELLO handshake timeout in seconds (default: 5)
372    hello_timeout_secs: u64,
373    /// Idle connection timeout in seconds (default: 30)
374    idle_timeout_secs: u64,
375}
376
377impl Client {
378    /// Create a new QUIC client for the specified host and port.
379    ///
380    /// This method creates a client using QUIC transport. For gRPC transport,
381    /// use [`from_dsn`](Client::from_dsn) with a `grpc://` scheme.
382    ///
383    /// # Arguments
384    ///
385    /// * `host` - The server hostname or IP address
386    /// * `port` - The server port (typically 3141 for Geode)
387    ///
388    /// # Example
389    ///
390    /// ```
391    /// use geode_client::Client;
392    ///
393    /// let client = Client::new("localhost", 3141);
394    /// let client = Client::new("192.168.1.100", 8443);
395    /// let client = Client::new(String::from("geode.example.com"), 3141);
396    /// ```
397    pub fn new(host: impl Into<String>, port: u16) -> Self {
398        Self {
399            transport: Transport::Quic,
400            host: host.into(),
401            port,
402            tls_enabled: true,
403            skip_verify: false,
404            page_size: 1000,
405            hello_name: "geode-rust".to_string(),
406            hello_ver: env!("CARGO_PKG_VERSION").to_string(),
407            conformance: "min".to_string(),
408            username: None,
409            password: None,
410            graph: None,
411            tenant: None,
412            role: None,
413            connect_timeout_secs: 10,
414            hello_timeout_secs: 15,
415            idle_timeout_secs: 30,
416        }
417    }
418
419    /// Create a new client from a DSN (Data Source Name) string.
420    ///
421    /// # Supported DSN Formats
422    ///
423    /// - `quic://host:port?options` - QUIC transport (recommended)
424    /// - `grpc://host:port?options` - gRPC transport
425    /// - `host:port?options` - Legacy format (defaults to QUIC)
426    ///
427    /// # Supported Options
428    ///
429    /// - `tls` - Enable/disable TLS (0/1/true/false)
430    /// - `insecure` or `skip_verify` - Skip TLS verification
431    /// - `page_size` - Results page size (default: 1000)
432    /// - `client_name` or `hello_name` - Client name
433    /// - `client_version` or `hello_ver` - Client version
434    /// - `conformance` - GQL conformance level
435    /// - `username` or `user` - Authentication username
436    /// - `password` or `pass` - Authentication password
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use geode_client::Client;
442    ///
443    /// // QUIC transport (explicit)
444    /// let client = Client::from_dsn("quic://localhost:3141").unwrap();
445    ///
446    /// // gRPC transport
447    /// let client = Client::from_dsn("grpc://localhost:50051?tls=0").unwrap();
448    ///
449    /// // Legacy format (defaults to QUIC)
450    /// let client = Client::from_dsn("localhost:3141?insecure=true").unwrap();
451    ///
452    /// // With authentication
453    /// let client = Client::from_dsn("quic://admin:secret@localhost:3141").unwrap();
454    ///
455    /// // IPv6 support
456    /// let client = Client::from_dsn("grpc://[::1]:50051").unwrap();
457    /// ```
458    ///
459    /// # Errors
460    ///
461    /// Returns `Error::InvalidDsn` if:
462    /// - DSN is empty
463    /// - Scheme is unsupported (not quic://, grpc://, or schemeless)
464    /// - Host is missing
465    /// - Port is invalid
466    pub fn from_dsn(dsn_str: &str) -> Result<Self> {
467        let dsn = Dsn::parse(dsn_str)?;
468
469        Ok(Self {
470            transport: dsn.transport(),
471            host: dsn.host().to_string(),
472            port: dsn.port(),
473            tls_enabled: dsn.tls_enabled(),
474            skip_verify: dsn.skip_verify(),
475            page_size: dsn.page_size(),
476            hello_name: dsn.client_name().to_string(),
477            hello_ver: dsn.client_version().to_string(),
478            conformance: dsn.conformance().to_string(),
479            username: dsn.username().map(String::from),
480            password: dsn.password().map(|p| SecretString::from(p.to_string())),
481            graph: dsn.graph().map(String::from),
482            tenant: dsn.tenant().map(String::from),
483            role: dsn.role().map(String::from),
484            connect_timeout_secs: dsn.connect_timeout_secs().unwrap_or(10),
485            hello_timeout_secs: 15,
486            idle_timeout_secs: 30,
487        })
488    }
489
490    /// Get the transport type for this client.
491    pub fn transport(&self) -> Transport {
492        self.transport
493    }
494
495    /// Skip TLS certificate verification.
496    ///
497    /// # Security Warning
498    ///
499    /// **This should only be used in development environments.** Disabling
500    /// certificate verification makes the connection vulnerable to
501    /// man-in-the-middle attacks.
502    ///
503    /// # Arguments
504    ///
505    /// * `skip` - If true, skip certificate verification
506    pub fn skip_verify(mut self, skip: bool) -> Self {
507        self.skip_verify = skip;
508        self
509    }
510
511    /// Set the page size for query results.
512    ///
513    /// Controls how many rows are returned per page when fetching results.
514    /// Larger values reduce round-trips but use more memory.
515    ///
516    /// # Arguments
517    ///
518    /// * `size` - Number of rows per page (default: 1000)
519    pub fn page_size(mut self, size: usize) -> Self {
520        self.page_size = size;
521        self
522    }
523
524    /// Set the client name sent to the server.
525    ///
526    /// This appears in server logs and can help with debugging.
527    ///
528    /// # Arguments
529    ///
530    /// * `name` - Client application name (default: "geode-rust-quinn")
531    pub fn client_name(mut self, name: impl Into<String>) -> Self {
532        self.hello_name = name.into();
533        self
534    }
535
536    /// Set the client version sent to the server.
537    ///
538    /// # Arguments
539    ///
540    /// * `version` - Client version string (default: "0.1.0")
541    pub fn client_version(mut self, version: impl Into<String>) -> Self {
542        self.hello_ver = version.into();
543        self
544    }
545
546    /// Set the GQL conformance level.
547    ///
548    /// # Arguments
549    ///
550    /// * `level` - Conformance level (default: "min")
551    pub fn conformance(mut self, level: impl Into<String>) -> Self {
552        self.conformance = level.into();
553        self
554    }
555
556    /// Set the graph name to bind on connection.
557    ///
558    /// When set, the graph name is sent in the HELLO message so the server
559    /// binds the session to the specified graph automatically.
560    ///
561    /// # Arguments
562    ///
563    /// * `graph` - Graph name to bind (e.g. "mygraph")
564    pub fn graph(mut self, graph: impl Into<String>) -> Self {
565        self.graph = Some(graph.into());
566        self
567    }
568
569    /// Set the tenant ID for multi-tenant isolation.
570    ///
571    /// When set, the tenant ID is sent in the HELLO message so the server
572    /// scopes the session's data access to the specified tenant. Required for
573    /// multi-tenant deployments (GAP-0841).
574    ///
575    /// # Arguments
576    ///
577    /// * `tenant` - Tenant identifier (e.g. "acme-corp")
578    pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
579        self.tenant = Some(tenant.into());
580        self
581    }
582
583    /// Set the FLE role for field-level access control.
584    ///
585    /// When set, the role is sent in the HELLO message so field-level
586    /// encryption (FLE) policies are evaluated against it. Required for FLE
587    /// deployments (GAP-0841).
588    ///
589    /// # Arguments
590    ///
591    /// * `role` - FLE role name (e.g. "analyst")
592    pub fn role(mut self, role: impl Into<String>) -> Self {
593        self.role = Some(role.into());
594        self
595    }
596
597    /// Set the authentication username.
598    ///
599    /// # Arguments
600    ///
601    /// * `username` - The username for authentication
602    ///
603    /// # Example
604    ///
605    /// ```
606    /// use geode_client::Client;
607    ///
608    /// let client = Client::new("localhost", 3141)
609    ///     .username("admin")
610    ///     .password("secret");
611    /// ```
612    pub fn username(mut self, username: impl Into<String>) -> Self {
613        self.username = Some(username.into());
614        self
615    }
616
617    /// Set the authentication password.
618    ///
619    /// The password is stored using `SecretString` which ensures it is:
620    /// - Zeroized from memory when dropped
621    /// - Not accidentally leaked in debug output
622    ///
623    /// # Arguments
624    ///
625    /// * `password` - The password for authentication
626    pub fn password(mut self, password: impl Into<String>) -> Self {
627        self.password = Some(SecretString::from(password.into()));
628        self
629    }
630
631    /// Set the connection timeout in seconds.
632    ///
633    /// This controls how long to wait for the initial QUIC connection
634    /// to be established. Default is 10 seconds.
635    ///
636    /// # Arguments
637    ///
638    /// * `seconds` - Timeout in seconds (must be > 0)
639    pub fn connect_timeout(mut self, seconds: u64) -> Self {
640        self.connect_timeout_secs = seconds.max(1);
641        self
642    }
643
644    /// Set the HELLO handshake timeout in seconds.
645    ///
646    /// This controls how long to wait for the server to respond to the
647    /// initial HELLO message. Default is 5 seconds.
648    ///
649    /// # Arguments
650    ///
651    /// * `seconds` - Timeout in seconds (must be > 0)
652    pub fn hello_timeout(mut self, seconds: u64) -> Self {
653        self.hello_timeout_secs = seconds.max(1);
654        self
655    }
656
657    /// Set the idle connection timeout in seconds.
658    ///
659    /// This controls how long an idle connection can remain open before
660    /// being automatically closed by the QUIC layer. Default is 30 seconds.
661    ///
662    /// # Arguments
663    ///
664    /// * `seconds` - Timeout in seconds (must be > 0)
665    pub fn idle_timeout(mut self, seconds: u64) -> Self {
666        self.idle_timeout_secs = seconds.max(1);
667        self
668    }
669
670    /// Validate the client configuration.
671    ///
672    /// Performs validation on all configuration parameters including:
673    /// - Hostname format (RFC 1035 compliant)
674    /// - Port number (1-65535)
675    /// - Page size (1-100,000)
676    ///
677    /// This method is automatically called by [`connect`](Self::connect).
678    /// You can call it manually to validate configuration before attempting
679    /// to connect.
680    ///
681    /// # Errors
682    ///
683    /// Returns a validation error if any parameter is invalid.
684    ///
685    /// # Example
686    ///
687    /// ```
688    /// use geode_client::Client;
689    ///
690    /// let client = Client::new("localhost", 3141);
691    /// assert!(client.validate().is_ok());
692    ///
693    /// // Invalid hostname
694    /// let invalid = Client::new("-invalid-host", 3141);
695    /// assert!(invalid.validate().is_err());
696    /// ```
697    pub fn validate(&self) -> Result<()> {
698        // Validate hostname format
699        validate::hostname(&self.host)?;
700
701        // Validate port (0 is reserved)
702        validate::port(self.port)?;
703
704        // Validate page size
705        validate::page_size(self.page_size)?;
706
707        Ok(())
708    }
709
710    /// Connect to the Geode database.
711    ///
712    /// Establishes a QUIC connection to the server, performs the TLS handshake,
713    /// and sends the initial HELLO message.
714    ///
715    /// # Returns
716    ///
717    /// A [`Connection`] that can be used to execute queries.
718    ///
719    /// # Errors
720    ///
721    /// Returns an error if:
722    /// - The hostname cannot be resolved
723    /// - The connection cannot be established
724    /// - TLS verification fails (unless `skip_verify` is true)
725    /// - The HELLO handshake fails
726    ///
727    /// # Example
728    ///
729    /// ```no_run
730    /// # use geode_client::Client;
731    /// # async fn example() -> geode_client::Result<()> {
732    /// let client = Client::new("localhost", 3141).skip_verify(true);
733    /// let mut conn = client.connect().await?;
734    /// // Use connection...
735    /// conn.close()?;
736    /// # Ok(())
737    /// # }
738    /// ```
739    // CANARY: REQ=REQ-CLIENT-RUST-001; FEATURE="RustClientConnection"; ASPECT=HelloHandshake; STATUS=IMPL; OWNER=clients; UPDATED=2025-02-14
740    pub async fn connect(&self) -> Result<Connection> {
741        // Validate configuration before connecting (Gap #9 - automatic validation)
742        self.validate()?;
743
744        // Expose the secret password only when needed for the connection
745        let password_ref = self.password.as_ref().map(|s| s.expose_secret());
746
747        match self.transport {
748            Transport::Quic => {
749                Connection::new_quic(
750                    &self.host,
751                    self.port,
752                    self.skip_verify,
753                    self.page_size,
754                    &self.hello_name,
755                    &self.hello_ver,
756                    &self.conformance,
757                    self.username.as_deref(),
758                    password_ref,
759                    self.graph.as_deref(),
760                    self.tenant.as_deref(),
761                    self.role.as_deref(),
762                    self.connect_timeout_secs,
763                    self.hello_timeout_secs,
764                    self.idle_timeout_secs,
765                )
766                .await
767            }
768            Transport::Grpc => {
769                #[cfg(feature = "grpc")]
770                {
771                    Connection::new_grpc(
772                        &self.host,
773                        self.port,
774                        self.tls_enabled,
775                        self.skip_verify,
776                        self.page_size,
777                        self.username.as_deref(),
778                        password_ref,
779                        self.graph.as_deref(),
780                        self.tenant.as_deref(),
781                        self.role.as_deref(),
782                    )
783                    .await
784                }
785                #[cfg(not(feature = "grpc"))]
786                {
787                    Err(Error::connection(
788                        "gRPC transport requires the 'grpc' feature to be enabled",
789                    ))
790                }
791            }
792        }
793    }
794}
795
796/// Internal connection type for transport-specific implementations.
797#[allow(dead_code)]
798enum ConnectionKind {
799    /// QUIC transport connection
800    Quic {
801        conn: quinn::Connection,
802        send: quinn::SendStream,
803        recv: quinn::RecvStream,
804        /// Reserved for future streaming support
805        buffer: Vec<u8>,
806        /// Reserved for request ID tracking
807        next_request_id: u64,
808        /// Session ID from HELLO handshake
809        session_id: String,
810    },
811    /// gRPC transport connection
812    #[cfg(feature = "grpc")]
813    Grpc {
814        client: Box<crate::grpc::GrpcClient>,
815    },
816}
817
818/// An active connection to a Geode database server.
819///
820/// A `Connection` represents a connection to the Geode server using either
821/// QUIC or gRPC transport. It provides methods for executing queries, managing
822/// transactions, and controlling the connection lifecycle.
823///
824/// # Transport Support
825///
826/// - **QUIC**: Uses a bidirectional stream with protobuf wire protocol
827/// - **gRPC**: Uses tonic-based gRPC client (requires `grpc` feature)
828///
829/// # Connection Lifecycle
830///
831/// 1. Create via [`Client::connect`]
832/// 2. Execute queries with [`query`](Connection::query) or [`query_with_params`](Connection::query_with_params)
833/// 3. Optionally use transactions with [`begin`](Connection::begin), [`commit`](Connection::commit), [`rollback`](Connection::rollback)
834/// 4. Close with [`close`](Connection::close)
835///
836/// # Example
837///
838/// ```no_run
839/// # use geode_client::Client;
840/// # async fn example() -> geode_client::Result<()> {
841/// let client = Client::new("localhost", 3141).skip_verify(true);
842/// let mut conn = client.connect().await?;
843///
844/// // Execute queries
845/// let (page, _) = conn.query("RETURN 42 AS answer").await?;
846/// println!("Answer: {}", page.rows[0].get("answer").unwrap().as_int()?);
847///
848/// // Use transactions
849/// conn.begin().await?;
850/// conn.query("CREATE (n:Node {id: 1})").await?;
851/// conn.commit().await?;
852///
853/// conn.close()?;
854/// # Ok(())
855/// # }
856/// ```
857///
858/// # Thread Safety
859///
860/// `Connection` is `!Sync` because the underlying transport streams are not thread-safe.
861/// For concurrent access, use [`ConnectionPool`](crate::ConnectionPool).
862pub struct Connection {
863    kind: ConnectionKind,
864    /// Page size for query results (reserved for future use)
865    #[allow(dead_code)]
866    page_size: usize,
867    /// Whether this connection is currently inside a transaction
868    in_transaction: bool,
869}
870
871impl Connection {
872    /// Create a new QUIC connection.
873    #[allow(clippy::too_many_arguments)]
874    async fn new_quic(
875        host: &str,
876        port: u16,
877        skip_verify: bool,
878        page_size: usize,
879        hello_name: &str,
880        hello_ver: &str,
881        conformance: &str,
882        username: Option<&str>,
883        password: Option<&str>,
884        graph: Option<&str>,
885        tenant: Option<&str>,
886        role: Option<&str>,
887        connect_timeout_secs: u64,
888        hello_timeout_secs: u64,
889        idle_timeout_secs: u64,
890    ) -> Result<Self> {
891        let mut last_err: Option<Error> = None;
892
893        for attempt in 1..=3 {
894            match Self::connect_quic_once(
895                host,
896                port,
897                skip_verify,
898                page_size,
899                hello_name,
900                hello_ver,
901                conformance,
902                username,
903                password,
904                graph,
905                tenant,
906                role,
907                connect_timeout_secs,
908                hello_timeout_secs,
909                idle_timeout_secs,
910            )
911            .await
912            {
913                Ok(conn) => return Ok(conn),
914                Err(e) => {
915                    last_err = Some(e);
916                    if attempt < 3 {
917                        debug!("Connection attempt {} failed, retrying...", attempt);
918                        tokio::time::sleep(Duration::from_millis(150)).await;
919                    }
920                }
921            }
922        }
923
924        Err(last_err.unwrap_or_else(|| Error::connection("Failed to connect")))
925    }
926
927    /// Create a new gRPC connection.
928    #[cfg(feature = "grpc")]
929    #[allow(clippy::too_many_arguments)]
930    async fn new_grpc(
931        host: &str,
932        port: u16,
933        tls_enabled: bool,
934        skip_verify: bool,
935        page_size: usize,
936        username: Option<&str>,
937        password: Option<&str>,
938        graph: Option<&str>,
939        tenant: Option<&str>,
940        role: Option<&str>,
941    ) -> Result<Self> {
942        use crate::dsn::Dsn;
943
944        // Build DSN for gRPC client
945        let tls_val = if tls_enabled { "1" } else { "0" };
946        let graph_suffix = graph
947            .map(|g| format!("&graph={}", urlencoding::encode(g)))
948            .unwrap_or_default();
949        let tenant_suffix = tenant
950            .map(|t| format!("&tenant={}", urlencoding::encode(t)))
951            .unwrap_or_default();
952        let role_suffix = role
953            .map(|r| format!("&role={}", urlencoding::encode(r)))
954            .unwrap_or_default();
955        let dsn_str = if let (Some(user), Some(pass)) = (username, password) {
956            format!(
957                "grpc://{}:{}@{}:{}?tls={}&insecure={}{}{}{}",
958                user,
959                pass,
960                host,
961                port,
962                tls_val,
963                skip_verify,
964                graph_suffix,
965                tenant_suffix,
966                role_suffix
967            )
968        } else {
969            format!(
970                "grpc://{}:{}?tls={}&insecure={}{}{}{}",
971                host, port, tls_val, skip_verify, graph_suffix, tenant_suffix, role_suffix
972            )
973        };
974
975        let dsn = Dsn::parse(&dsn_str)?;
976        let client = Box::new(crate::grpc::GrpcClient::connect(&dsn).await?);
977
978        Ok(Self {
979            kind: ConnectionKind::Grpc { client },
980            page_size,
981            in_transaction: false,
982        })
983    }
984
985    #[allow(clippy::too_many_arguments)]
986    async fn connect_quic_once(
987        host: &str,
988        port: u16,
989        skip_verify: bool,
990        page_size: usize,
991        hello_name: &str,
992        hello_ver: &str,
993        conformance: &str,
994        username: Option<&str>,
995        password: Option<&str>,
996        graph: Option<&str>,
997        tenant: Option<&str>,
998        role: Option<&str>,
999        connect_timeout_secs: u64,
1000        hello_timeout_secs: u64,
1001        idle_timeout_secs: u64,
1002    ) -> Result<Self> {
1003        debug!("Creating connection to {}:{}", host, port);
1004
1005        // Install default crypto provider for rustls
1006        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
1007
1008        // Build Quinn client config with TLS 1.3 explicitly (QUIC requires TLS 1.3)
1009        let mut client_crypto = if skip_verify {
1010            // SECURITY WARNING: Disabling TLS verification exposes connections to MITM attacks.
1011            // Credentials sent in HELLO may be intercepted. Only use for development/testing.
1012            warn!(
1013                "TLS certificate verification DISABLED - connection to {}:{} is vulnerable to MITM attacks. \
1014                 Do NOT use skip_verify in production!",
1015                host, port
1016            );
1017            rustls::ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
1018                .dangerous()
1019                .with_custom_certificate_verifier(Arc::new(SkipServerVerification))
1020                .with_no_client_auth()
1021        } else {
1022            // Load system root certificates for proper TLS verification
1023            let mut root_store = rustls::RootCertStore::empty();
1024
1025            let cert_result = rustls_native_certs::load_native_certs();
1026
1027            // Log any errors that occurred during certificate loading
1028            for err in &cert_result.errors {
1029                warn!("Error loading native certificate: {:?}", err);
1030            }
1031
1032            let mut certs_loaded = 0;
1033            let mut certs_failed = 0;
1034
1035            for cert in cert_result.certs {
1036                match root_store.add(cert) {
1037                    Ok(()) => certs_loaded += 1,
1038                    Err(_) => certs_failed += 1,
1039                }
1040            }
1041
1042            if certs_loaded == 0 {
1043                return Err(Error::tls(
1044                    "No system root certificates found. TLS verification cannot proceed. \
1045                     Either install system CA certificates or use skip_verify(true) for development only.",
1046                ));
1047            }
1048
1049            debug!(
1050                "Loaded {} system root certificates ({} failed to parse)",
1051                certs_loaded, certs_failed
1052            );
1053
1054            rustls::ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
1055                .with_root_certificates(root_store)
1056                .with_no_client_auth()
1057        };
1058
1059        // Set ALPN protocols
1060        client_crypto.alpn_protocols = vec![GEODE_ALPN.to_vec()];
1061
1062        let mut client_config = ClientConfig::new(Arc::new(
1063            quinn::crypto::rustls::QuicClientConfig::try_from(client_crypto)
1064                .map_err(|e| Error::connection(format!("Failed to create QUIC config: {}", e)))?,
1065        ));
1066
1067        // Configure QUIC transport parameters to match Python/Go clients
1068        let mut transport = quinn::TransportConfig::default();
1069        // Cap idle timeout to quinn's maximum (2^62 - 1 microseconds ≈ 146 years)
1070        // to prevent panic from VarInt overflow
1071        let idle_timeout = Duration::from_secs(idle_timeout_secs.min(146_000 * 365 * 24 * 3600));
1072        transport.max_idle_timeout(Some(idle_timeout.try_into().map_err(|_| {
1073            Error::connection("Idle timeout value too large for QUIC protocol")
1074        })?));
1075        transport.keep_alive_interval(Some(Duration::from_secs(5)));
1076        client_config.transport_config(Arc::new(transport));
1077
1078        // Create endpoint - "0.0.0.0:0" is a valid socket address literal that binds
1079        // to any available port on all interfaces, so this parse cannot fail
1080        let mut endpoint = Endpoint::client(
1081            "0.0.0.0:0"
1082                .parse()
1083                .expect("0.0.0.0:0 is a valid socket address"),
1084        )
1085        .map_err(|e| Error::connection(format!("Failed to create endpoint: {}", e)))?;
1086        endpoint.set_default_client_config(client_config);
1087
1088        // Resolve server address (supports hostnames as well as IP literals)
1089        let mut resolved_addrs = format!("{}:{}", host, port)
1090            .to_socket_addrs()
1091            .map_err(|e| {
1092                Error::connection(format!(
1093                    "Failed to resolve address {}:{} - {}",
1094                    host, port, e
1095                ))
1096            })?;
1097
1098        let server_addr: SocketAddr = resolved_addrs
1099            .find(|addr| matches!(addr, SocketAddr::V4(_) | SocketAddr::V6(_)))
1100            .ok_or_else(|| Error::connection("Invalid address: could not resolve host"))?;
1101
1102        debug!("Connecting to {}", server_addr);
1103
1104        // When skipping verification, don't use actual hostname for SNI
1105        // This matches Python client behavior which avoids server_name when skip_verify=True
1106        let server_name = if skip_verify {
1107            "localhost" // Use generic name when skipping verification
1108        } else {
1109            host
1110        };
1111
1112        trace!("Using server name for SNI: {}", server_name);
1113
1114        let conn = timeout(
1115            Duration::from_secs(connect_timeout_secs),
1116            endpoint
1117                .connect(server_addr, server_name)
1118                .map_err(|e| Error::connection(format!("Connection failed: {}", e)))?,
1119        )
1120        .await
1121        .map_err(|_| Error::connection("Connection timeout"))?
1122        .map_err(|e| Error::connection(format!("Failed to establish connection: {}", e)))?;
1123
1124        debug!("Connection established to {}:{}", host, port);
1125
1126        // Open a single bidirectional stream used for the entire session.
1127        let (mut send, mut recv) = conn
1128            .open_bi()
1129            .await
1130            .map_err(|e| Error::connection(format!("Failed to open stream: {}", e)))?;
1131
1132        // Send HELLO message using protobuf with length prefix
1133        let hello_req = proto::HelloRequest {
1134            username: username.unwrap_or("").to_string(),
1135            password: password.unwrap_or("").to_string(),
1136            tenant_id: tenant.map(String::from),
1137            client_name: hello_name.to_string(),
1138            client_version: hello_ver.to_string(),
1139            wanted_conformance: conformance.to_string(),
1140            graph: graph.map(String::from),
1141            role: role.map(String::from),
1142        };
1143        let msg = proto::QuicClientMessage {
1144            msg: Some(proto::quic_client_message::Msg::Hello(hello_req)),
1145        };
1146        let data = proto::encode_with_length_prefix(&msg);
1147
1148        send.write_all(&data)
1149            .await
1150            .map_err(|e| Error::connection(format!("Failed to send HELLO: {}", e)))?;
1151
1152        // Wait for HELLO response (length-prefixed protobuf)
1153        let mut length_buf = [0u8; 4];
1154        timeout(
1155            Duration::from_secs(hello_timeout_secs),
1156            recv.read_exact(&mut length_buf),
1157        )
1158        .await
1159        .map_err(|_| Error::connection("HELLO response timeout"))?
1160        .map_err(|e| Error::connection(format!("Failed to read HELLO response length: {}", e)))?;
1161
1162        let msg_len = u32::from_be_bytes(length_buf) as usize;
1163
1164        if msg_len > MAX_PROTO_FRAME_BYTES {
1165            return Err(Error::limit(format!(
1166                "HELLO response frame size {} bytes exceeds max {} bytes",
1167                msg_len, MAX_PROTO_FRAME_BYTES
1168            )));
1169        }
1170
1171        let mut msg_buf = vec![0u8; msg_len];
1172        recv.read_exact(&mut msg_buf)
1173            .await
1174            .map_err(|e| Error::connection(format!("Failed to read HELLO response body: {}", e)))?;
1175
1176        let hello_response = proto::decode_quic_server_message(&msg_buf)?;
1177
1178        let session_id = match hello_response.msg {
1179            Some(proto::quic_server_message::Msg::Hello(ref hello_resp)) => {
1180                if !hello_resp.success {
1181                    return Err(Error::connection(format!(
1182                        "Authentication failed: {}",
1183                        hello_resp.error_message
1184                    )));
1185                }
1186                hello_resp.session_id.clone()
1187            }
1188            _ => {
1189                return Err(Error::connection("Expected HELLO response"));
1190            }
1191        };
1192
1193        debug!("HELLO handshake complete, session_id={}", session_id);
1194
1195        Ok(Self {
1196            kind: ConnectionKind::Quic {
1197                conn,
1198                send,
1199                recv,
1200                buffer: Vec::new(),
1201                next_request_id: 1,
1202                session_id,
1203            },
1204            page_size,
1205            in_transaction: false,
1206        })
1207    }
1208
1209    /// Send a protobuf message over QUIC.
1210    async fn send_proto_quic(
1211        send: &mut quinn::SendStream,
1212        msg: &proto::QuicClientMessage,
1213    ) -> Result<()> {
1214        let data = proto::encode_with_length_prefix(msg);
1215        send.write_all(&data)
1216            .await
1217            .map_err(|e| Error::connection(format!("Failed to send message: {}", e)))?;
1218        Ok(())
1219    }
1220
1221    /// Read a protobuf message over QUIC with timeout.
1222    /// Timeout covers the entire read (length prefix + body) to prevent
1223    /// hangs when large responses arrive slowly across multiple QUIC frames.
1224    /// Enforces MAX_PROTO_FRAME_BYTES to prevent unbounded allocation (CWE-400).
1225    async fn read_proto_quic(
1226        recv: &mut quinn::RecvStream,
1227        timeout_secs: u64,
1228    ) -> Result<proto::QuicServerMessage> {
1229        timeout(Duration::from_secs(timeout_secs), async {
1230            // Read 4-byte length prefix
1231            let mut length_buf = [0u8; 4];
1232            recv.read_exact(&mut length_buf)
1233                .await
1234                .map_err(|e| Error::connection(format!("Failed to read response length: {}", e)))?;
1235
1236            let msg_len = u32::from_be_bytes(length_buf) as usize;
1237
1238            // Enforce frame size limit to prevent unbounded memory allocation
1239            if msg_len > MAX_PROTO_FRAME_BYTES {
1240                return Err(Error::limit(format!(
1241                    "Frame size {} bytes exceeds max {} bytes",
1242                    msg_len, MAX_PROTO_FRAME_BYTES
1243                )));
1244            }
1245
1246            let mut msg_buf = vec![0u8; msg_len];
1247            recv.read_exact(&mut msg_buf)
1248                .await
1249                .map_err(|e| Error::connection(format!("Failed to read response body: {}", e)))?;
1250
1251            proto::decode_quic_server_message(&msg_buf)
1252        })
1253        .await
1254        .map_err(|_| Error::timeout())?
1255    }
1256
1257    /// Parse protobuf rows into Value maps (static version for QUIC).
1258    fn parse_proto_rows_static(
1259        proto_rows: &[proto::Row],
1260        columns: &[Column],
1261    ) -> Result<Vec<HashMap<String, Value>>> {
1262        let mut rows = Vec::with_capacity(proto_rows.len());
1263        for proto_row in proto_rows {
1264            let mut row = HashMap::with_capacity(columns.len());
1265            for (i, col) in columns.iter().enumerate() {
1266                let value = if i < proto_row.values.len() {
1267                    crate::convert::proto_to_value(&proto_row.values[i])
1268                } else {
1269                    Value::null()
1270                };
1271                row.insert(col.name.clone(), value);
1272            }
1273            rows.push(row);
1274        }
1275        Ok(rows)
1276    }
1277
1278    /// Send BEGIN transaction command over QUIC.
1279    async fn send_begin_quic(
1280        send: &mut quinn::SendStream,
1281        recv: &mut quinn::RecvStream,
1282        session_id: &str,
1283    ) -> Result<()> {
1284        let msg = proto::QuicClientMessage {
1285            msg: Some(proto::quic_client_message::Msg::Begin(
1286                proto::BeginRequest {
1287                    session_id: session_id.to_string(),
1288                    ..Default::default()
1289                },
1290            )),
1291        };
1292        Self::send_proto_quic(send, &msg).await?;
1293
1294        let resp = Self::read_proto_quic(recv, 5).await?;
1295        if !matches!(resp.msg, Some(proto::quic_server_message::Msg::Begin(_))) {
1296            return Err(Error::protocol("Expected BEGIN response"));
1297        }
1298        Ok(())
1299    }
1300
1301    /// Send COMMIT transaction command over QUIC.
1302    async fn send_commit_quic(
1303        send: &mut quinn::SendStream,
1304        recv: &mut quinn::RecvStream,
1305        session_id: &str,
1306    ) -> Result<()> {
1307        let msg = proto::QuicClientMessage {
1308            msg: Some(proto::quic_client_message::Msg::Commit(
1309                proto::CommitRequest {
1310                    session_id: session_id.to_string(),
1311                },
1312            )),
1313        };
1314        Self::send_proto_quic(send, &msg).await?;
1315
1316        let resp = Self::read_proto_quic(recv, 5).await?;
1317        if !matches!(resp.msg, Some(proto::quic_server_message::Msg::Commit(_))) {
1318            return Err(Error::protocol("Expected COMMIT response"));
1319        }
1320        Ok(())
1321    }
1322
1323    /// Send ROLLBACK transaction command over QUIC.
1324    async fn send_rollback_quic(
1325        send: &mut quinn::SendStream,
1326        recv: &mut quinn::RecvStream,
1327        session_id: &str,
1328    ) -> Result<()> {
1329        let msg = proto::QuicClientMessage {
1330            msg: Some(proto::quic_client_message::Msg::Rollback(
1331                proto::RollbackRequest {
1332                    session_id: session_id.to_string(),
1333                },
1334            )),
1335        };
1336        Self::send_proto_quic(send, &msg).await?;
1337
1338        let resp = Self::read_proto_quic(recv, 5).await?;
1339        if !matches!(resp.msg, Some(proto::quic_server_message::Msg::Rollback(_))) {
1340            return Err(Error::protocol("Expected ROLLBACK response"));
1341        }
1342        Ok(())
1343    }
1344
1345    /// Send SAVEPOINT command over QUIC.
1346    async fn send_savepoint_quic(
1347        send: &mut quinn::SendStream,
1348        recv: &mut quinn::RecvStream,
1349        session_id: &str,
1350        name: &str,
1351    ) -> Result<()> {
1352        let msg = proto::QuicClientMessage {
1353            msg: Some(proto::quic_client_message::Msg::Savepoint(
1354                proto::SavepointRequest {
1355                    name: name.to_string(),
1356                    session_id: session_id.to_string(),
1357                },
1358            )),
1359        };
1360        Self::send_proto_quic(send, &msg).await?;
1361
1362        let resp = Self::read_proto_quic(recv, 5).await?;
1363        if !matches!(
1364            resp.msg,
1365            Some(proto::quic_server_message::Msg::Savepoint(_))
1366        ) {
1367            return Err(Error::protocol("Expected SAVEPOINT response"));
1368        }
1369        Ok(())
1370    }
1371
1372    /// Send ROLLBACK TO command over QUIC.
1373    async fn send_rollback_to_quic(
1374        send: &mut quinn::SendStream,
1375        recv: &mut quinn::RecvStream,
1376        session_id: &str,
1377        name: &str,
1378    ) -> Result<()> {
1379        let msg = proto::QuicClientMessage {
1380            msg: Some(proto::quic_client_message::Msg::RollbackTo(
1381                proto::RollbackToRequest {
1382                    name: name.to_string(),
1383                    session_id: session_id.to_string(),
1384                },
1385            )),
1386        };
1387        Self::send_proto_quic(send, &msg).await?;
1388
1389        let resp = Self::read_proto_quic(recv, 5).await?;
1390        if !matches!(
1391            resp.msg,
1392            Some(proto::quic_server_message::Msg::RollbackTo(_))
1393        ) {
1394            return Err(Error::protocol("Expected ROLLBACK_TO response"));
1395        }
1396        Ok(())
1397    }
1398
1399    /// Execute a GQL query without parameters.
1400    ///
1401    /// # Arguments
1402    ///
1403    /// * `gql` - The GQL query string
1404    ///
1405    /// # Returns
1406    ///
1407    /// A tuple of (`Page`, `Option<String>`) where the page contains the results
1408    /// and the optional string contains any query warnings.
1409    ///
1410    /// # Errors
1411    ///
1412    /// Returns [`Error::Query`] if the query fails to execute.
1413    ///
1414    /// # Example
1415    ///
1416    /// ```no_run
1417    /// # use geode_client::Client;
1418    /// # async fn example() -> geode_client::Result<()> {
1419    /// # let client = Client::new("localhost", 3141).skip_verify(true);
1420    /// # let mut conn = client.connect().await?;
1421    /// let (page, _) = conn.query("MATCH (n:Person) RETURN n.name LIMIT 10").await?;
1422    /// for row in &page.rows {
1423    ///     println!("Name: {}", row.get("name").unwrap().as_string()?);
1424    /// }
1425    /// # Ok(())
1426    /// # }
1427    /// ```
1428    pub async fn query(&mut self, gql: &str) -> Result<(Page, Option<String>)> {
1429        self.query_with_params(gql, &HashMap::new()).await
1430    }
1431
1432    /// Execute a GQL query with parameters.
1433    ///
1434    /// Parameters are substituted for `$param_name` placeholders in the query.
1435    /// This is the recommended way to include dynamic values in queries, as it
1436    /// prevents injection attacks and allows query plan caching.
1437    ///
1438    /// # Arguments
1439    ///
1440    /// * `gql` - The GQL query string with parameter placeholders
1441    /// * `params` - A map of parameter names to values
1442    ///
1443    /// # Returns
1444    ///
1445    /// A tuple of (`Page`, `Option<String>`) where the page contains the results
1446    /// and the optional string contains any query warnings.
1447    ///
1448    /// # Errors
1449    ///
1450    /// Returns [`Error::Query`] if the query fails to execute.
1451    ///
1452    /// # Example
1453    ///
1454    /// ```no_run
1455    /// # use geode_client::{Client, Value};
1456    /// # use std::collections::HashMap;
1457    /// # async fn example() -> geode_client::Result<()> {
1458    /// # let client = Client::new("localhost", 3141).skip_verify(true);
1459    /// # let mut conn = client.connect().await?;
1460    /// let mut params = HashMap::new();
1461    /// params.insert("name".to_string(), Value::string("Alice"));
1462    /// params.insert("min_age".to_string(), Value::int(25));
1463    ///
1464    /// let (page, _) = conn.query_with_params(
1465    ///     "MATCH (p:Person {name: $name}) WHERE p.age >= $min_age RETURN p",
1466    ///     &params
1467    /// ).await?;
1468    /// # Ok(())
1469    /// # }
1470    /// ```
1471    pub async fn query_with_params(
1472        &mut self,
1473        gql: &str,
1474        params: &HashMap<String, Value>,
1475    ) -> Result<(Page, Option<String>)> {
1476        validate::query(gql)?;
1477        for key in params.keys() {
1478            validate::param_name(key)?;
1479        }
1480        match &mut self.kind {
1481            ConnectionKind::Quic {
1482                send,
1483                recv,
1484                buffer,
1485                session_id,
1486                ..
1487            } => {
1488                Self::query_with_params_quic(
1489                    send,
1490                    recv,
1491                    buffer,
1492                    gql,
1493                    params,
1494                    session_id,
1495                    self.page_size,
1496                )
1497                .await
1498            }
1499            #[cfg(feature = "grpc")]
1500            ConnectionKind::Grpc { client } => client.query_with_params(gql, params).await,
1501        }
1502    }
1503
1504    /// Execute a GQL query with parameters over QUIC transport.
1505    async fn query_with_params_quic(
1506        send: &mut quinn::SendStream,
1507        recv: &mut quinn::RecvStream,
1508        buffer: &mut Vec<u8>,
1509        gql: &str,
1510        params: &HashMap<String, Value>,
1511        session_id: &str,
1512        page_size: usize,
1513    ) -> Result<(Page, Option<String>)> {
1514        let (page, cursor) =
1515            Self::query_with_params_quic_inner(send, recv, buffer, gql, params, session_id).await?;
1516
1517        // If the first page is not final, fetch remaining pages via PULL
1518        if !page.final_page {
1519            let mut all_rows = page.rows;
1520            let columns = page.columns;
1521            let mut ordered = page.ordered;
1522            let mut order_keys = page.order_keys;
1523            let mut request_id: u64 = 0;
1524            let mut page_count: usize = 1; // first page already fetched
1525
1526            loop {
1527                // Enforce row and page limits to prevent unbounded memory growth (CWE-400)
1528                if all_rows.len() > DEFAULT_MAX_ROWS {
1529                    return Err(Error::limit(format!(
1530                        "Total rows {} exceeds max {}",
1531                        all_rows.len(),
1532                        DEFAULT_MAX_ROWS
1533                    )));
1534                }
1535                page_count += 1;
1536                if page_count > DEFAULT_MAX_PAGES {
1537                    return Err(Error::limit(format!(
1538                        "Page count {} exceeds max {}",
1539                        page_count, DEFAULT_MAX_PAGES
1540                    )));
1541                }
1542
1543                request_id += 1;
1544                let pull_req = proto::QuicClientMessage {
1545                    msg: Some(proto::quic_client_message::Msg::Pull(proto::PullRequest {
1546                        request_id,
1547                        page_size: page_size as u32,
1548                        session_id: session_id.to_string(),
1549                    })),
1550                };
1551                Self::send_proto_quic(send, &pull_req).await?;
1552
1553                let resp = Self::read_proto_quic_buffered(recv, buffer, 30).await?;
1554
1555                // Server sends pull data in pull.response
1556                let exec_resp = match &resp.msg {
1557                    Some(proto::quic_server_message::Msg::Pull(pull)) => pull.response.as_ref(),
1558                    Some(proto::quic_server_message::Msg::Execute(e)) => Some(e),
1559                    _ => None,
1560                };
1561
1562                let exec_resp = match exec_resp {
1563                    Some(e) => e,
1564                    None => break,
1565                };
1566
1567                if let Some(proto::execution_response::Payload::Error(ref err)) = exec_resp.payload
1568                {
1569                    return Err(Error::Query {
1570                        code: err.code.clone(),
1571                        message: err.message.clone(),
1572                    });
1573                }
1574
1575                if let Some(proto::execution_response::Payload::Page(ref page_data)) =
1576                    exec_resp.payload
1577                {
1578                    let rows = Self::parse_proto_rows_static(&page_data.rows, &columns)?;
1579                    all_rows.extend(rows);
1580                    ordered = page_data.ordered;
1581                    order_keys = page_data.order_keys.clone();
1582                    if page_data.r#final {
1583                        break;
1584                    }
1585                } else {
1586                    break;
1587                }
1588            }
1589
1590            let final_page = Page {
1591                columns,
1592                rows: all_rows,
1593                ordered,
1594                order_keys,
1595                final_page: true,
1596            };
1597            Self::drain_execute_trailers_quic(recv, buffer).await?;
1598            return Ok((final_page, cursor));
1599        }
1600
1601        if page.final_page {
1602            Self::drain_execute_trailers_quic(recv, buffer).await?;
1603        }
1604        Ok((page, cursor))
1605    }
1606
1607    /// Inner implementation for QUIC query (reads first page).
1608    async fn query_with_params_quic_inner(
1609        send: &mut quinn::SendStream,
1610        recv: &mut quinn::RecvStream,
1611        buffer: &mut Vec<u8>,
1612        gql: &str,
1613        params: &HashMap<String, Value>,
1614        session_id: &str,
1615    ) -> Result<(Page, Option<String>)> {
1616        // Convert types::Value to proto::Param entries
1617        let params_proto: Vec<proto::Param> = params
1618            .iter()
1619            .map(|(k, v)| proto::Param {
1620                name: k.clone(),
1621                value: Some(v.to_proto_value()),
1622            })
1623            .collect();
1624
1625        // Send Execute request via protobuf
1626        let exec_req = proto::ExecuteRequest {
1627            session_id: session_id.to_string(),
1628            query: gql.to_string(),
1629            params: params_proto,
1630        };
1631        let msg = proto::QuicClientMessage {
1632            msg: Some(proto::quic_client_message::Msg::Execute(exec_req)),
1633        };
1634        Self::send_proto_quic(send, &msg)
1635            .await
1636            .map_err(|e| Error::query(format!("{}", e)))?;
1637
1638        // Read first response (should be schema or error)
1639        let resp = Self::read_proto_quic_buffered(recv, buffer, 10).await?;
1640
1641        let exec_resp = match resp.msg {
1642            Some(proto::quic_server_message::Msg::Execute(e)) => e,
1643            _ => return Err(Error::protocol("Expected Execute response")),
1644        };
1645
1646        // Check for error in payload
1647        if let Some(proto::execution_response::Payload::Error(ref err)) = exec_resp.payload {
1648            // Drain any follow-up messages (e.g., data page) the server may send after error
1649            let _ = Self::try_read_proto_quic_buffered(recv, buffer).await;
1650            return Err(Error::Query {
1651                code: err.code.clone(),
1652                message: err.message.clone(),
1653            });
1654        }
1655
1656        // Parse columns from schema payload
1657        let columns: Vec<Column> = match exec_resp.payload {
1658            Some(proto::execution_response::Payload::Schema(ref s)) => s
1659                .columns
1660                .iter()
1661                .map(|c| Column {
1662                    name: c.name.clone(),
1663                    col_type: c.r#type.clone(),
1664                })
1665                .collect(),
1666            _ => Vec::new(),
1667        };
1668
1669        trace!("Schema columns: {:?}", columns);
1670
1671        // The server may emit metrics or heartbeat frames between schema and the
1672        // first data page. Ignore non-page execute frames until we see data.
1673        while let Some(inline_resp) = Self::try_read_proto_quic_buffered(recv, buffer).await? {
1674            if let Some(proto::quic_server_message::Msg::Execute(inline_exec)) = inline_resp.msg {
1675                if let Some(proto::execution_response::Payload::Error(ref err)) =
1676                    inline_exec.payload
1677                {
1678                    return Err(Error::Query {
1679                        code: err.code.clone(),
1680                        message: err.message.clone(),
1681                    });
1682                }
1683
1684                if let Some(proto::execution_response::Payload::Page(ref page_data)) =
1685                    inline_exec.payload
1686                {
1687                    let rows = Self::parse_proto_rows_static(&page_data.rows, &columns)?;
1688                    let page = Page {
1689                        columns,
1690                        rows,
1691                        ordered: page_data.ordered,
1692                        order_keys: page_data.order_keys.clone(),
1693                        final_page: page_data.r#final,
1694                    };
1695                    return Ok((page, None));
1696                }
1697            }
1698        }
1699
1700        // Non-row statements can complete with a status-only response.
1701        if columns.is_empty() {
1702            return Ok((
1703                Page {
1704                    columns,
1705                    rows: Vec::new(),
1706                    ordered: false,
1707                    order_keys: Vec::new(),
1708                    final_page: true,
1709                },
1710                None,
1711            ));
1712        }
1713
1714        // Check if we got a page in the first response
1715        if let Some(proto::execution_response::Payload::Page(ref page_data)) = exec_resp.payload {
1716            let rows = Self::parse_proto_rows_static(&page_data.rows, &columns)?;
1717            let page = Page {
1718                columns,
1719                rows,
1720                ordered: page_data.ordered,
1721                order_keys: page_data.order_keys.clone(),
1722                final_page: page_data.r#final,
1723            };
1724            return Ok((page, None));
1725        }
1726
1727        // Read blocking responses until we see a data page or query error.
1728        loop {
1729            let resp = Self::read_proto_quic_buffered(recv, buffer, 30).await?;
1730            if let Some(proto::quic_server_message::Msg::Execute(exec_resp)) = resp.msg {
1731                if let Some(proto::execution_response::Payload::Error(ref err)) = exec_resp.payload
1732                {
1733                    return Err(Error::Query {
1734                        code: err.code.clone(),
1735                        message: err.message.clone(),
1736                    });
1737                }
1738
1739                if let Some(proto::execution_response::Payload::Page(ref page_data)) =
1740                    exec_resp.payload
1741                {
1742                    let rows = Self::parse_proto_rows_static(&page_data.rows, &columns)?;
1743                    let page = Page {
1744                        columns,
1745                        rows,
1746                        ordered: page_data.ordered,
1747                        order_keys: page_data.order_keys.clone(),
1748                        final_page: page_data.r#final,
1749                    };
1750                    return Ok((page, None));
1751                }
1752            }
1753        }
1754    }
1755
1756    fn decode_buffered_quic_message(
1757        buffer: &mut Vec<u8>,
1758    ) -> Result<Option<proto::QuicServerMessage>> {
1759        if buffer.len() < 4 {
1760            return Ok(None);
1761        }
1762
1763        let msg_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
1764        if msg_len > MAX_PROTO_FRAME_BYTES {
1765            return Err(Error::limit(format!(
1766                "Frame size {} bytes exceeds max {} bytes",
1767                msg_len, MAX_PROTO_FRAME_BYTES
1768            )));
1769        }
1770
1771        let total_len = 4 + msg_len;
1772        if buffer.len() < total_len {
1773            return Ok(None);
1774        }
1775
1776        let msg = proto::decode_quic_server_message(&buffer[4..total_len])?;
1777        buffer.drain(..total_len);
1778        Ok(Some(msg))
1779    }
1780
1781    async fn read_proto_quic_buffered(
1782        recv: &mut quinn::RecvStream,
1783        buffer: &mut Vec<u8>,
1784        timeout_secs: u64,
1785    ) -> Result<proto::QuicServerMessage> {
1786        if let Some(msg) = Self::decode_buffered_quic_message(buffer)? {
1787            return Ok(msg);
1788        }
1789
1790        let deadline = Instant::now() + Duration::from_secs(timeout_secs);
1791        loop {
1792            let now = Instant::now();
1793            if now >= deadline {
1794                return Err(Error::timeout());
1795            }
1796
1797            let remaining = deadline.saturating_duration_since(now);
1798            let chunk = timeout(remaining, recv.read_chunk(MAX_PROTO_FRAME_BYTES, true))
1799                .await
1800                .map_err(|_| Error::timeout())?
1801                .map_err(|e| Error::connection(format!("Failed to read response: {}", e)))?
1802                .ok_or_else(|| Error::connection("Stream closed while reading response"))?;
1803            buffer.extend_from_slice(&chunk.bytes);
1804
1805            if let Some(msg) = Self::decode_buffered_quic_message(buffer)? {
1806                return Ok(msg);
1807            }
1808        }
1809    }
1810
1811    async fn try_read_proto_quic_buffered(
1812        recv: &mut quinn::RecvStream,
1813        buffer: &mut Vec<u8>,
1814    ) -> Result<Option<proto::QuicServerMessage>> {
1815        if let Some(msg) = Self::decode_buffered_quic_message(buffer)? {
1816            return Ok(Some(msg));
1817        }
1818
1819        let read_result = timeout(
1820            Duration::from_millis(500),
1821            recv.read_chunk(MAX_PROTO_FRAME_BYTES, true),
1822        )
1823        .await;
1824        let chunk = match read_result {
1825            Ok(Ok(Some(chunk))) => chunk,
1826            Ok(Ok(None)) => return Ok(None),
1827            Ok(Err(e)) => {
1828                return Err(Error::connection(format!("Failed to read response: {}", e)));
1829            }
1830            Err(_) => return Ok(None),
1831        };
1832
1833        buffer.extend_from_slice(&chunk.bytes);
1834        Self::decode_buffered_quic_message(buffer)
1835    }
1836
1837    async fn drain_execute_trailers_quic(
1838        recv: &mut quinn::RecvStream,
1839        buffer: &mut Vec<u8>,
1840    ) -> Result<()> {
1841        while let Some(resp) = Self::try_read_proto_quic_buffered(recv, buffer).await? {
1842            let Some(proto::quic_server_message::Msg::Execute(exec)) = resp.msg else {
1843                break;
1844            };
1845
1846            let is_trailer = matches!(
1847                exec.payload,
1848                Some(proto::execution_response::Payload::Metrics(_))
1849                    | Some(proto::execution_response::Payload::Heartbeat(_))
1850            );
1851            if !is_trailer {
1852                break;
1853            }
1854        }
1855        Ok(())
1856    }
1857
1858    /// Execute a query without parameters (synchronous-style blocking version for test runner)
1859    pub fn query_sync(
1860        &mut self,
1861        gql: &str,
1862        params: Option<HashMap<String, serde_json::Value>>,
1863    ) -> Result<Page> {
1864        let params_map = params.unwrap_or_default();
1865        let mut params_typed: HashMap<String, Value> = HashMap::new();
1866        for (k, v) in params_map {
1867            let typed_val = crate::types::Value::from_json(v)?;
1868            params_typed.insert(k, typed_val);
1869        }
1870
1871        match tokio::runtime::Handle::try_current() {
1872            Ok(handle) => {
1873                let (page, _cursor) =
1874                    handle.block_on(self.query_with_params(gql, &params_typed))?;
1875                Ok(page)
1876            }
1877            Err(_) => {
1878                let rt = tokio::runtime::Runtime::new()
1879                    .map_err(|e| Error::query(format!("Failed to create runtime: {}", e)))?;
1880                let (page, _cursor) = rt.block_on(self.query_with_params(gql, &params_typed))?;
1881                Ok(page)
1882            }
1883        }
1884    }
1885
1886    /// Begin a new transaction.
1887    ///
1888    /// After calling `begin`, all queries will be part of the transaction until
1889    /// [`commit`](Connection::commit) or [`rollback`](Connection::rollback) is called.
1890    ///
1891    /// # Errors
1892    ///
1893    /// Returns an error if a transaction is already in progress or if the
1894    /// server rejects the request.
1895    ///
1896    /// # Example
1897    ///
1898    /// ```no_run
1899    /// # use geode_client::Client;
1900    /// # async fn example() -> geode_client::Result<()> {
1901    /// # let client = Client::new("localhost", 3141).skip_verify(true);
1902    /// # let mut conn = client.connect().await?;
1903    /// conn.begin().await?;
1904    /// conn.query("CREATE (n:Node {id: 1})").await?;
1905    /// conn.query("CREATE (n:Node {id: 2})").await?;
1906    /// conn.commit().await?;  // Both nodes are now persisted
1907    /// # Ok(())
1908    /// # }
1909    /// ```
1910    pub async fn begin(&mut self) -> Result<()> {
1911        let result = match &mut self.kind {
1912            ConnectionKind::Quic {
1913                send,
1914                recv,
1915                session_id,
1916                ..
1917            } => Self::send_begin_quic(send, recv, session_id).await,
1918            #[cfg(feature = "grpc")]
1919            ConnectionKind::Grpc { client } => client.begin().await,
1920        };
1921        if result.is_ok() {
1922            self.in_transaction = true;
1923        }
1924        result
1925    }
1926
1927    /// Commit the current transaction.
1928    ///
1929    /// Persists all changes made since [`begin`](Connection::begin) was called.
1930    ///
1931    /// # Errors
1932    ///
1933    /// Returns an error if no transaction is in progress or if the server
1934    /// rejects the commit.
1935    ///
1936    /// # Example
1937    ///
1938    /// ```no_run
1939    /// # use geode_client::Client;
1940    /// # async fn example() -> geode_client::Result<()> {
1941    /// # let client = Client::new("localhost", 3141).skip_verify(true);
1942    /// # let mut conn = client.connect().await?;
1943    /// conn.begin().await?;
1944    /// conn.query("CREATE (n:Node)").await?;
1945    /// conn.commit().await?;  // Changes are now permanent
1946    /// # Ok(())
1947    /// # }
1948    /// ```
1949    pub async fn commit(&mut self) -> Result<()> {
1950        let result = match &mut self.kind {
1951            ConnectionKind::Quic {
1952                send,
1953                recv,
1954                session_id,
1955                ..
1956            } => Self::send_commit_quic(send, recv, session_id).await,
1957            #[cfg(feature = "grpc")]
1958            ConnectionKind::Grpc { client } => client.commit().await,
1959        };
1960        if result.is_ok() {
1961            self.in_transaction = false;
1962        }
1963        result
1964    }
1965
1966    /// Rollback the current transaction.
1967    ///
1968    /// Discards all changes made since [`begin`](Connection::begin) was called.
1969    ///
1970    /// # Errors
1971    ///
1972    /// Returns an error if no transaction is in progress.
1973    ///
1974    /// # Example
1975    ///
1976    /// ```no_run
1977    /// # use geode_client::Client;
1978    /// # async fn example() -> geode_client::Result<()> {
1979    /// # let client = Client::new("localhost", 3141).skip_verify(true);
1980    /// # let mut conn = client.connect().await?;
1981    /// conn.begin().await?;
1982    /// match conn.query("CREATE (n:InvalidNode)").await {
1983    ///     Ok(_) => conn.commit().await?,
1984    ///     Err(_) => conn.rollback().await?,  // Undo everything
1985    /// }
1986    /// # Ok(())
1987    /// # }
1988    /// ```
1989    pub async fn rollback(&mut self) -> Result<()> {
1990        let result = match &mut self.kind {
1991            ConnectionKind::Quic {
1992                send,
1993                recv,
1994                session_id,
1995                ..
1996            } => Self::send_rollback_quic(send, recv, session_id).await,
1997            #[cfg(feature = "grpc")]
1998            ConnectionKind::Grpc { client } => client.rollback().await,
1999        };
2000        if result.is_ok() {
2001            self.in_transaction = false;
2002        }
2003        result
2004    }
2005
2006    /// Create a savepoint within the current transaction.
2007    ///
2008    /// Savepoints allow partial rollback of a transaction. You can roll back
2009    /// to a savepoint without aborting the entire transaction.
2010    ///
2011    /// # Arguments
2012    ///
2013    /// * `name` - The name for the savepoint
2014    ///
2015    /// # Returns
2016    ///
2017    /// A [`Savepoint`] that can be passed to [`rollback_to`](Connection::rollback_to).
2018    ///
2019    /// # Errors
2020    ///
2021    /// Returns an error if the server rejects the savepoint creation.
2022    ///
2023    /// # Example
2024    ///
2025    /// ```no_run
2026    /// # use geode_client::Client;
2027    /// # async fn example() -> geode_client::Result<()> {
2028    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2029    /// # let mut conn = client.connect().await?;
2030    /// conn.begin().await?;
2031    /// conn.query("CREATE (a:Node)").await?;
2032    /// let sp = conn.savepoint("sp1").await?;
2033    /// conn.query("CREATE (b:Node)").await?;
2034    /// conn.rollback_to(&sp).await?;  // Undo only the second create
2035    /// conn.commit().await?;  // First node is saved
2036    /// # Ok(())
2037    /// # }
2038    /// ```
2039    pub async fn savepoint(&mut self, name: &str) -> Result<Savepoint> {
2040        match &mut self.kind {
2041            ConnectionKind::Quic {
2042                send,
2043                recv,
2044                session_id,
2045                ..
2046            } => Self::send_savepoint_quic(send, recv, session_id, name).await?,
2047            #[cfg(feature = "grpc")]
2048            ConnectionKind::Grpc { client } => client.savepoint(name).await?,
2049        }
2050        Ok(Savepoint {
2051            name: name.to_string(),
2052        })
2053    }
2054
2055    /// Roll back to a previously created savepoint.
2056    ///
2057    /// Discards all changes made after the savepoint was created, but keeps
2058    /// the transaction active and preserves changes made before the savepoint.
2059    ///
2060    /// # Arguments
2061    ///
2062    /// * `savepoint` - The savepoint to roll back to
2063    ///
2064    /// # Errors
2065    ///
2066    /// Returns an error if the server rejects the rollback.
2067    ///
2068    /// # Example
2069    ///
2070    /// ```no_run
2071    /// # use geode_client::Client;
2072    /// # async fn example() -> geode_client::Result<()> {
2073    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2074    /// # let mut conn = client.connect().await?;
2075    /// conn.begin().await?;
2076    /// let sp = conn.savepoint("sp1").await?;
2077    /// conn.query("CREATE (n:Temp)").await?;
2078    /// conn.rollback_to(&sp).await?;  // Undo the create
2079    /// conn.commit().await?;
2080    /// # Ok(())
2081    /// # }
2082    /// ```
2083    pub async fn rollback_to(&mut self, savepoint: &Savepoint) -> Result<()> {
2084        match &mut self.kind {
2085            ConnectionKind::Quic {
2086                send,
2087                recv,
2088                session_id,
2089                ..
2090            } => Self::send_rollback_to_quic(send, recv, session_id, &savepoint.name).await?,
2091            #[cfg(feature = "grpc")]
2092            ConnectionKind::Grpc { client } => client.rollback_to(&savepoint.name).await?,
2093        }
2094        Ok(())
2095    }
2096
2097    /// Create a prepared statement for efficient repeated execution.
2098    ///
2099    /// Prepared statements allow you to define a query once and execute it
2100    /// multiple times with different parameters. The query text is parsed
2101    /// to extract parameter names (tokens starting with `$`).
2102    ///
2103    /// # Arguments
2104    ///
2105    /// * `query` - The GQL query string with parameter placeholders
2106    ///
2107    /// # Returns
2108    ///
2109    /// A [`PreparedStatement`] that can be executed multiple times.
2110    ///
2111    /// # Example
2112    ///
2113    /// ```no_run
2114    /// # use geode_client::{Client, Value};
2115    /// # use std::collections::HashMap;
2116    /// # async fn example() -> geode_client::Result<()> {
2117    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2118    /// # let mut conn = client.connect().await?;
2119    /// let stmt = conn.prepare("MATCH (p:Person {id: $id}) RETURN p.name")?;
2120    ///
2121    /// for id in 1..=100 {
2122    ///     let mut params = HashMap::new();
2123    ///     params.insert("id".to_string(), Value::int(id));
2124    ///     let (page, _) = stmt.execute(&mut conn, &params).await?;
2125    ///     // Process results...
2126    /// }
2127    /// # Ok(())
2128    /// # }
2129    /// ```
2130    pub fn prepare(&self, query: &str) -> Result<PreparedStatement> {
2131        Ok(PreparedStatement::new(query))
2132    }
2133
2134    /// Get the execution plan for a query without running it.
2135    ///
2136    /// This is useful for understanding how the database will execute a query
2137    /// and for identifying potential performance issues.
2138    ///
2139    /// # Arguments
2140    ///
2141    /// * `gql` - The GQL query string to explain
2142    ///
2143    /// # Returns
2144    ///
2145    /// A [`QueryPlan`] containing the execution plan details.
2146    ///
2147    /// # Errors
2148    ///
2149    /// Returns an error if the query is invalid or cannot be planned.
2150    ///
2151    /// # Example
2152    ///
2153    /// ```no_run
2154    /// # use geode_client::Client;
2155    /// # async fn example() -> geode_client::Result<()> {
2156    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2157    /// # let mut conn = client.connect().await?;
2158    /// let plan = conn.explain("MATCH (p:Person)-[:KNOWS]->(f) RETURN f").await?;
2159    /// println!("Estimated rows: {}", plan.estimated_rows);
2160    /// for op in &plan.operations {
2161    ///     println!("  {} - {}", op.op_type, op.description);
2162    /// }
2163    /// # Ok(())
2164    /// # }
2165    /// ```
2166    pub async fn explain(&mut self, gql: &str) -> Result<QueryPlan> {
2167        // Execute EXPLAIN as a query via protobuf
2168        let explain_query = format!("EXPLAIN {}", gql);
2169        let (_page, _) = self.query(&explain_query).await?;
2170
2171        // Parse the plan from the response
2172        // The result format depends on server implementation
2173        Ok(QueryPlan {
2174            operations: Vec::new(),
2175            estimated_rows: 0,
2176            raw: serde_json::json!({}),
2177        })
2178    }
2179
2180    /// Execute a query and return the execution profile with timing information.
2181    ///
2182    /// This runs the query and collects detailed execution statistics including
2183    /// actual row counts and timing for each operation.
2184    ///
2185    /// # Arguments
2186    ///
2187    /// * `gql` - The GQL query string to profile
2188    ///
2189    /// # Returns
2190    ///
2191    /// A [`QueryProfile`] containing the execution plan and runtime statistics.
2192    ///
2193    /// # Errors
2194    ///
2195    /// Returns an error if the query fails to execute.
2196    ///
2197    /// # Example
2198    ///
2199    /// ```no_run
2200    /// # use geode_client::Client;
2201    /// # async fn example() -> geode_client::Result<()> {
2202    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2203    /// # let mut conn = client.connect().await?;
2204    /// let profile = conn.profile("MATCH (p:Person) RETURN p LIMIT 100").await?;
2205    /// println!("Execution time: {:.2}ms", profile.execution_time_ms);
2206    /// println!("Actual rows: {}", profile.actual_rows);
2207    /// # Ok(())
2208    /// # }
2209    /// ```
2210    pub async fn profile(&mut self, gql: &str) -> Result<QueryProfile> {
2211        // Execute PROFILE as a query via protobuf
2212        let profile_query = format!("PROFILE {}", gql);
2213        let (page, _) = self.query(&profile_query).await?;
2214
2215        // Parse the profile from the response
2216        let plan = QueryPlan {
2217            operations: Vec::new(),
2218            estimated_rows: 0,
2219            raw: serde_json::json!({}),
2220        };
2221
2222        Ok(QueryProfile {
2223            plan,
2224            actual_rows: page.rows.len() as u64,
2225            execution_time_ms: 0.0,
2226            raw: serde_json::json!({}),
2227        })
2228    }
2229
2230    /// Execute multiple queries in a batch.
2231    ///
2232    /// This is more efficient than executing queries one at a time when you
2233    /// have multiple independent queries to run.
2234    ///
2235    /// # Arguments
2236    ///
2237    /// * `queries` - A slice of (query, optional params) tuples
2238    ///
2239    /// # Returns
2240    ///
2241    /// A `Vec<Page>` with results for each query, in the same order as input.
2242    ///
2243    /// # Errors
2244    ///
2245    /// Returns an error if any query fails. Queries are executed in order,
2246    /// so earlier queries may have completed before the error.
2247    ///
2248    /// # Example
2249    ///
2250    /// ```no_run
2251    /// # use geode_client::Client;
2252    /// # async fn example() -> geode_client::Result<()> {
2253    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2254    /// # let mut conn = client.connect().await?;
2255    /// let results = conn.batch(&[
2256    ///     ("MATCH (n:Person) RETURN count(n)", None),
2257    ///     ("MATCH (n:Company) RETURN count(n)", None),
2258    ///     ("MATCH ()-[r:WORKS_AT]->() RETURN count(r)", None),
2259    /// ]).await?;
2260    ///
2261    /// for (i, page) in results.iter().enumerate() {
2262    ///     println!("Query {}: {} rows", i + 1, page.rows.len());
2263    /// }
2264    /// # Ok(())
2265    /// # }
2266    /// ```
2267    pub async fn batch(
2268        &mut self,
2269        queries: &[(&str, Option<&HashMap<String, Value>>)],
2270    ) -> Result<Vec<Page>> {
2271        let mut results = Vec::with_capacity(queries.len());
2272
2273        for (query, params) in queries {
2274            let (page, _) = match params {
2275                Some(p) => self.query_with_params(query, p).await?,
2276                None => self.query(query).await?,
2277            };
2278            results.push(page);
2279        }
2280
2281        Ok(results)
2282    }
2283
2284    /// Parse plan operations from a server response.
2285    /// Reserved for future EXPLAIN/PROFILE response parsing.
2286    #[allow(dead_code)]
2287    fn parse_plan_operations(result: &serde_json::Value) -> Vec<PlanOperation> {
2288        let mut operations = Vec::new();
2289
2290        if let Some(ops) = result.get("operations").and_then(|o| o.as_array()) {
2291            for op in ops {
2292                operations.push(Self::parse_single_operation(op));
2293            }
2294        } else if let Some(plan) = result.get("plan") {
2295            // Alternative format: single "plan" object
2296            operations.push(Self::parse_single_operation(plan));
2297        }
2298
2299        operations
2300    }
2301
2302    /// Parse a single operation from JSON.
2303    #[allow(dead_code)]
2304    fn parse_single_operation(op: &serde_json::Value) -> PlanOperation {
2305        let op_type = op
2306            .get("type")
2307            .or_else(|| op.get("op_type"))
2308            .and_then(|t| t.as_str())
2309            .unwrap_or("Unknown")
2310            .to_string();
2311
2312        let description = op
2313            .get("description")
2314            .or_else(|| op.get("desc"))
2315            .and_then(|d| d.as_str())
2316            .unwrap_or("")
2317            .to_string();
2318
2319        let estimated_rows = op
2320            .get("estimated_rows")
2321            .or_else(|| op.get("rows"))
2322            .and_then(|r| r.as_u64());
2323
2324        let children = op
2325            .get("children")
2326            .and_then(|c| c.as_array())
2327            .map(|arr| arr.iter().map(Self::parse_single_operation).collect())
2328            .unwrap_or_default();
2329
2330        PlanOperation {
2331            op_type,
2332            description,
2333            estimated_rows,
2334            children,
2335        }
2336    }
2337
2338    /// Close the connection.
2339    ///
2340    /// Gracefully closes the connection. After calling this method,
2341    /// the connection can no longer be used.
2342    ///
2343    /// # Note
2344    ///
2345    /// It's good practice to explicitly close connections, but they will also
2346    /// be closed when dropped.
2347    ///
2348    /// # Example
2349    ///
2350    /// ```no_run
2351    /// # use geode_client::Client;
2352    /// # async fn example() -> geode_client::Result<()> {
2353    /// # let client = Client::new("localhost", 3141).skip_verify(true);
2354    /// let mut conn = client.connect().await?;
2355    /// // ... use connection ...
2356    /// conn.close()?;
2357    /// # Ok(())
2358    /// # }
2359    /// ```
2360    pub fn close(&mut self) -> Result<()> {
2361        match &mut self.kind {
2362            ConnectionKind::Quic { conn, .. } => {
2363                // QUIC close is asynchronous and best-effort - the CONNECTION_CLOSE frame
2364                // will be sent by Quinn's internal I/O handling. No blocking delay needed.
2365                // (Gap #17: Removed std::thread::sleep that blocked the async runtime)
2366                conn.close(0u32.into(), b"client closing");
2367                Ok(())
2368            }
2369            #[cfg(feature = "grpc")]
2370            ConnectionKind::Grpc { client } => client.close(),
2371        }
2372    }
2373
2374    /// Check if the connection is still healthy.
2375    ///
2376    /// Returns `true` if the underlying QUIC connection is still open and usable.
2377    /// This is used by connection pools to verify connections before reuse.
2378    ///
2379    /// # Example
2380    ///
2381    /// ```ignore
2382    /// let mut conn = client.connect().await?;
2383    /// if conn.is_healthy() {
2384    ///     // Connection is still usable
2385    ///     let (page, _) = conn.query("RETURN 1").await?;
2386    /// }
2387    /// ```
2388    /// Returns whether this connection is currently inside a transaction.
2389    ///
2390    /// This is set to `true` after a successful [`begin`](Connection::begin) and
2391    /// reset to `false` after a successful [`commit`](Connection::commit) or
2392    /// [`rollback`](Connection::rollback).
2393    pub fn in_transaction(&self) -> bool {
2394        self.in_transaction
2395    }
2396
2397    pub fn is_healthy(&self) -> bool {
2398        match &self.kind {
2399            ConnectionKind::Quic { conn, .. } => {
2400                // Quinn's close_reason() returns Some if the connection was closed
2401                conn.close_reason().is_none()
2402            }
2403            #[cfg(feature = "grpc")]
2404            ConnectionKind::Grpc { .. } => {
2405                // gRPC connections are managed by tonic, always report healthy
2406                true
2407            }
2408        }
2409    }
2410}
2411
2412/// Certificate verifier that skips all verification (INSECURE - for development only)
2413#[derive(Debug)]
2414struct SkipServerVerification;
2415
2416impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
2417    fn verify_server_cert(
2418        &self,
2419        _end_entity: &CertificateDer,
2420        _intermediates: &[CertificateDer],
2421        _server_name: &RustlsServerName,
2422        _ocsp_response: &[u8],
2423        _now: rustls::pki_types::UnixTime,
2424    ) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
2425        Ok(rustls::client::danger::ServerCertVerified::assertion())
2426    }
2427
2428    fn verify_tls12_signature(
2429        &self,
2430        _message: &[u8],
2431        _cert: &CertificateDer,
2432        _dss: &rustls::DigitallySignedStruct,
2433    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
2434        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
2435    }
2436
2437    fn verify_tls13_signature(
2438        &self,
2439        _message: &[u8],
2440        _cert: &CertificateDer,
2441        _dss: &rustls::DigitallySignedStruct,
2442    ) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
2443        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
2444    }
2445
2446    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
2447        vec![
2448            rustls::SignatureScheme::RSA_PKCS1_SHA256,
2449            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
2450            rustls::SignatureScheme::ED25519,
2451        ]
2452    }
2453}
2454
2455#[cfg(test)]
2456mod tests {
2457    use super::*;
2458
2459    // PreparedStatement tests
2460
2461    #[test]
2462    fn test_prepared_statement_new() {
2463        let stmt = PreparedStatement::new("MATCH (n:Person {id: $id}) RETURN n");
2464        assert_eq!(stmt.query(), "MATCH (n:Person {id: $id}) RETURN n");
2465        assert_eq!(stmt.param_names(), &["id"]);
2466    }
2467
2468    #[test]
2469    fn test_prepared_statement_multiple_params() {
2470        let stmt = PreparedStatement::new(
2471            "MATCH (p:Person {name: $name}) WHERE p.age > $min_age AND p.city = $city RETURN p",
2472        );
2473        assert!(stmt.query().contains("$name"));
2474        let names = stmt.param_names();
2475        assert_eq!(names.len(), 3);
2476        assert!(names.contains(&"name".to_string()));
2477        assert!(names.contains(&"min_age".to_string()));
2478        assert!(names.contains(&"city".to_string()));
2479    }
2480
2481    #[test]
2482    fn test_prepared_statement_no_params() {
2483        let stmt = PreparedStatement::new("MATCH (n) RETURN n LIMIT 10");
2484        assert!(stmt.param_names().is_empty());
2485    }
2486
2487    #[test]
2488    fn test_prepared_statement_duplicate_params() {
2489        let stmt =
2490            PreparedStatement::new("MATCH (a {id: $id})-[:KNOWS]->(b {id: $id}) RETURN a, b");
2491        // Should deduplicate parameter names
2492        assert_eq!(stmt.param_names(), &["id"]);
2493    }
2494
2495    #[test]
2496    fn test_prepared_statement_underscore_params() {
2497        let stmt = PreparedStatement::new("MATCH (n {user_id: $user_id}) RETURN n");
2498        assert_eq!(stmt.param_names(), &["user_id"]);
2499    }
2500
2501    #[test]
2502    fn test_prepared_statement_numeric_params() {
2503        let stmt = PreparedStatement::new("RETURN $param1, $param2, $param123");
2504        let names = stmt.param_names();
2505        assert_eq!(names.len(), 3);
2506        assert!(names.contains(&"param1".to_string()));
2507        assert!(names.contains(&"param2".to_string()));
2508        assert!(names.contains(&"param123".to_string()));
2509    }
2510
2511    // PlanOperation tests
2512
2513    #[test]
2514    fn test_plan_operation_struct() {
2515        let op = PlanOperation {
2516            op_type: "NodeScan".to_string(),
2517            description: "Scan Person nodes".to_string(),
2518            estimated_rows: Some(100),
2519            children: vec![],
2520        };
2521        assert_eq!(op.op_type, "NodeScan");
2522        assert_eq!(op.description, "Scan Person nodes");
2523        assert_eq!(op.estimated_rows, Some(100));
2524        assert!(op.children.is_empty());
2525    }
2526
2527    #[test]
2528    fn test_plan_operation_with_children() {
2529        let child = PlanOperation {
2530            op_type: "Filter".to_string(),
2531            description: "Filter by age".to_string(),
2532            estimated_rows: Some(50),
2533            children: vec![],
2534        };
2535        let parent = PlanOperation {
2536            op_type: "Projection".to_string(),
2537            description: "Project name, age".to_string(),
2538            estimated_rows: Some(50),
2539            children: vec![child],
2540        };
2541        assert_eq!(parent.children.len(), 1);
2542        assert_eq!(parent.children[0].op_type, "Filter");
2543    }
2544
2545    // QueryPlan tests
2546
2547    #[test]
2548    fn test_query_plan_struct() {
2549        let plan = QueryPlan {
2550            operations: vec![PlanOperation {
2551                op_type: "NodeScan".to_string(),
2552                description: "Full scan".to_string(),
2553                estimated_rows: Some(1000),
2554                children: vec![],
2555            }],
2556            estimated_rows: 1000,
2557            raw: serde_json::json!({"type": "plan"}),
2558        };
2559        assert_eq!(plan.operations.len(), 1);
2560        assert_eq!(plan.estimated_rows, 1000);
2561    }
2562
2563    // QueryProfile tests
2564
2565    #[test]
2566    fn test_query_profile_struct() {
2567        let plan = QueryPlan {
2568            operations: vec![],
2569            estimated_rows: 100,
2570            raw: serde_json::json!({}),
2571        };
2572        let profile = QueryProfile {
2573            plan,
2574            actual_rows: 95,
2575            execution_time_ms: 12.5,
2576            raw: serde_json::json!({"type": "profile"}),
2577        };
2578        assert_eq!(profile.actual_rows, 95);
2579        assert!((profile.execution_time_ms - 12.5).abs() < 0.001);
2580    }
2581
2582    // Page tests
2583
2584    #[test]
2585    fn test_page_struct() {
2586        let page = Page {
2587            columns: vec![Column {
2588                name: "x".to_string(),
2589                col_type: "INT".to_string(),
2590            }],
2591            rows: vec![],
2592            ordered: false,
2593            order_keys: vec![],
2594            final_page: true,
2595        };
2596        assert_eq!(page.columns.len(), 1);
2597        assert!(page.rows.is_empty());
2598        assert!(page.final_page);
2599    }
2600
2601    // Column tests
2602
2603    #[test]
2604    fn test_column_struct() {
2605        let col = Column {
2606            name: "age".to_string(),
2607            col_type: "INT".to_string(),
2608        };
2609        assert_eq!(col.name, "age");
2610        assert_eq!(col.col_type, "INT");
2611    }
2612
2613    // Savepoint tests
2614
2615    #[test]
2616    fn test_savepoint_struct() {
2617        let sp = Savepoint {
2618            name: "before_update".to_string(),
2619        };
2620        assert_eq!(sp.name, "before_update");
2621    }
2622
2623    // Client builder tests
2624
2625    #[test]
2626    fn test_client_builder_defaults() {
2627        let _client = Client::new("localhost", 3141);
2628        // Test passes if it compiles - verifies defaults work
2629    }
2630
2631    #[test]
2632    fn test_client_builder_chain() {
2633        let _client = Client::new("example.com", 8443)
2634            .skip_verify(true)
2635            .page_size(500)
2636            .client_name("test-app")
2637            .client_version("2.0.0")
2638            .conformance("full");
2639        // Test passes if it compiles - verifies builder chain works
2640    }
2641
2642    #[test]
2643    fn test_client_clone() {
2644        let client = Client::new("localhost", 3141).skip_verify(true);
2645        let _cloned = client.clone();
2646        // Test passes if it compiles - verifies Clone is implemented
2647    }
2648
2649    // parse_plan_operations tests
2650
2651    #[test]
2652    fn test_parse_plan_operations_empty() {
2653        let result = serde_json::json!({});
2654        let ops = Connection::parse_plan_operations(&result);
2655        assert!(ops.is_empty());
2656    }
2657
2658    #[test]
2659    fn test_parse_plan_operations_array() {
2660        let result = serde_json::json!({
2661            "operations": [
2662                {"type": "NodeScan", "description": "Scan nodes", "estimated_rows": 100},
2663                {"type": "Filter", "description": "Apply filter", "estimated_rows": 50}
2664            ]
2665        });
2666        let ops = Connection::parse_plan_operations(&result);
2667        assert_eq!(ops.len(), 2);
2668        assert_eq!(ops[0].op_type, "NodeScan");
2669        assert_eq!(ops[1].op_type, "Filter");
2670    }
2671
2672    #[test]
2673    fn test_parse_plan_operations_single_plan() {
2674        let result = serde_json::json!({
2675            "plan": {"op_type": "FullScan", "desc": "Full table scan"}
2676        });
2677        let ops = Connection::parse_plan_operations(&result);
2678        assert_eq!(ops.len(), 1);
2679        assert_eq!(ops[0].op_type, "FullScan");
2680        assert_eq!(ops[0].description, "Full table scan");
2681    }
2682
2683    #[test]
2684    fn test_parse_single_operation() {
2685        let op_json = serde_json::json!({
2686            "type": "IndexScan",
2687            "description": "Use index on Person(name)",
2688            "estimated_rows": 25,
2689            "children": [
2690                {"type": "Filter", "description": "Filter results"}
2691            ]
2692        });
2693        let op = Connection::parse_single_operation(&op_json);
2694        assert_eq!(op.op_type, "IndexScan");
2695        assert_eq!(op.description, "Use index on Person(name)");
2696        assert_eq!(op.estimated_rows, Some(25));
2697        assert_eq!(op.children.len(), 1);
2698        assert_eq!(op.children[0].op_type, "Filter");
2699    }
2700
2701    #[test]
2702    fn test_parse_single_operation_minimal() {
2703        let op_json = serde_json::json!({});
2704        let op = Connection::parse_single_operation(&op_json);
2705        assert_eq!(op.op_type, "Unknown");
2706        assert_eq!(op.description, "");
2707        assert_eq!(op.estimated_rows, None);
2708        assert!(op.children.is_empty());
2709    }
2710
2711    #[test]
2712    fn test_parse_single_operation_alt_fields() {
2713        let op_json = serde_json::json!({
2714            "op_type": "Sort",
2715            "desc": "Sort by name ASC",
2716            "rows": 100
2717        });
2718        let op = Connection::parse_single_operation(&op_json);
2719        assert_eq!(op.op_type, "Sort");
2720        assert_eq!(op.description, "Sort by name ASC");
2721        assert_eq!(op.estimated_rows, Some(100));
2722    }
2723
2724    // redact_dsn tests - Gap #7 (DSN Password Exposure)
2725
2726    #[test]
2727    fn test_redact_dsn_url_with_password() {
2728        let dsn = "quic://admin:secret123@localhost:3141";
2729        let redacted = redact_dsn(dsn);
2730        assert!(redacted.contains("[REDACTED]"));
2731        assert!(!redacted.contains("secret123"));
2732        assert!(redacted.contains("admin"));
2733        assert!(redacted.contains("localhost"));
2734    }
2735
2736    #[test]
2737    fn test_redact_dsn_url_without_password() {
2738        let dsn = "quic://admin@localhost:3141";
2739        let redacted = redact_dsn(dsn);
2740        assert!(!redacted.contains("[REDACTED]"));
2741        assert!(redacted.contains("admin"));
2742        assert!(redacted.contains("localhost"));
2743    }
2744
2745    #[test]
2746    fn test_redact_dsn_url_no_auth() {
2747        let dsn = "quic://localhost:3141";
2748        let redacted = redact_dsn(dsn);
2749        assert_eq!(redacted, dsn);
2750    }
2751
2752    #[test]
2753    fn test_redact_dsn_query_param_password() {
2754        let dsn = "localhost:3141?username=admin&password=secret123";
2755        let redacted = redact_dsn(dsn);
2756        assert!(redacted.contains("[REDACTED]"));
2757        assert!(!redacted.contains("secret123"));
2758        assert!(redacted.contains("username=admin"));
2759    }
2760
2761    #[test]
2762    fn test_redact_dsn_query_param_pass() {
2763        let dsn = "localhost:3141?user=admin&pass=mysecret";
2764        let redacted = redact_dsn(dsn);
2765        assert!(redacted.contains("[REDACTED]"));
2766        assert!(!redacted.contains("mysecret"));
2767    }
2768
2769    #[test]
2770    fn test_redact_dsn_simple_no_password() {
2771        let dsn = "localhost:3141?insecure=true";
2772        let redacted = redact_dsn(dsn);
2773        assert_eq!(redacted, dsn);
2774    }
2775
2776    #[test]
2777    fn test_redact_dsn_url_with_query_and_password() {
2778        let dsn = "quic://user:pass@localhost:3141?insecure=true";
2779        let redacted = redact_dsn(dsn);
2780        assert!(redacted.contains("[REDACTED]"));
2781        assert!(!redacted.contains(":pass@"));
2782        assert!(redacted.contains("insecure=true"));
2783    }
2784
2785    // validate() tests - Gap #9 (Validation Not Automatic)
2786
2787    #[test]
2788    fn test_client_validate_valid() {
2789        let client = Client::new("localhost", 3141);
2790        assert!(client.validate().is_ok());
2791    }
2792
2793    #[test]
2794    fn test_client_validate_valid_hostname() {
2795        let client = Client::new("geode.example.com", 3141);
2796        assert!(client.validate().is_ok());
2797    }
2798
2799    #[test]
2800    fn test_client_validate_valid_ipv4() {
2801        let client = Client::new("192.168.1.1", 8443);
2802        assert!(client.validate().is_ok());
2803    }
2804
2805    #[test]
2806    fn test_client_validate_invalid_hostname_hyphen_start() {
2807        let client = Client::new("-invalid", 3141);
2808        assert!(client.validate().is_err());
2809    }
2810
2811    #[test]
2812    fn test_client_validate_invalid_hostname_hyphen_end() {
2813        let client = Client::new("invalid-", 3141);
2814        assert!(client.validate().is_err());
2815    }
2816
2817    #[test]
2818    fn test_client_validate_invalid_port_zero() {
2819        let client = Client::new("localhost", 0);
2820        assert!(client.validate().is_err());
2821    }
2822
2823    #[test]
2824    fn test_client_validate_invalid_page_size_zero() {
2825        let client = Client::new("localhost", 3141).page_size(0);
2826        assert!(client.validate().is_err());
2827    }
2828
2829    #[test]
2830    fn test_client_validate_invalid_page_size_too_large() {
2831        let client = Client::new("localhost", 3141).page_size(200_000);
2832        assert!(client.validate().is_err());
2833    }
2834
2835    #[test]
2836    fn test_client_validate_with_all_options() {
2837        let client = Client::new("geode.example.com", 8443)
2838            .skip_verify(true)
2839            .page_size(500)
2840            .username("admin")
2841            .password("secret")
2842            .connect_timeout(15)
2843            .hello_timeout(10)
2844            .idle_timeout(60);
2845        assert!(client.validate().is_ok());
2846    }
2847
2848    // Gap #13: Test that extreme timeout values don't cause builder panics
2849    #[test]
2850    fn test_client_extreme_timeout_values() {
2851        // These should not panic - the actual validation/capping happens at connect time
2852        let _client = Client::new("localhost", 3141)
2853            .connect_timeout(u64::MAX)
2854            .hello_timeout(u64::MAX)
2855            .idle_timeout(u64::MAX);
2856        // Builder should accept any u64 value without panicking
2857    }
2858
2859    #[test]
2860    fn test_convert_edge_uses_type_field() {
2861        let edge = proto::EdgeValue {
2862            id: 100,
2863            from_id: 1,
2864            to_id: 2,
2865            label: "KNOWS".to_string(),
2866            properties: vec![],
2867        };
2868        let proto_val = proto::Value {
2869            kind: Some(proto::value::Kind::EdgeVal(edge)),
2870        };
2871        let val = crate::convert::proto_to_value(&proto_val);
2872        let obj = val.as_object().unwrap();
2873        assert_eq!(obj.get("type").unwrap().as_string().unwrap(), "KNOWS");
2874        assert!(
2875            obj.get("label").is_none(),
2876            "edge should not have 'label' field"
2877        );
2878    }
2879
2880    #[test]
2881    fn test_convert_edge_uses_start_end_node() {
2882        let edge = proto::EdgeValue {
2883            id: 100,
2884            from_id: 42,
2885            to_id: 99,
2886            label: "LIKES".to_string(),
2887            properties: vec![],
2888        };
2889        let proto_val = proto::Value {
2890            kind: Some(proto::value::Kind::EdgeVal(edge)),
2891        };
2892        let val = crate::convert::proto_to_value(&proto_val);
2893        let obj = val.as_object().unwrap();
2894        assert_eq!(obj.get("start_node").unwrap().as_int().unwrap(), 42);
2895        assert_eq!(obj.get("end_node").unwrap().as_int().unwrap(), 99);
2896        assert!(obj.get("from_id").is_none());
2897        assert!(obj.get("to_id").is_none());
2898    }
2899
2900    #[test]
2901    fn test_convert_edge_with_properties() {
2902        let edge = proto::EdgeValue {
2903            id: 100,
2904            from_id: 1,
2905            to_id: 2,
2906            label: "KNOWS".to_string(),
2907            properties: vec![proto::MapEntry {
2908                key: "since".to_string(),
2909                value: Some(proto::Value {
2910                    kind: Some(proto::value::Kind::IntVal(proto::IntValue {
2911                        value: 2020,
2912                        kind: 1,
2913                    })),
2914                }),
2915            }],
2916        };
2917        let proto_val = proto::Value {
2918            kind: Some(proto::value::Kind::EdgeVal(edge)),
2919        };
2920        let val = crate::convert::proto_to_value(&proto_val);
2921        let obj = val.as_object().unwrap();
2922        let props = obj.get("properties").unwrap().as_object().unwrap();
2923        assert_eq!(props.get("since").unwrap().as_int().unwrap(), 2020);
2924    }
2925
2926    #[test]
2927    fn test_convert_node_fields() {
2928        let node = proto::NodeValue {
2929            id: 42,
2930            labels: vec!["Person".to_string()],
2931            properties: vec![proto::MapEntry {
2932                key: "name".to_string(),
2933                value: Some(proto::Value {
2934                    kind: Some(proto::value::Kind::StringVal(proto::StringValue {
2935                        value: "Alice".to_string(),
2936                        kind: 1,
2937                    })),
2938                }),
2939            }],
2940        };
2941        let proto_val = proto::Value {
2942            kind: Some(proto::value::Kind::NodeVal(node)),
2943        };
2944        let val = crate::convert::proto_to_value(&proto_val);
2945        let obj = val.as_object().unwrap();
2946        assert_eq!(obj.get("id").unwrap().as_int().unwrap(), 42);
2947        let labels = obj.get("labels").unwrap().as_array().unwrap();
2948        assert_eq!(labels.len(), 1);
2949        let props = obj.get("properties").unwrap().as_object().unwrap();
2950        assert_eq!(props.get("name").unwrap().as_string().unwrap(), "Alice");
2951    }
2952
2953    // Row parsing tests
2954
2955    fn null_proto_value() -> proto::Value {
2956        proto::Value {
2957            kind: Some(proto::value::Kind::NullVal(proto::NullValue {})),
2958        }
2959    }
2960
2961    fn node_proto_value(id: u64) -> proto::Value {
2962        proto::Value {
2963            kind: Some(proto::value::Kind::NodeVal(proto::NodeValue {
2964                id,
2965                labels: vec!["Person".to_string()],
2966                properties: vec![],
2967            })),
2968        }
2969    }
2970
2971    #[test]
2972    fn test_real_node_row_kept() {
2973        let columns = vec![Column {
2974            name: "n".to_string(),
2975            col_type: "NODE".to_string(),
2976        }];
2977        let proto_rows = vec![proto::Row {
2978            values: vec![node_proto_value(42)],
2979        }];
2980        let rows = Connection::parse_proto_rows_static(&proto_rows, &columns).unwrap();
2981        assert_eq!(rows.len(), 1, "real NODE row must be kept");
2982        let obj = rows[0].get("n").unwrap().as_object().unwrap();
2983        assert_eq!(obj.get("id").unwrap().as_int().unwrap(), 42);
2984    }
2985
2986    #[test]
2987    fn test_scalar_null_row_kept() {
2988        // A null in a scalar column (e.g. RETURN null) is NOT a no-match
2989        // sentinel and must be preserved.
2990        let columns = vec![Column {
2991            name: "x".to_string(),
2992            col_type: "STRING".to_string(),
2993        }];
2994        let proto_rows = vec![proto::Row {
2995            values: vec![null_proto_value()],
2996        }];
2997        let rows = Connection::parse_proto_rows_static(&proto_rows, &columns).unwrap();
2998        assert_eq!(rows.len(), 1, "scalar null row must be kept");
2999        assert!(rows[0].get("x").unwrap().is_null());
3000    }
3001}