Skip to main content

wasi_pg_client/
connection.rs

1//! PostgreSQL connection management.
2//!
3//! This module defines the `Connection` struct which represents a connection to a PostgreSQL server.
4//! It handles the connection lifecycle, authentication, protocol state, and graceful close.
5
6use std::collections::VecDeque;
7
8use crate::protocol::{BackendMessage, FrontendMessage, TransactionStatus};
9
10use crate::auth::{self, ServerParams};
11use crate::config::Config;
12use crate::ensure_random_available;
13use crate::error::{Error, PgError, Result};
14use crate::notification::Notification;
15use crate::query::{Notice, NoticeHandler};
16use crate::transport::{
17    AsyncTransport, BufferedTransport, ClientTransport, PgTransport, SslMode, TlsConfig,
18};
19
20#[cfg(feature = "tracing")]
21use crate::tracing_ext::{TARGET_CONNECTION, TARGET_NOTIFICATION, TARGET_RECONNECT};
22
23// ---------------------------------------------------------------------------
24// Connection state machine
25// ---------------------------------------------------------------------------
26
27/// Internal state of a PostgreSQL connection.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum ConnectionState {
31    /// Initial state before any network activity.
32    Disconnected,
33    /// TCP/TLS handshake in progress.
34    Connecting,
35    /// Startup message sent, waiting for Authentication* or ReadyForQuery.
36    StartingUp,
37    /// Authentication challenge/response exchange in progress.
38    Authenticating,
39    /// ReadyForQuery received; connection is idle and can accept commands.
40    Idle,
41    /// A simple query is in flight.
42    ActiveSimpleQuery,
43    /// An extended query is in flight.
44    ActiveExtendedQuery,
45    /// A COPY IN operation is in progress.
46    CopyIn,
47    /// A COPY OUT operation is in progress.
48    CopyOut,
49    /// A row stream is active and borrowing the connection.
50    Streaming,
51    /// Connection is being closed gracefully.
52    Closing,
53    /// Connection is closed or unusable due to error.
54    Closed,
55}
56
57impl ConnectionState {
58    /// Returns `true` if the connection is in a state where queries may be sent.
59    pub fn is_idle(self) -> bool {
60        matches!(self, ConnectionState::Idle)
61    }
62
63    /// Returns `true` if the connection is fully closed.
64    pub fn is_closed(self) -> bool {
65        matches!(self, ConnectionState::Closed)
66    }
67}
68
69// ---------------------------------------------------------------------------
70// Connection
71// ---------------------------------------------------------------------------
72
73/// A connection to a PostgreSQL server.
74///
75/// The connection is established when `Connection::connect` is called and is closed when the
76/// connection is dropped. The connection can be used to execute queries and manage transactions.
77pub struct Connection {
78    pub(crate) transport: PgTransport<ClientTransport>,
79    pub(crate) codec: auth::Codec,
80    pub(crate) server_params: ServerParams,
81    pub(crate) state: ConnectionState,
82    pub(crate) config: Config,
83    pub(crate) transaction_status: TransactionStatus,
84    pub(crate) notification_queue: VecDeque<Notification>,
85    pub(crate) notice_handler: Option<NoticeHandler>,
86    pub(crate) statement_counter: u32,
87    /// Whether the connection needs recovery (e.g., a RowStream was dropped
88    /// before being fully consumed).
89    pub(crate) needs_recovery: bool,
90    /// Health and reconnection state.
91    pub(crate) health: crate::reconnect::session::ConnectionHealth,
92    /// Session state tracking for reconnection.
93    pub(crate) session_state: crate::reconnect::session::SessionState,
94}
95
96impl Connection {
97    // =======================================================================
98    // Connection establishment
99    // =======================================================================
100
101    /// Establishes a new connection to the PostgreSQL server using the given configuration.
102    ///
103    /// This method performs the following steps:
104    /// 1. Resolves the host and port to a TCP address.
105    /// 2. Establishes a TCP connection (with optional TLS).
106    /// 3. Performs the PostgreSQL startup handshake.
107    /// 4. Authenticates with the server.
108    /// 5. Collects server parameters until `ReadyForQuery`.
109    #[must_use = "connection errors should be checked"]
110    pub async fn connect(config: &Config) -> Result<Self> {
111        #[cfg(feature = "tracing")]
112        let span = tracing::info_span!(
113            target: TARGET_CONNECTION,
114            "connect",
115            host = %config.get_host(),
116            port = config.get_port(),
117            database = ?config.get_database(),
118            user = %config.get_user(),
119        );
120        #[cfg(feature = "tracing")]
121        let _enter = span.enter();
122
123        ensure_random_available();
124
125        let mut transport = build_pg_transport(config).await?;
126        let mut codec = auth::Codec::new();
127
128        // Send StartupMessage
129        #[cfg(feature = "tracing")]
130        tracing::debug!(target: TARGET_CONNECTION, "Sending startup message");
131        let startup = crate::protocol::FrontendMessage::Startup {
132            params: config.startup_params(),
133        };
134        codec
135            .send(&mut transport, &startup)
136            .await
137            .map_err(Error::from)?;
138
139        // Authenticate
140        let server_params = auth::authenticate(&mut transport, &mut codec, config)
141            .await
142            .map_err(Error::from)?;
143
144        #[cfg(feature = "tracing")]
145        tracing::info!(target: TARGET_CONNECTION, server_version = %server_params.server_version, process_id = server_params.process_id, "Connection established successfully");
146
147        Ok(Self {
148            transport,
149            codec,
150            server_params,
151            state: ConnectionState::Idle,
152            config: config.clone(),
153            transaction_status: TransactionStatus::Idle,
154            notification_queue: VecDeque::new(),
155            notice_handler: None,
156            statement_counter: 0,
157            needs_recovery: false,
158            health: crate::reconnect::session::ConnectionHealth::new(),
159            session_state: crate::reconnect::session::SessionState::new(),
160        })
161    }
162
163    /// Connect with automatic retry using the given retry policy.
164    ///
165    /// This is useful for connection establishment that may fail transiently
166    /// (e.g. the server is temporarily unavailable). The retry policy
167    /// controls the number of attempts and the delay between them.
168    #[must_use = "connection errors should be checked"]
169    pub async fn connect_with_retry(
170        config: &Config,
171        retry_policy: &crate::reconnect::RetryPolicy,
172    ) -> Result<Self> {
173        retry_policy.retry(|| Self::connect(config)).await
174    }
175
176    /// Convenience: connect from a connection string (URI or key-value format).
177    #[must_use = "connection errors should be checked"]
178    pub async fn connect_str(s: &str) -> Result<Self> {
179        let config = match Config::from_uri(s) {
180            Ok(c) => c,
181            Err(uri_err) => match Config::from_key_value(s) {
182                Ok(c) => c,
183                Err(kv_err) => {
184                    return Err(PgError::Config(format!(
185                        "could not parse connection string as URI ({uri_err}) or key=value format ({kv_err})"
186                    )));
187                }
188            },
189        };
190        Self::connect(&config).await
191    }
192
193    // =======================================================================
194    // Accessors
195    // =======================================================================
196
197    /// Returns a reference to the configuration used for this connection.
198    pub fn config(&self) -> &Config {
199        &self.config
200    }
201
202    fn escape_sql_literal(value: &str) -> String {
203        value.replace('\\', "\\\\").replace('\'', "''")
204    }
205
206    pub(crate) fn build_set_param_sql(key: &str, value: &str) -> String {
207        format!(
208            "SET {} = '{}'",
209            crate::transaction::quote_identifier(key),
210            Self::escape_sql_literal(value)
211        )
212    }
213
214    pub(crate) fn build_listen_sql(channel: &str) -> String {
215        format!("LISTEN {}", crate::transaction::quote_identifier(channel))
216    }
217
218    pub(crate) fn build_unlisten_sql(channel: &str) -> String {
219        format!("UNLISTEN {}", crate::transaction::quote_identifier(channel))
220    }
221
222    /// Set a runtime parameter on the server.
223    ///
224    /// Sends `SET key = value` and tracks the change in [`SessionState`]
225    /// for automatic re-application on reconnection.
226    ///
227    /// # Example
228    /// ```ignore
229    /// conn.set_param("timezone", "UTC").await?;
230    /// ```
231    #[must_use = "set_param errors should be checked"]
232    pub async fn set_param(&mut self, key: &str, value: &str) -> Result<()> {
233        let sql = Self::build_set_param_sql(key, value);
234        self.execute(&sql).await?;
235        self.session_state.track_set_guc(key, value);
236        Ok(())
237    }
238
239    /// Register opaque initialization SQL to replay after a reconnect.
240    ///
241    /// This is intended for idempotent baseline session setup such as pool-level
242    /// `after_connect` hooks. The SQL is replayed before tracked session state
243    /// (for example, `set_param()` changes) is rebuilt, so later runtime
244    /// overrides still win.
245    pub fn set_reconnect_init_sql(&mut self, sql: impl Into<String>) {
246        self.session_state.set_reconnect_init_sql(sql);
247    }
248
249    /// Clear any previously registered reconnect initialization SQL.
250    pub fn clear_reconnect_init_sql(&mut self) {
251        self.session_state.clear_reconnect_init_sql();
252    }
253
254    /// Return the registered reconnect initialization SQL, if any.
255    pub fn reconnect_init_sql(&self) -> Option<&str> {
256        self.session_state.reconnect_init_sql()
257    }
258
259    /// Returns the current connection state.
260    pub fn state(&self) -> ConnectionState {
261        self.state
262    }
263
264    /// Returns whether the connection is closed or unusable.
265    pub fn is_closed(&self) -> bool {
266        self.state.is_closed()
267    }
268
269    /// Returns whether the connection is idle and ready to accept commands.
270    pub fn is_idle(&self) -> bool {
271        self.state.is_idle()
272    }
273
274    /// Returns the server parameters collected during connection startup.
275    pub fn server_params(&self) -> &ServerParams {
276        &self.server_params
277    }
278
279    /// Returns the server version string (e.g. "16.0").
280    pub fn server_version(&self) -> &str {
281        &self.server_params.server_version
282    }
283
284    /// Returns the backend process ID.
285    pub fn process_id(&self) -> i32 {
286        self.server_params.process_id
287    }
288
289    /// Returns the secret key (used for cancel requests).
290    pub fn secret_key(&self) -> i32 {
291        self.server_params.secret_key
292    }
293
294    /// Returns the current transaction status.
295    pub fn transaction_status(&self) -> TransactionStatus {
296        self.transaction_status
297    }
298
299    /// Takes any queued notifications, leaving the queue empty.
300    pub fn drain_notifications(&mut self) -> Vec<Notification> {
301        self.notification_queue.drain(..).collect()
302    }
303
304    /// Get a cancellation token for this connection.
305    ///
306    /// The token can be sent to another task or thread to cancel a
307    /// running query. It contains the host, port, process ID, and
308    /// secret key needed to send an out-of-band cancellation request.
309    ///
310    /// # Example
311    /// ```ignore
312    /// let token = conn.cancel_token();
313    ///
314    /// // In another task:
315    /// tokio::spawn(async move {
316    ///     tokio::time::sleep(Duration::from_secs(5)).await;
317    ///     token.cancel().await.unwrap();
318    /// });
319    /// ```
320    pub fn cancel_token(&self) -> crate::cancel::CancelToken {
321        crate::cancel::CancelToken {
322            host: self.config.host.clone(),
323            port: self.config.port,
324            process_id: self.server_params.process_id,
325            secret_key: self.server_params.secret_key,
326            ssl_mode: self.config.ssl_mode,
327            accept_invalid_certs: self.config.accept_invalid_certs,
328        }
329    }
330
331    /// Set a handler that will be called for every server notice.
332    ///
333    /// The previous handler (if any) is replaced.
334    pub fn set_notice_handler(&mut self, handler: NoticeHandler) {
335        self.notice_handler = Some(handler);
336    }
337
338    /// Remove the current notice handler.
339    pub fn clear_notice_handler(&mut self) {
340        self.notice_handler = None;
341    }
342
343    /// Returns true if the connection is using TLS.
344    pub fn is_tls(&self) -> bool {
345        #[cfg(feature = "tls")]
346        {
347            matches!(self.transport, crate::transport::PgTransport::Tls(_))
348        }
349        #[cfg(not(feature = "tls"))]
350        {
351            false
352        }
353    }
354
355    /// Get TLS info if the connection is encrypted.
356    ///
357    /// Returns `None` if the connection is not using TLS.
358    #[cfg(feature = "tls")]
359    pub fn tls_info(&self) -> Option<crate::transport::TlsInfo> {
360        self.transport.tls_info()
361    }
362
363    /// Check if the connection is still alive by sending a simple query.
364    #[must_use = "ping errors should be checked"]
365    pub async fn ping(&mut self) -> Result<()> {
366        self.query("SELECT 1").await?;
367        self.health.mark_alive();
368        Ok(())
369    }
370
371    /// Check connection state without sending a query.
372    /// Examines the transport and protocol state.
373    pub fn is_healthy(&self) -> bool {
374        !self.is_closed() && self.transaction_status != crate::protocol::TransactionStatus::Failed
375    }
376
377    /// Reset the connection state (clear failed transaction, discard temp objects).
378    #[must_use = "reset errors should be checked"]
379    pub async fn reset(&mut self) -> Result<()> {
380        if self.transaction_status == crate::protocol::TransactionStatus::Failed
381            || self.transaction_status == crate::protocol::TransactionStatus::InTransaction
382        {
383            self.execute("ROLLBACK").await?;
384        }
385        self.execute("DISCARD ALL").await?;
386        self.session_state.clear();
387        Ok(())
388    }
389
390    /// Whether the connection needs recovery (e.g., a `RowStream` was dropped
391    /// before being fully consumed).
392    ///
393    /// When this returns `true`, the connection may have unread protocol
394    /// messages in its buffer. Call [`Connection::recover`] to drain them
395    /// and restore the connection to a usable state.
396    pub fn needs_recovery(&self) -> bool {
397        self.needs_recovery
398    }
399
400    /// Recover the connection after an incomplete stream consumption.
401    ///
402    /// Reads messages until `ReadyForQuery` is received, discarding
403    /// everything. This is needed when a `RowStream` is dropped before
404    /// being fully consumed.
405    #[must_use = "recovery errors should be checked"]
406    pub async fn recover(&mut self) -> Result<()> {
407        if self.needs_recovery {
408            self.read_until_ready().await?;
409            self.needs_recovery = false;
410        }
411        Ok(())
412    }
413
414    // =======================================================================
415    // Reconnection & Resilience
416    // =======================================================================
417
418    /// Check if the connection is believed to be alive.
419    ///
420    /// This is a fast check based on internal state. It does not send a query.
421    /// For a definitive check, use `ping()`.
422    pub fn is_alive(&self) -> bool {
423        self.health.is_alive()
424    }
425
426    /// Check if the connection might be broken based on time since last use.
427    ///
428    /// Returns true if the connection hasn't been confirmed alive in longer
429    /// than the specified threshold. This is a heuristic — the connection
430    /// might still be alive, but it's worth checking before use.
431    pub fn is_stale(&self, threshold: std::time::Duration) -> bool {
432        match self.health.last_confirmed_alive() {
433            Some(last) => last.elapsed() > threshold,
434            None => true, // never confirmed alive
435        }
436    }
437
438    /// Get the number of times this connection has been reconnected.
439    pub fn reconnect_count(&self) -> u32 {
440        self.health.reconnect_count()
441    }
442
443    /// Get a reference to the session state.
444    pub fn session_state(&self) -> &crate::reconnect::session::SessionState {
445        &self.session_state
446    }
447
448    /// Attempt to reconnect this connection.
449    ///
450    /// This closes the current (broken) connection and establishes a new one
451    /// using the original configuration. If `rebuild_session` is enabled in
452    /// the reconnection config, registered reconnect initialization SQL is
453    /// replayed first and tracked session state (LISTEN channels, GUC
454    /// parameters) is rebuilt afterwards.
455    ///
456    /// # Safety
457    ///
458    /// This should only be called when the connection is known to be broken.
459    /// Calling this on a live connection will close it and create a new one,
460    /// which may cause server-side state to be lost.
461    #[must_use = "reconnection errors should be checked"]
462    pub async fn reconnect(&mut self) -> crate::error::Result<()> {
463        let session_state = self.session_state.clone();
464
465        #[cfg(feature = "tracing")]
466        tracing::info!(
467            target: TARGET_RECONNECT,
468            reconnect_count = self.health.reconnect_count(),
469            has_session_state = session_state.has_state(),
470            has_reconnect_init_sql = session_state.reconnect_init_sql().is_some(),
471            "Attempting to reconnect"
472        );
473
474        // 1. Close the old connection (best-effort — it's probably already broken)
475        #[cfg(feature = "tracing")]
476        tracing::debug!(target: TARGET_RECONNECT, "Shutting down old transport before reconnect");
477        self.health.mark_broken();
478        if let Err(e) = self.transport.shutdown().await {
479            #[cfg(feature = "tracing")]
480            tracing::debug!(target: TARGET_RECONNECT, error = %e, "Old transport shutdown error (expected during reconnect)");
481            let _ = &e;
482        }
483
484        // 2. Establish and fully initialize a replacement connection before we
485        //    swap it into self. That way, a reconnect-init failure does not
486        //    leave self holding a partially initialized replacement transport.
487        let mut new_conn = Self::connect(&self.config).await?;
488        new_conn.rebuild_reconnect_init_sql(&session_state).await?;
489        if self.config.get_reconnect().rebuild_session {
490            new_conn.rebuild_session(&session_state).await?;
491        }
492
493        // 3. Replace our internals with the new connection's using swap
494        //    (Connection implements Drop, so we can't move fields out)
495        std::mem::swap(&mut self.transport, &mut new_conn.transport);
496        std::mem::swap(&mut self.codec, &mut new_conn.codec);
497        std::mem::swap(&mut self.server_params, &mut new_conn.server_params);
498        self.transaction_status = new_conn.transaction_status;
499        self.notification_queue.clear();
500        self.state = new_conn.state;
501        self.health.reset_after_reconnect();
502        self.needs_recovery = false;
503
504        // new_conn will be dropped here, cleaning up the old transport
505
506        #[cfg(feature = "tracing")]
507        tracing::info!(
508            target: TARGET_RECONNECT,
509            reconnect_count = self.health.reconnect_count(),
510            "Reconnection successful"
511        );
512
513        Ok(())
514    }
515
516    async fn rebuild_reconnect_init_sql(
517        &mut self,
518        state: &crate::reconnect::session::SessionState,
519    ) -> crate::error::Result<()> {
520        if let Some(sql) = state.reconnect_init_sql() {
521            #[cfg(feature = "tracing")]
522            tracing::debug!(target: TARGET_RECONNECT, "Replaying reconnect initialization SQL");
523            self.execute(sql).await?;
524        }
525
526        Ok(())
527    }
528
529    /// Rebuild session state after reconnection.
530    ///
531    /// This re-LISTENs on channels and re-SETs custom GUC parameters.
532    /// Errors during rebuild are logged but not propagated — partial rebuild
533    /// is acceptable.
534    async fn rebuild_session(
535        &mut self,
536        state: &crate::reconnect::session::SessionState,
537    ) -> crate::error::Result<()> {
538        // Re-prepare statements (lazily — just clear tracking, the statement
539        // cache will re-prepare on next use)
540        #[cfg(feature = "tracing")]
541        if !state.prepared_statements().is_empty() {
542            tracing::debug!(
543                target: TARGET_RECONNECT,
544                count = state.prepared_statements().len(),
545                "Prepared statements will be re-prepared lazily on next use"
546            );
547        }
548
549        // Re-LISTEN on channels
550        for channel in state.listen_channels() {
551            let sql = Self::build_listen_sql(channel);
552            match self.execute(&sql).await {
553                Ok(_) => {
554                    #[cfg(feature = "tracing")]
555                    tracing::debug!(target: TARGET_RECONNECT, channel = %channel, "Re-LISTENed on channel after reconnect");
556                    self.session_state.track_listen(channel);
557                }
558                Err(e) => {
559                    #[cfg(feature = "tracing")]
560                    tracing::warn!(target: TARGET_RECONNECT, channel = %channel, error = %e, "Failed to re-LISTEN on channel after reconnect");
561                    let _ = &e; // suppress unused warning when tracing is disabled
562                }
563            }
564        }
565
566        // Re-SET custom GUC parameters
567        for (key, value) in state.custom_gucs() {
568            let sql = Self::build_set_param_sql(key, value);
569            match self.execute(&sql).await {
570                Ok(_) => {
571                    #[cfg(feature = "tracing")]
572                    tracing::debug!(target: TARGET_RECONNECT, key = %key, "Re-SET GUC parameter after reconnect");
573                    self.session_state.track_set_guc(key, value);
574                }
575                Err(e) => {
576                    #[cfg(feature = "tracing")]
577                    tracing::warn!(target: TARGET_RECONNECT, key = %key, error = %e, "Failed to re-SET GUC parameter after reconnect");
578                    let _ = &e; // suppress unused warning when tracing is disabled
579                }
580            }
581        }
582
583        Ok(())
584    }
585
586    /// Execute an operation with automatic reconnection and retry.
587    ///
588    /// This is the primary resilience method. It:
589    /// 1. Executes the operation
590    /// 2. If the connection is broken, attempts to reconnect and retry
591    /// 3. If the error is transient (serialization failure, deadlock), retries
592    /// 4. Respects the configured retry policy (max attempts, backoff)
593    ///
594    /// # Example
595    ///
596    /// ```rust,ignore
597    /// let result = conn.with_retry(|conn| {
598    ///     conn.query_params("SELECT * FROM users WHERE id = $1", &[&user_id])
599    /// }).await?;
600    /// ```
601    #[must_use = "retry errors should be checked"]
602    pub async fn with_retry<T, F, Fut>(&mut self, f: F) -> crate::error::Result<T>
603    where
604        F: Fn(&mut Connection) -> Fut,
605        Fut: std::future::Future<Output = crate::error::Result<T>>,
606    {
607        let config = self.config.get_reconnect().clone();
608        let max_attempts = if config.enabled {
609            config.max_attempts.max(1)
610        } else {
611            1 // no retry if reconnection is disabled
612        };
613
614        let mut attempt = 0;
615
616        loop {
617            attempt += 1;
618
619            // Execute the operation
620            match f(self).await {
621                Ok(result) => {
622                    self.health.mark_alive();
623                    self.session_state.set_in_transaction(
624                        self.transaction_status != crate::protocol::TransactionStatus::Idle,
625                    );
626                    return Ok(result);
627                }
628                Err(err) => {
629                    let class = crate::reconnect::classify::classify_error(&err);
630
631                    match class {
632                        crate::reconnect::classify::ErrorClass::Permanent => {
633                            // Permanent error — no retry
634                            return Err(err);
635                        }
636                        crate::reconnect::classify::ErrorClass::Transient => {
637                            // Transient error — retry if attempts remain
638                            if attempt >= max_attempts {
639                                #[cfg(feature = "tracing")]
640                                tracing::warn!(
641                                    target: TARGET_RECONNECT,
642                                    attempt = attempt,
643                                    max_attempts = max_attempts,
644                                    "Transient error: max retry attempts reached"
645                                );
646                                return Err(err);
647                            }
648
649                            let delay = config.delay_for_attempt(attempt);
650                            #[cfg(feature = "tracing")]
651                            tracing::debug!(
652                                target: TARGET_RECONNECT,
653                                attempt = attempt,
654                                delay_ms = delay.as_millis(),
655                                "Transient error: retrying after backoff"
656                            );
657                            reconnect_sleep(delay).await;
658                            continue;
659                        }
660                        crate::reconnect::classify::ErrorClass::Broken => {
661                            // Broken connection — reconnect and retry if enabled
662                            if !config.enabled {
663                                self.health.mark_broken();
664                                return Err(err);
665                            }
666
667                            // Check if reconnection is safe
668                            if !config.allow_mid_transaction && self.session_state.in_transaction()
669                            {
670                                #[cfg(feature = "tracing")]
671                                tracing::error!(
672                                    target: TARGET_RECONNECT,
673                                    "Connection broken mid-transaction. \
674                                     Reconnection is disabled for mid-transaction failures \
675                                     (set allow_mid_transaction=true to override)."
676                                );
677                                self.health.mark_broken();
678                                return Err(err);
679                            }
680
681                            if attempt >= max_attempts {
682                                #[cfg(feature = "tracing")]
683                                tracing::warn!(
684                                    target: TARGET_RECONNECT,
685                                    attempt = attempt,
686                                    max_attempts = max_attempts,
687                                    "Connection broken: max reconnection attempts reached"
688                                );
689                                return Err(err);
690                            }
691
692                            // Invoke callback
693                            if let Some(ref callback) = config.on_before_reconnect {
694                                callback(attempt, &err);
695                            }
696
697                            // Reconnect
698                            let delay = config.delay_for_attempt(attempt);
699                            #[cfg(feature = "tracing")]
700                            tracing::debug!(
701                                target: TARGET_RECONNECT,
702                                attempt = attempt,
703                                delay_ms = delay.as_millis(),
704                                "Connection broken: reconnecting after backoff"
705                            );
706                            reconnect_sleep(delay).await;
707
708                            match self.reconnect().await {
709                                Ok(()) => continue, // retry the operation
710                                Err(reconnect_err) => {
711                                    #[cfg(feature = "tracing")]
712                                    tracing::error!(
713                                        target: TARGET_RECONNECT,
714                                        error = %reconnect_err,
715                                        "Reconnection failed"
716                                    );
717                                    let _ = &reconnect_err; // suppress unused warning when tracing is disabled
718                                                            // Return the original error, not the reconnection error
719                                    return Err(err);
720                                }
721                            }
722                        }
723                    }
724                }
725            }
726        }
727    }
728
729    /// Ensure the connection is alive before use.
730    ///
731    /// If the connection is stale (hasn't been used recently), ping it
732    /// to verify it's still alive. If it's broken, attempt reconnection
733    /// if configured.
734    #[must_use = "health check errors should be checked"]
735    pub async fn ensure_alive(&mut self) -> crate::error::Result<()> {
736        if !self.health.is_alive() {
737            // Connection is known to be broken
738            #[cfg(feature = "tracing")]
739            tracing::warn!(target: TARGET_RECONNECT, "Connection is known to be broken");
740            if self.config.get_reconnect().enabled {
741                self.reconnect().await?;
742            } else {
743                return Err(crate::error::PgError::ConnectionClosed);
744            }
745            return Ok(());
746        }
747
748        if self.is_stale(self.config.get_stale().stale_threshold)
749            && self.config.get_stale().ping_on_stale
750        {
751            match self.ping().await {
752                Ok(()) => {
753                    self.health.mark_alive();
754                }
755                Err(e) => {
756                    #[cfg(feature = "tracing")]
757                    tracing::debug!(target: TARGET_RECONNECT, error = %e, "Stale connection ping failed");
758                    self.health.mark_broken();
759
760                    if self.config.get_reconnect().enabled {
761                        self.reconnect().await?;
762                    } else {
763                        return Err(e);
764                    }
765                }
766            }
767        }
768
769        Ok(())
770    }
771
772    pub(crate) fn handle_notice(&self, notice: &Notice) {
773        if let Some(ref handler) = self.notice_handler {
774            handler(notice);
775        } else {
776            #[cfg(feature = "tracing")]
777            tracing::warn!(severity = %notice.severity(), code = %notice.code(), message = %notice.message(), "server notice");
778        }
779    }
780
781    // =======================================================================
782    // Lifecycle
783    // =======================================================================
784
785    /// Gracefully closes the connection.
786    ///
787    /// Sends a `Terminate` message (`X`) to the server and shuts down the
788    /// underlying transport. After closing, the connection cannot be used for
789    /// further operations.
790    #[must_use = "close errors should be checked"]
791    pub async fn close(&mut self) -> Result<()> {
792        if self.state.is_closed() {
793            return Ok(());
794        }
795
796        #[cfg(feature = "tracing")]
797        tracing::info!(target: TARGET_CONNECTION, "Closing connection");
798
799        self.state = ConnectionState::Closing;
800
801        // Best-effort: send Terminate, ignore errors.
802        let _ = self
803            .codec
804            .send(&mut self.transport, &FrontendMessage::Terminate)
805            .await;
806
807        let _ = self.transport.shutdown().await;
808
809        self.state = ConnectionState::Closed;
810
811        #[cfg(feature = "tracing")]
812        tracing::debug!(target: TARGET_CONNECTION, "Connection closed");
813
814        Ok(())
815    }
816
817    /// Force-close the connection without sending a Terminate message.
818    ///
819    /// This is useful when the connection is already known to be broken.
820    pub async fn abort(&mut self) {
821        self.state = ConnectionState::Closed;
822        let _ = self.transport.shutdown().await;
823    }
824
825    /// Internal: read messages until `ReadyForQuery`, discarding everything else.
826    ///
827    /// Used after errors to resync the protocol state or to drain the response
828    /// stream after a query.
829    pub(crate) async fn read_until_ready(&mut self) -> Result<()> {
830        loop {
831            let msg = self.codec.read_message(&mut self.transport).await?;
832            match msg {
833                BackendMessage::ReadyForQuery(body) => {
834                    self.transaction_status = TransactionStatus::from_u8(body.status())
835                        .unwrap_or(TransactionStatus::Idle);
836                    self.session_state.set_in_transaction(
837                        self.transaction_status != crate::protocol::TransactionStatus::Idle,
838                    );
839                    self.state = ConnectionState::Idle;
840                    return Ok(());
841                }
842                BackendMessage::ParameterStatus(body) => {
843                    if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
844                        self.server_params
845                            .params
846                            .insert(name.to_string(), value.to_string());
847                    }
848                }
849                BackendMessage::NotificationResponse(body) => {
850                    let channel = body.channel().unwrap_or("").to_string();
851                    let payload = body.message().unwrap_or("").to_string();
852                    let process_id = body.process_id();
853                    #[cfg(feature = "tracing")]
854                    tracing::debug!(
855                        target: TARGET_NOTIFICATION,
856                        channel = %channel,
857                        process_id = process_id,
858                        payload_len = payload.len(),
859                        "Received notification"
860                    );
861                    self.notification_queue.push_back(Notification {
862                        process_id,
863                        channel,
864                        payload,
865                    });
866                }
867                BackendMessage::NoticeResponse(body) => {
868                    if let Ok(notice) = Notice::from_fields(&body) {
869                        self.handle_notice(&notice);
870                    }
871                }
872                _ => {} // discard other messages
873            }
874        }
875    }
876
877    /// Internal: handle asynchronous messages that can arrive at any time.
878    ///
879    /// PostgreSQL can send `NotificationResponse`, `NoticeResponse`, and
880    /// `ParameterStatus` messages asynchronously, interleaved with query
881    /// results. This method handles those messages and returns `true` if
882    /// the message was consumed (the caller should read the next message).
883    /// Returns `false` if the message is a synchronous response that the
884    /// caller should handle itself.
885    ///
886    /// Every message-reading loop in the library should call this method
887    /// for each message before processing it, to ensure no notifications
888    /// are lost.
889    pub(crate) fn handle_async_message(&mut self, msg: &BackendMessage) -> bool {
890        match msg {
891            BackendMessage::NotificationResponse(body) => {
892                let channel = body.channel().unwrap_or("").to_string();
893                let payload = body.message().unwrap_or("").to_string();
894                let process_id = body.process_id();
895                #[cfg(feature = "tracing")]
896                tracing::debug!(
897                    target: TARGET_NOTIFICATION,
898                    channel = %channel,
899                    process_id = process_id,
900                    payload_len = payload.len(),
901                    "Received notification"
902                );
903                #[cfg(feature = "tracing")]
904                tracing::trace!(
905                    target: TARGET_NOTIFICATION,
906                    channel = %channel,
907                    payload = %payload,
908                    "Received notification (with payload)"
909                );
910                self.notification_queue.push_back(Notification {
911                    process_id,
912                    channel,
913                    payload,
914                });
915                true
916            }
917            BackendMessage::NoticeResponse(body) => {
918                if let Ok(notice) = Notice::from_fields(body) {
919                    self.handle_notice(&notice);
920                }
921                true
922            }
923            BackendMessage::ParameterStatus(body) => {
924                if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
925                    self.server_params
926                        .params
927                        .insert(name.to_string(), value.to_string());
928                }
929                true
930            }
931            _ => false,
932        }
933    }
934
935    /// Internal: transition to a new state, returning an error if the current
936    /// state does not permit the transition.
937    pub(crate) fn transition(&mut self, new_state: ConnectionState) -> Result<()> {
938        match (self.state, new_state) {
939            // Any → Closed is always allowed (error recovery).
940            (_, ConnectionState::Closed) => {}
941            // Idle → active states.
942            (ConnectionState::Idle, ConnectionState::ActiveSimpleQuery)
943            | (ConnectionState::Idle, ConnectionState::ActiveExtendedQuery)
944            | (ConnectionState::Idle, ConnectionState::CopyIn)
945            | (ConnectionState::Idle, ConnectionState::CopyOut)
946            | (ConnectionState::Idle, ConnectionState::Streaming) => {}
947            // Active → Idle (completion).
948            (ConnectionState::ActiveSimpleQuery, ConnectionState::Idle)
949            | (ConnectionState::ActiveExtendedQuery, ConnectionState::Idle)
950            | (ConnectionState::CopyIn, ConnectionState::Idle)
951            | (ConnectionState::CopyOut, ConnectionState::Idle)
952            | (ConnectionState::Streaming, ConnectionState::Idle) => {}
953            // Idle → Closing.
954            (ConnectionState::Idle, ConnectionState::Closing) => {}
955            // Any other transition is invalid.
956            (old, new) => {
957                return Err(PgError::InvalidState(format!(
958                    "invalid state transition from {old:?} to {new:?}"
959                )));
960            }
961        }
962        self.state = new_state;
963        Ok(())
964    }
965
966    // =======================================================================
967    // Synchronous COPY recovery helpers (for Drop implementations)
968    // =======================================================================
969
970    /// Best-effort synchronous cancellation of a COPY IN operation.
971    ///
972    /// This is called from `CopyIn::drop()` when the `CopyIn` was not
973    /// properly finished or cancelled. It encodes a `CopyFail` message
974    /// and attempts a blocking write + flush on the underlying transport.
975    ///
976    /// **Limitations:**
977    /// - On WASI targets (async-only I/O), this is a no-op because we
978    ///   cannot perform I/O in a synchronous `Drop` context.
979    /// - On native targets with `NativeTcpTransport`, this will attempt
980    ///   a blocking write of the `CopyFail` message followed by a drain
981    ///   of the server's response.
982    /// - If the write fails (e.g., broken pipe), the error is silently
983    ///   ignored — the connection is already in a bad state.
984    pub(crate) fn cancel_copy_in_sync(&mut self, reason: &str) {
985        // Encode CopyFail message using the Codec
986        let copy_fail = FrontendMessage::CopyFail {
987            message: reason.to_string(),
988        };
989        if self.codec.encode_to_buffer(&copy_fail).is_err() {
990            self.state = ConnectionState::Closed;
991            return;
992        }
993        // Clone the buffer data to avoid borrow conflict with try_sync_write_and_flush
994        let data = self.codec.write_buffer().to_vec();
995
996        // Attempt to write and flush synchronously
997        let written = self.try_sync_write_and_flush(&data);
998
999        if written {
1000            // Try to drain the server's response (ErrorResponse + ReadyForQuery)
1001            // so the connection might be recoverable
1002            self.try_sync_drain_until_ready();
1003            self.state = ConnectionState::Idle;
1004        } else {
1005            // Could not send CopyFail — connection is broken
1006            self.state = ConnectionState::Closed;
1007        }
1008    }
1009
1010    /// Best-effort synchronous drain of a COPY OUT operation.
1011    ///
1012    /// This is called from `CopyOut::drop()` when there is unread COPY
1013    /// data. It attempts to read and discard data until `ReadyForQuery`
1014    /// so the connection can be reused.
1015    ///
1016    /// **Limitations:**
1017    /// - On WASI targets (async-only I/O), this is a no-op.
1018    /// - On native targets, this performs blocking reads.
1019    /// - If the read fails, the connection is marked as `Closed`.
1020    pub(crate) fn drain_copy_out_sync(&mut self) {
1021        if self.try_sync_drain_until_ready() {
1022            self.state = ConnectionState::Idle;
1023        } else {
1024            self.state = ConnectionState::Closed;
1025        }
1026    }
1027
1028    /// Attempt a synchronous write + flush on the underlying transport.
1029    ///
1030    /// Returns `true` if the write succeeded, `false` otherwise.
1031    /// This only works for `NativeTcpTransport`; for WASI it's a no-op.
1032    #[cfg(all(not(target_arch = "wasm32"), feature = "test-native"))]
1033    fn try_sync_write_and_flush(&mut self, data: &[u8]) -> bool {
1034        use std::io::Write;
1035
1036        match &mut self.transport {
1037            PgTransport::Plain(ref mut buffered) => {
1038                let inner = buffered.inner_mut();
1039                match inner {
1040                    ClientTransport::Native(ref mut tcp) => {
1041                        // First, try to flush any data that the BufferedTransport
1042                        // may have buffered. We can't call async flush, so we
1043                        // write directly to the TcpStream.
1044                        //
1045                        // Note: This bypasses the BufferedTransport's buffer,
1046                        // which means any previously buffered data may be lost.
1047                        // This is acceptable because we're in an error recovery
1048                        // path and the connection state is already compromised.
1049                        if let Err(e) = tcp.stream.write_all(data) {
1050                            let _ = &e;
1051                            false
1052                        } else {
1053                            tcp.stream.flush().is_ok()
1054                        }
1055                    }
1056                    _ => false,
1057                }
1058            }
1059            PgTransport::Tls(_) => {
1060                // TLS transport doesn't support easy sync I/O
1061                false
1062            }
1063        }
1064    }
1065
1066    /// Attempt a synchronous write + flush — WASI no-op.
1067    #[cfg(target_arch = "wasm32")]
1068    fn try_sync_write_and_flush(&mut self, _data: &[u8]) -> bool {
1069        // WASI I/O is async-only; cannot perform I/O in Drop
1070        false
1071    }
1072
1073    /// Attempt a synchronous write + flush — fallback no-op when no native transport.
1074    #[cfg(all(not(target_arch = "wasm32"), not(feature = "test-native")))]
1075    fn try_sync_write_and_flush(&mut self, _data: &[u8]) -> bool {
1076        false
1077    }
1078
1079    /// Attempt to synchronously read and discard messages until ReadyForQuery.
1080    ///
1081    /// Returns `true` if `ReadyForQuery` was received, `false` otherwise.
1082    #[cfg(all(not(target_arch = "wasm32"), feature = "test-native"))]
1083    fn try_sync_drain_until_ready(&mut self) -> bool {
1084        use std::io::Read;
1085
1086        match &mut self.transport {
1087            PgTransport::Plain(ref mut buffered) => {
1088                let inner = buffered.inner_mut();
1089                match inner {
1090                    ClientTransport::Native(ref mut tcp) => {
1091                        // Read in a loop looking for ReadyForQuery ('Z')
1092                        let mut buf = [0u8; 4096];
1093                        let mut scan_buf: Vec<u8> = Vec::new();
1094
1095                        loop {
1096                            // Check if we already have ReadyForQuery in scan_buf
1097                            // ReadyForQuery: 'Z' (1 byte) + length (4 bytes, big-endian) + status (1 byte)
1098                            // Length should be 5 (includes the length field itself)
1099                            if let Some(pos) = scan_buf.iter().position(|&b| b == b'Z') {
1100                                // Need at least 6 bytes: 'Z' + 4-byte length + 1-byte status
1101                                if scan_buf.len() >= pos + 6 {
1102                                    let len = i32::from_be_bytes([
1103                                        scan_buf[pos + 1],
1104                                        scan_buf[pos + 2],
1105                                        scan_buf[pos + 3],
1106                                        scan_buf[pos + 4],
1107                                    ]);
1108                                    if len == 5 {
1109                                        // Validate status byte is a known transaction status
1110                                        let status = scan_buf[pos + 5];
1111                                        if status == b'I' || status == b'T' || status == b'E' {
1112                                            return true;
1113                                        }
1114                                    }
1115                                }
1116                            }
1117
1118                            match tcp.stream.read(&mut buf) {
1119                                Ok(0) => return false, // EOF
1120                                Ok(n) => scan_buf.extend_from_slice(&buf[..n]),
1121                                Err(_) => return false,
1122                            }
1123
1124                            // Safety limit: don't read more than 10MB
1125                            if scan_buf.len() > 10 * 1024 * 1024 {
1126                                return false;
1127                            }
1128                        }
1129                    }
1130                    _ => false,
1131                }
1132            }
1133            PgTransport::Tls(_) => false,
1134        }
1135    }
1136
1137    /// Attempt to synchronously drain — WASI no-op.
1138    #[cfg(target_arch = "wasm32")]
1139    fn try_sync_drain_until_ready(&mut self) -> bool {
1140        false
1141    }
1142
1143    /// Attempt to synchronously drain — fallback no-op.
1144    #[cfg(all(not(target_arch = "wasm32"), not(feature = "test-native")))]
1145    fn try_sync_drain_until_ready(&mut self) -> bool {
1146        false
1147    }
1148}
1149
1150impl Drop for Connection {
1151    fn drop(&mut self) {
1152        // We cannot perform async I/O in Drop, so we cannot send a
1153        // PostgreSQL Terminate message. However, we CAN shut down the
1154        // underlying TCP socket synchronously, which sends a FIN to the
1155        // server and ensures the connection is properly cleaned up.
1156        //
1157        // Without this, connections that are dropped without calling
1158        // `conn.close().await` would leak until the OS reclaims them or
1159        // the server times out, causing accumulation and potential lock
1160        // contention in long-running test suites.
1161        //
1162        // The transport's own Drop impl handles the actual socket
1163        // shutdown (WasiTcpTransport calls socket.shutdown(),
1164        // NativeTcpTransport calls stream.shutdown()). Here we just
1165        // ensure the state is marked Closed so the transport is dropped.
1166        #[cfg(feature = "tracing")]
1167        tracing::debug!(target: TARGET_CONNECTION, state = ?self.state, "Connection dropped; transport Drop will close socket");
1168        self.state = ConnectionState::Closed;
1169        // The transport field is dropped here, triggering its Drop impl
1170        // which performs the actual socket shutdown.
1171    }
1172}
1173
1174// ---------------------------------------------------------------------------
1175// Platform-specific transport construction
1176// ---------------------------------------------------------------------------
1177
1178#[cfg(target_arch = "wasm32")]
1179async fn build_pg_transport(config: &Config) -> Result<PgTransport<ClientTransport>> {
1180    let tcp = crate::transport::connect_with_timeout(
1181        config.get_host(),
1182        config.get_port(),
1183        config.get_connect_timeout(),
1184    )
1185    .await
1186    .map_err(PgError::Transport)?;
1187    apply_tls(ClientTransport::Wasi(tcp), config).await
1188}
1189
1190#[cfg(all(not(target_arch = "wasm32"), feature = "tokio-transport"))]
1191async fn build_pg_transport(config: &Config) -> Result<PgTransport<ClientTransport>> {
1192    let tcp = crate::transport::connect_with_timeout(
1193        config.get_host(),
1194        config.get_port(),
1195        config.get_connect_timeout(),
1196    )
1197    .await
1198    .map_err(PgError::Transport)?;
1199    apply_tls(ClientTransport::Tokio(tcp), config).await
1200}
1201
1202#[cfg(all(
1203    not(target_arch = "wasm32"),
1204    not(feature = "tokio-transport"),
1205    feature = "test-native"
1206))]
1207async fn build_pg_transport(config: &Config) -> Result<PgTransport<ClientTransport>> {
1208    let tcp = crate::transport::NativeTcpTransport::connect_with_timeout(
1209        config.get_host(),
1210        config.get_port(),
1211        config.get_connect_timeout(),
1212    )
1213    .map_err(PgError::Transport)?;
1214    apply_tls(ClientTransport::Native(tcp), config).await
1215}
1216
1217#[cfg(all(
1218    not(target_arch = "wasm32"),
1219    not(feature = "tokio-transport"),
1220    not(feature = "test-native")
1221))]
1222async fn build_pg_transport(_config: &Config) -> Result<PgTransport<ClientTransport>> {
1223    Err(PgError::Unsupported(
1224        "no transport available for this target. Enable the 'tokio-transport' feature (recommended) or 'test-native' feature, or compile for wasm32-wasip2".into(),
1225    ))
1226}
1227
1228#[allow(dead_code)]
1229async fn apply_tls(tcp: ClientTransport, config: &Config) -> Result<PgTransport<ClientTransport>> {
1230    if matches!(config.get_ssl_mode(), SslMode::Disable) {
1231        Ok(PgTransport::Plain(BufferedTransport::new(tcp)))
1232    } else {
1233        let tls_config = TlsConfig {
1234            mode: config.get_ssl_mode(),
1235            server_name: config.get_host().into(),
1236            accept_invalid_certs: config.get_accept_invalid_certs(),
1237            ..Default::default()
1238        };
1239        crate::transport::negotiate_tls(tcp, &tls_config)
1240            .await
1241            .map_err(PgError::Transport)
1242    }
1243}
1244
1245/// Platform-aware async sleep for reconnection backoff.
1246/// Uses `wstd::time::Timer::after` on WASI P2.
1247#[cfg(target_arch = "wasm32")]
1248async fn reconnect_sleep(duration: std::time::Duration) {
1249    wstd::time::Timer::after(duration.into()).wait().await;
1250}
1251
1252#[cfg(not(target_arch = "wasm32"))]
1253async fn reconnect_sleep(duration: std::time::Duration) {
1254    #[cfg(feature = "tokio-transport")]
1255    tokio::time::sleep(duration).await;
1256
1257    #[cfg(not(feature = "tokio-transport"))]
1258    {
1259        std::thread::sleep(duration);
1260    }
1261}
1262
1263// ---------------------------------------------------------------------------
1264// Tests
1265// ---------------------------------------------------------------------------
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270    use crate::transport::MockTransport;
1271
1272    /// Compile-time assertion that `Connection` is `Send` on WASI.
1273    /// This verifies that the wstd 0.6 upgrade (Arc instead of Rc) works.
1274    #[test]
1275    #[cfg(target_arch = "wasm32")]
1276    fn connection_is_send() {
1277        fn assert_send<T: Send>() {}
1278        assert_send::<Connection>();
1279    }
1280
1281    #[test]
1282    fn test_connection_state_transitions() {
1283        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1284            MockTransport::new(vec![]),
1285        )));
1286        let mut conn = Connection {
1287            transport,
1288            codec: auth::Codec::new(),
1289            server_params: ServerParams::default(),
1290            state: ConnectionState::Idle,
1291            config: Config::new(),
1292            transaction_status: TransactionStatus::Idle,
1293            notification_queue: VecDeque::new(),
1294            notice_handler: None,
1295            statement_counter: 0,
1296            needs_recovery: false,
1297            health: crate::reconnect::session::ConnectionHealth::new(),
1298            session_state: crate::reconnect::session::SessionState::new(),
1299        };
1300
1301        assert!(conn.is_idle());
1302        assert!(!conn.is_closed());
1303
1304        conn.transition(ConnectionState::ActiveSimpleQuery).unwrap();
1305        assert_eq!(conn.state(), ConnectionState::ActiveSimpleQuery);
1306
1307        conn.transition(ConnectionState::Idle).unwrap();
1308        assert!(conn.is_idle());
1309
1310        conn.transition(ConnectionState::Closing).unwrap();
1311        assert_eq!(conn.state(), ConnectionState::Closing);
1312
1313        conn.transition(ConnectionState::Closed).unwrap();
1314        assert!(conn.is_closed());
1315    }
1316
1317    #[test]
1318    fn test_invalid_state_transition() {
1319        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1320            MockTransport::new(vec![]),
1321        )));
1322        let mut conn = Connection {
1323            transport,
1324            codec: auth::Codec::new(),
1325            server_params: ServerParams::default(),
1326            state: ConnectionState::ActiveSimpleQuery,
1327            config: Config::new(),
1328            transaction_status: TransactionStatus::Idle,
1329            notification_queue: VecDeque::new(),
1330            notice_handler: None,
1331            statement_counter: 0,
1332            needs_recovery: false,
1333            health: crate::reconnect::session::ConnectionHealth::new(),
1334            session_state: crate::reconnect::session::SessionState::new(),
1335        };
1336
1337        assert!(conn.transition(ConnectionState::Streaming).is_err());
1338    }
1339
1340    #[test]
1341    fn test_is_healthy_idle() {
1342        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1343            MockTransport::new(vec![]),
1344        )));
1345        let conn = Connection {
1346            transport,
1347            codec: auth::Codec::new(),
1348            server_params: ServerParams::default(),
1349            state: ConnectionState::Idle,
1350            config: Config::new(),
1351            transaction_status: TransactionStatus::Idle,
1352            notification_queue: VecDeque::new(),
1353            notice_handler: None,
1354            statement_counter: 0,
1355            needs_recovery: false,
1356            health: crate::reconnect::session::ConnectionHealth::new(),
1357            session_state: crate::reconnect::session::SessionState::new(),
1358        };
1359        assert!(conn.is_healthy());
1360    }
1361
1362    #[test]
1363    fn test_is_healthy_closed() {
1364        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1365            MockTransport::new(vec![]),
1366        )));
1367        let conn = Connection {
1368            transport,
1369            codec: auth::Codec::new(),
1370            server_params: ServerParams::default(),
1371            state: ConnectionState::Closed,
1372            config: Config::new(),
1373            transaction_status: TransactionStatus::Idle,
1374            notification_queue: VecDeque::new(),
1375            notice_handler: None,
1376            statement_counter: 0,
1377            needs_recovery: false,
1378            health: crate::reconnect::session::ConnectionHealth::new(),
1379            session_state: crate::reconnect::session::SessionState::new(),
1380        };
1381        assert!(!conn.is_healthy());
1382    }
1383
1384    #[test]
1385    fn test_is_healthy_failed_transaction() {
1386        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1387            MockTransport::new(vec![]),
1388        )));
1389        let conn = Connection {
1390            transport,
1391            codec: auth::Codec::new(),
1392            server_params: ServerParams::default(),
1393            state: ConnectionState::Idle,
1394            config: Config::new(),
1395            transaction_status: TransactionStatus::Failed,
1396            notification_queue: VecDeque::new(),
1397            notice_handler: None,
1398            statement_counter: 0,
1399            needs_recovery: false,
1400            health: crate::reconnect::session::ConnectionHealth::new(),
1401            session_state: crate::reconnect::session::SessionState::new(),
1402        };
1403        assert!(!conn.is_healthy());
1404    }
1405
1406    #[test]
1407    fn test_is_healthy_in_transaction() {
1408        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1409            MockTransport::new(vec![]),
1410        )));
1411        let conn = Connection {
1412            transport,
1413            codec: auth::Codec::new(),
1414            server_params: ServerParams::default(),
1415            state: ConnectionState::Idle,
1416            config: Config::new(),
1417            transaction_status: TransactionStatus::InTransaction,
1418            notification_queue: VecDeque::new(),
1419            notice_handler: None,
1420            statement_counter: 0,
1421            needs_recovery: false,
1422            health: crate::reconnect::session::ConnectionHealth::new(),
1423            session_state: crate::reconnect::session::SessionState::new(),
1424        };
1425        // InTransaction is still healthy (just busy)
1426        assert!(conn.is_healthy());
1427    }
1428
1429    fn build_command_complete_msg(tag: &str) -> Vec<u8> {
1430        let mut buf = vec![b'C'];
1431        let mut body = Vec::new();
1432        body.extend_from_slice(tag.as_bytes());
1433        body.push(0);
1434        let len = (body.len() + 4) as i32;
1435        buf.extend_from_slice(&len.to_be_bytes());
1436        buf.extend_from_slice(&body);
1437        buf
1438    }
1439
1440    fn build_ready_for_query(status: u8) -> Vec<u8> {
1441        vec![b'Z', 0, 0, 0, 5, status]
1442    }
1443
1444    #[test]
1445    fn test_build_set_param_sql_quotes_and_escapes() {
1446        let sql = Connection::build_set_param_sql("time\"zone", "O'Reilly\\UTC");
1447        assert_eq!(sql, "SET \"time\"\"zone\" = 'O''Reilly\\\\UTC'");
1448    }
1449
1450    #[test]
1451    fn test_build_listen_and_unlisten_sql_quote_identifier() {
1452        assert_eq!(
1453            Connection::build_listen_sql("chan\"nel"),
1454            "LISTEN \"chan\"\"nel\""
1455        );
1456        assert_eq!(
1457            Connection::build_unlisten_sql("chan\"nel"),
1458            "UNLISTEN \"chan\"\"nel\""
1459        );
1460    }
1461
1462    fn make_connection(read_data: Vec<u8>) -> Connection {
1463        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
1464            MockTransport::new(read_data),
1465        )));
1466        Connection {
1467            transport,
1468            codec: auth::Codec::new(),
1469            server_params: ServerParams::default(),
1470            state: ConnectionState::Idle,
1471            config: Config::new(),
1472            transaction_status: TransactionStatus::Idle,
1473            notification_queue: VecDeque::new(),
1474            notice_handler: None,
1475            statement_counter: 0,
1476            needs_recovery: false,
1477            health: crate::reconnect::session::ConnectionHealth::new(),
1478            session_state: crate::reconnect::session::SessionState::new(),
1479        }
1480    }
1481
1482    #[tokio::test]
1483    async fn test_set_param() {
1484        // Build mock response for SET command
1485        let mut data = Vec::new();
1486        data.extend_from_slice(&build_command_complete_msg("SET"));
1487        data.extend_from_slice(&build_ready_for_query(b'I'));
1488
1489        let mut conn = make_connection(data);
1490        assert!(conn.session_state.custom_gucs().get("timezone").is_none());
1491        conn.set_param("timezone", "UTC").await.unwrap();
1492        assert_eq!(
1493            conn.session_state.custom_gucs().get("timezone"),
1494            Some(&"UTC".to_string())
1495        );
1496    }
1497
1498    #[tokio::test]
1499    async fn test_reset_clears_session_state_but_preserves_reconnect_init_sql() {
1500        let mut data = Vec::new();
1501        data.extend_from_slice(&build_command_complete_msg("ROLLBACK"));
1502        data.extend_from_slice(&build_ready_for_query(b'I'));
1503        data.extend_from_slice(&build_command_complete_msg("DISCARD ALL"));
1504        data.extend_from_slice(&build_ready_for_query(b'I'));
1505
1506        let mut conn = make_connection(data);
1507        conn.transaction_status = TransactionStatus::Failed;
1508        conn.session_state.track_prepare("stmt1", "SELECT 1");
1509        conn.session_state.track_listen("events");
1510        conn.session_state.track_temp_table("tmp_data");
1511        conn.session_state.track_set_guc("timezone", "UTC");
1512        conn.session_state.set_in_transaction(true);
1513        conn.set_reconnect_init_sql("SET timezone = 'UTC'");
1514
1515        conn.reset().await.unwrap();
1516
1517        assert_eq!(conn.transaction_status(), TransactionStatus::Idle);
1518        assert!(!conn.session_state().has_state());
1519        assert!(conn.session_state().listen_channels().is_empty());
1520        assert!(conn.session_state().prepared_statements().is_empty());
1521        assert!(conn.session_state().temporary_tables().is_empty());
1522        assert!(conn.session_state().custom_gucs().is_empty());
1523        assert!(conn.session_state().is_reconnect_safe());
1524        assert_eq!(conn.reconnect_init_sql(), Some("SET timezone = 'UTC'"));
1525    }
1526
1527    #[test]
1528    fn test_reconnect_init_sql_accessors() {
1529        let mut conn = make_connection(vec![]);
1530        assert_eq!(conn.reconnect_init_sql(), None);
1531
1532        conn.set_reconnect_init_sql("SET timezone = 'UTC'");
1533        assert_eq!(conn.reconnect_init_sql(), Some("SET timezone = 'UTC'"));
1534        assert_eq!(
1535            conn.session_state.reconnect_init_sql(),
1536            Some("SET timezone = 'UTC'")
1537        );
1538
1539        conn.clear_reconnect_init_sql();
1540        assert_eq!(conn.reconnect_init_sql(), None);
1541        assert_eq!(conn.session_state.reconnect_init_sql(), None);
1542    }
1543}