Skip to main content

wasi_pg_client/copy/
mod.rs

1//! PostgreSQL COPY protocol — bulk data import / export.
2//!
3//! This module provides [`CopyIn`] (client → server) and [`CopyOut`]
4//! (server → client) for high-performance bulk data transfer.
5//!
6//! # Example — COPY IN (text)
7//! ```ignore
8//! let mut copy = conn.copy_in("COPY my_table FROM STDIN").await?;
9//! copy.write_row(&["1", "alice"]).await?;
10//! copy.write_row(&["2", "bob"]).await?;
11//! let rows = copy.finish().await?;
12//! ```
13//!
14//! # Example — COPY OUT (text)
15//! ```ignore
16//! let mut copy = conn.copy_out("COPY my_table TO STDOUT").await?;
17//! while let Some(chunk) = copy.read_next().await? {
18//!     process(&chunk);
19//! }
20//! ```
21
22use crate::protocol::{BackendMessage, FrontendMessage, TransactionStatus};
23use fallible_iterator::FallibleIterator;
24
25use crate::connection::{Connection, ConnectionState};
26use crate::error::{Error, PgError, PgServerError, Result};
27
28#[cfg(feature = "tracing")]
29use crate::tracing_ext::{truncate_str, TARGET_COPY};
30
31mod binary;
32
33pub use binary::BinaryCopyWriter;
34
35// ---------------------------------------------------------------------------
36// CSV parsing helper
37// ---------------------------------------------------------------------------
38
39/// Parse a single CSV line into fields.
40///
41/// Handles quoted fields (where the quote character is doubled to escape),
42/// fields containing the delimiter, and fields spanning multiple characters.
43fn parse_csv_line(line: &str, delimiter: char, quote: char) -> Vec<String> {
44    let mut fields = Vec::new();
45    let mut current = String::new();
46    let mut in_quotes = false;
47    let mut chars = line.chars().peekable();
48
49    while let Some(ch) = chars.next() {
50        if in_quotes {
51            if ch == quote {
52                // Check for escaped quote (doubled)
53                if chars.peek() == Some(&quote) {
54                    chars.next(); // consume the second quote
55                    current.push(quote);
56                } else {
57                    // End of quoted field
58                    in_quotes = false;
59                }
60            } else {
61                current.push(ch);
62            }
63        } else if ch == delimiter {
64            fields.push(std::mem::take(&mut current));
65        } else if ch == quote {
66            in_quotes = true;
67        } else {
68            current.push(ch);
69        }
70    }
71
72    fields.push(current);
73    fields
74}
75
76// ---------------------------------------------------------------------------
77// CopyFormat
78// ---------------------------------------------------------------------------
79
80/// Supported COPY formats.
81#[derive(Debug, Clone, PartialEq, Eq, Default)]
82#[non_exhaustive]
83pub enum CopyFormat {
84    /// Text format (tab-separated, newline-terminated rows).
85    #[default]
86    Text,
87    /// CSV format with options.
88    Csv {
89        /// Field delimiter (default `,`).
90        delimiter: char,
91        /// String representing NULL (default empty string).
92        null: String,
93        /// Whether to include a header row.
94        header: bool,
95        /// Quote character (default `"`).
96        quote: char,
97        /// Escape character (default `"`).
98        escape: char,
99    },
100    /// Binary format.
101    Binary,
102}
103
104impl CopyFormat {
105    /// Returns the SQL `WITH (...)` clause fragment for this format.
106    pub fn to_sql_options(&self) -> String {
107        match self {
108            CopyFormat::Text => String::new(),
109            CopyFormat::Csv {
110                delimiter,
111                null,
112                header,
113                quote,
114                escape,
115            } => {
116                format!(
117                    "WITH (FORMAT csv, DELIMITER '{}', NULL '{}', HEADER {}, QUOTE '{}', ESCAPE '{}')",
118                    delimiter, null, header, quote, escape
119                )
120            }
121            CopyFormat::Binary => "WITH (FORMAT binary)".to_string(),
122        }
123    }
124}
125
126// ---------------------------------------------------------------------------
127// CopyIn
128// ---------------------------------------------------------------------------
129
130/// A writer for a COPY IN operation.
131///
132/// Created via [`Connection::copy_in`]. Data is sent to the server in
133/// chunks. The operation must be completed with [`finish`](Self::finish)
134/// or cancelled with [`cancel`](Self::cancel).
135#[non_exhaustive]
136pub struct CopyIn<'a> {
137    conn: &'a mut Connection,
138    format: u8,
139    column_formats: Vec<u16>,
140    done: bool,
141}
142
143impl<'a> CopyIn<'a> {
144    /// The overall format code for this COPY operation.
145    ///
146    /// `0` = text, `1` = binary.
147    pub fn format(&self) -> u8 {
148        self.format
149    }
150
151    /// Per-column format codes.
152    pub fn column_formats(&self) -> &[u16] {
153        &self.column_formats
154    }
155
156    /// Send a raw chunk of COPY data.
157    #[must_use = "copy write errors should be checked"]
158    pub async fn write(&mut self, data: &[u8]) -> Result<()> {
159        #[cfg(feature = "tracing")]
160        tracing::trace!(target: TARGET_COPY, chunk_len = data.len(), "COPY IN: writing data chunk");
161        self.conn
162            .codec
163            .send(
164                &mut self.conn.transport,
165                &FrontendMessage::CopyData {
166                    data: data.to_vec(),
167                },
168            )
169            .await
170            .map_err(Error::from)
171    }
172
173    /// Send a single text-format row.
174    ///
175    /// Columns are joined with `\t` and terminated with `\n`.
176    #[must_use = "copy write errors should be checked"]
177    pub async fn write_row(&mut self, columns: &[&str]) -> Result<()> {
178        let line = columns.join("\t") + "\n";
179        self.write(line.as_bytes()).await
180    }
181
182    /// Send a single CSV-format row.
183    ///
184    /// This uses PostgreSQL CSV format rules: fields containing the delimiter,
185    /// quote character, or newline are wrapped in quotes; quotes inside quoted
186    /// fields are doubled.
187    ///
188    /// **Note:** This method cannot represent NULL values. Use
189    /// [`write_csv_row_with_null`](Self::write_csv_row_with_null) if you need
190    /// NULL support.
191    #[must_use = "copy write errors should be checked"]
192    pub async fn write_csv_row(
193        &mut self,
194        columns: &[&str],
195        delimiter: char,
196        quote: char,
197    ) -> Result<()> {
198        let mut line = String::new();
199        for (i, col) in columns.iter().enumerate() {
200            if i > 0 {
201                line.push(delimiter);
202            }
203            let needs_quote = col.contains(delimiter)
204                || col.contains(quote)
205                || col.contains('\n')
206                || col.contains('\r');
207            if needs_quote {
208                line.push(quote);
209                for ch in col.chars() {
210                    if ch == quote {
211                        line.push(quote);
212                    }
213                    line.push(ch);
214                }
215                line.push(quote);
216            } else {
217                line.push_str(col);
218            }
219        }
220        line.push('\n');
221        self.write(line.as_bytes()).await
222    }
223
224    /// Send a single CSV-format row with NULL support.
225    ///
226    /// Like [`write_csv_row`](Self::write_csv_row), but each column is an
227    /// `Option<&str>`. `None` values are written as the `null_string`
228    /// parameter (typically `""` for the default PostgreSQL CSV NULL
229    /// representation, which is an empty string).
230    ///
231    /// # Example
232    /// ```ignore
233    /// let mut copy = conn.copy_in("COPY users (id, name) FROM STDIN WITH (FORMAT csv)").await?;
234    /// copy.write_csv_row_with_null(
235    ///     &[Some("1"), None, Some("active")],
236    ///     ',', '"', "",
237    /// ).await?;
238    /// ```
239    #[must_use = "copy write errors should be checked"]
240    pub async fn write_csv_row_with_null(
241        &mut self,
242        columns: &[Option<&str>],
243        delimiter: char,
244        quote: char,
245        null_string: &str,
246    ) -> Result<()> {
247        let mut line = String::new();
248        for (i, col) in columns.iter().enumerate() {
249            if i > 0 {
250                line.push(delimiter);
251            }
252            match col {
253                Some(val) => {
254                    let needs_quote = val.contains(delimiter)
255                        || val.contains(quote)
256                        || val.contains('\n')
257                        || val.contains('\r');
258                    if needs_quote {
259                        line.push(quote);
260                        for ch in val.chars() {
261                            if ch == quote {
262                                line.push(quote);
263                            }
264                            line.push(ch);
265                        }
266                        line.push(quote);
267                    } else {
268                        line.push_str(val);
269                    }
270                }
271                None => {
272                    // Write the NULL representation string
273                    line.push_str(null_string);
274                }
275            }
276        }
277        line.push('\n');
278        self.write(line.as_bytes()).await
279    }
280
281    /// Finish the COPY operation successfully.
282    ///
283    /// Sends `CopyDone` and waits for `CommandComplete` + `ReadyForQuery`.
284    /// Returns the number of rows copied.
285    #[must_use = "copy finish errors should be checked"]
286    pub async fn finish(mut self) -> Result<u64> {
287        self.conn
288            .codec
289            .send(&mut self.conn.transport, &FrontendMessage::CopyDone)
290            .await
291            .map_err(Error::from)?;
292
293        let mut rows = 0u64;
294        loop {
295            let msg = self
296                .conn
297                .codec
298                .read_message(&mut self.conn.transport)
299                .await?;
300            if self.conn.handle_async_message(&msg) {
301                continue;
302            }
303            match msg {
304                BackendMessage::CommandComplete(body) => {
305                    let tag = body.tag().unwrap_or("");
306                    rows = crate::query::result::CommandTag::new(tag.to_string())
307                        .rows_affected()
308                        .unwrap_or(0);
309                }
310                BackendMessage::ReadyForQuery(body) => {
311                    self.conn.transaction_status = TransactionStatus::from_u8(body.status())
312                        .unwrap_or(TransactionStatus::Idle);
313                    self.done = true;
314                    self.conn.state = ConnectionState::Idle;
315                    break;
316                }
317                BackendMessage::ErrorResponse(body) => {
318                    let server_err = PgServerError::from_error_body(&body).map_err(PgError::Io)?;
319                    self.conn.read_until_ready().await?;
320                    self.done = true;
321                    self.conn.state = ConnectionState::Idle;
322                    return Err(PgError::Server(Box::new(server_err)));
323                }
324                _ => {}
325            }
326        }
327
328        #[cfg(feature = "tracing")]
329        tracing::info!(target: TARGET_COPY, rows = rows, "COPY IN: finished");
330        Ok(rows)
331    }
332
333    /// Cancel the COPY operation.
334    ///
335    /// Sends `CopyFail` with the given reason and waits for the server
336    /// to return to ready state.
337    #[must_use = "copy cancel errors should be checked"]
338    pub async fn cancel(mut self, reason: &str) -> Result<()> {
339        self.conn
340            .codec
341            .send(
342                &mut self.conn.transport,
343                &FrontendMessage::CopyFail {
344                    message: reason.to_string(),
345                },
346            )
347            .await
348            .map_err(Error::from)?;
349
350        self.conn.read_until_ready().await?;
351        self.done = true;
352        self.conn.state = ConnectionState::Idle;
353        Ok(())
354    }
355}
356
357impl<'a> Drop for CopyIn<'a> {
358    fn drop(&mut self) {
359        if self.done || std::thread::panicking() {
360            return;
361        }
362        #[cfg(feature = "tracing")]
363        tracing::warn!(target: TARGET_COPY, "CopyIn dropped without finish; connection may need recovery");
364        // Drop cannot be async. Attempt a best-effort synchronous
365        // CopyFail message so the server can recover the connection.
366        //
367        // For NativeTcpTransport (blocking TCP), this will work because
368        // the underlying TcpStream supports blocking I/O.
369        //
370        // For WASI (async-only I/O), we cannot perform I/O in Drop.
371        // The connection will be left in a broken state. Users must
372        // call .finish().await or .cancel().await explicitly.
373        //
374        // We encode the CopyFail message directly into the transport's
375        // write buffer. The next flush (or the transport's Drop) will
376        // attempt to send it.
377        self.conn
378            .cancel_copy_in_sync("CopyIn dropped without finish");
379    }
380}
381
382// ---------------------------------------------------------------------------
383// CopyOut
384// ---------------------------------------------------------------------------
385
386/// A reader for a COPY OUT operation.
387///
388/// Created via [`Connection::copy_out`]. Data is received from the server
389/// in chunks.
390#[non_exhaustive]
391pub struct CopyOut<'a> {
392    conn: &'a mut Connection,
393    format: u8,
394    column_formats: Vec<u16>,
395    done: bool,
396}
397
398impl<'a> CopyOut<'a> {
399    /// The overall format code for this COPY operation.
400    ///
401    /// `0` = text, `1` = binary.
402    pub fn format(&self) -> u8 {
403        self.format
404    }
405
406    /// Per-column format codes.
407    pub fn column_formats(&self) -> &[u16] {
408        &self.column_formats
409    }
410
411    /// Read the next chunk of COPY data.
412    ///
413    /// Returns `None` when the server has finished sending data.
414    #[must_use = "copy read errors should be checked"]
415    pub async fn read_next(&mut self) -> Result<Option<Vec<u8>>> {
416        loop {
417            let msg = self
418                .conn
419                .codec
420                .read_message(&mut self.conn.transport)
421                .await?;
422            if self.conn.handle_async_message(&msg) {
423                continue;
424            }
425            match msg {
426                BackendMessage::CopyData(body) => {
427                    return Ok(Some(body.data().to_vec()));
428                }
429                BackendMessage::CopyDone => {
430                    // Continue to read CommandComplete + ReadyForQuery
431                }
432                BackendMessage::CommandComplete(_) => {
433                    // Wait for ReadyForQuery
434                }
435                BackendMessage::ReadyForQuery(body) => {
436                    self.conn.transaction_status = TransactionStatus::from_u8(body.status())
437                        .unwrap_or(TransactionStatus::Idle);
438                    self.done = true;
439                    self.conn.state = ConnectionState::Idle;
440                    return Ok(None);
441                }
442                BackendMessage::ErrorResponse(body) => {
443                    let server_err = PgServerError::from_error_body(&body).map_err(PgError::Io)?;
444                    self.conn.read_until_ready().await?;
445                    self.done = true;
446                    self.conn.state = ConnectionState::Idle;
447                    return Err(PgError::Server(Box::new(server_err)));
448                }
449                _ => {}
450            }
451        }
452    }
453
454    /// Read all remaining COPY data into a single buffer.
455    #[must_use = "copy read errors should be checked"]
456    pub async fn read_all(&mut self) -> Result<Vec<u8>> {
457        let mut result = Vec::new();
458        while let Some(chunk) = self.read_next().await? {
459            result.extend_from_slice(&chunk);
460        }
461        Ok(result)
462    }
463
464    /// Process each chunk with a callback (streaming).
465    #[must_use = "copy read errors should be checked"]
466    pub async fn for_each<F>(&mut self, mut f: F) -> Result<()>
467    where
468        F: FnMut(&[u8]) -> Result<()>,
469    {
470        while let Some(chunk) = self.read_next().await? {
471            f(&chunk)?;
472        }
473        Ok(())
474    }
475
476    /// Process each text-format row with a callback.
477    ///
478    /// This is a convenience method for text-format COPY OUT operations.
479    /// It collects all data, splits by newlines, and calls `f` for each
480    /// row's tab-separated fields.
481    ///
482    /// # Example
483    /// ```ignore
484    /// let mut copy = conn.copy_out("COPY users TO STDOUT").await?;
485    /// copy.for_each_row(|fields| {
486    ///     println!("id={}, name={}", fields[0], fields[1]);
487    ///     Ok(())
488    /// }).await?;
489    /// ```
490    #[must_use = "copy read errors should be checked"]
491    pub async fn for_each_row<F>(&mut self, mut f: F) -> Result<()>
492    where
493        F: FnMut(&[&str]) -> Result<()>,
494    {
495        let mut buffer = String::new();
496        while let Some(chunk) = self.read_next().await? {
497            buffer.push_str(&String::from_utf8_lossy(&chunk));
498
499            // Process complete lines
500            while let Some(newline_pos) = buffer.find('\n') {
501                let line = buffer[..newline_pos].to_string();
502                buffer = buffer[newline_pos + 1..].to_string();
503
504                // Skip empty lines (trailing newline)
505                if line.is_empty() {
506                    continue;
507                }
508
509                let fields: Vec<&str> = line.split('\t').collect();
510                f(&fields)?;
511            }
512        }
513
514        // Process any remaining data without trailing newline
515        let remaining = buffer.trim_end();
516        if !remaining.is_empty() {
517            let fields: Vec<&str> = remaining.split('\t').collect();
518            f(&fields)?;
519        }
520
521        Ok(())
522    }
523
524    /// Process each CSV-format row with a callback.
525    ///
526    /// This is a convenience method for CSV-format COPY OUT operations.
527    /// It collects all data, splits by newlines, and parses each row
528    /// as a simple CSV line (handling quoted fields with the specified
529    /// delimiter and quote character).
530    ///
531    /// # Example
532    /// ```ignore
533    /// let mut copy = conn.copy_out("COPY users TO STDOUT WITH (FORMAT csv)").await?;
534    /// copy.for_each_csv_row(',', '"', |fields| {
535    ///     println!("id={}, name={}", fields[0], fields[1]);
536    ///     Ok(())
537    /// }).await?;
538    /// ```
539    #[must_use = "copy read errors should be checked"]
540    pub async fn for_each_csv_row<F>(
541        &mut self,
542        delimiter: char,
543        quote: char,
544        mut f: F,
545    ) -> Result<()>
546    where
547        F: FnMut(&[String]) -> Result<()>,
548    {
549        let mut buffer = String::new();
550        while let Some(chunk) = self.read_next().await? {
551            buffer.push_str(&String::from_utf8_lossy(&chunk));
552
553            // Process complete lines
554            while let Some(newline_pos) = buffer.find('\n') {
555                let line = buffer[..newline_pos].to_string();
556                buffer = buffer[newline_pos + 1..].to_string();
557
558                // Skip empty lines (trailing newline)
559                if line.is_empty() {
560                    continue;
561                }
562
563                let fields = parse_csv_line(&line, delimiter, quote);
564                f(&fields)?;
565            }
566        }
567
568        // Process any remaining data without trailing newline
569        let remaining = buffer.trim_end();
570        if !remaining.is_empty() {
571            let fields = parse_csv_line(remaining, delimiter, quote);
572            f(&fields)?;
573        }
574
575        Ok(())
576    }
577}
578
579impl<'a> Drop for CopyOut<'a> {
580    fn drop(&mut self) {
581        if self.done || std::thread::panicking() {
582            return;
583        }
584        #[cfg(feature = "tracing")]
585        tracing::warn!(target: TARGET_COPY, "CopyOut dropped without finish; connection may need recovery");
586        // Drop cannot be async. Attempt a best-effort synchronous drain
587        // so the server can recover the connection.
588        //
589        // For NativeTcpTransport (blocking TCP), this will work because
590        // the underlying TcpStream supports blocking I/O.
591        //
592        // For WASI (async-only I/O), we cannot perform I/O in Drop.
593        // The connection will be left in a broken state. Users must
594        // consume all data via read_next() or read_all() explicitly.
595        self.conn.drain_copy_out_sync();
596    }
597}
598
599// ---------------------------------------------------------------------------
600// Connection extensions
601// ---------------------------------------------------------------------------
602
603impl Connection {
604    /// Start a COPY IN operation.
605    ///
606    /// The `sql` should be a `COPY ... FROM STDIN` statement.
607    ///
608    /// # Example
609    /// ```ignore
610    /// let mut copy = conn.copy_in("COPY users (id, name) FROM STDIN").await?;
611    /// copy.write_row(&["1", "alice"]).await?;
612    /// copy.write_row(&["2", "bob"]).await?;
613    /// let rows = copy.finish().await?;
614    /// ```
615    #[must_use = "copy errors should be checked"]
616    pub async fn copy_in(&mut self, sql: &str) -> Result<CopyIn<'_>> {
617        #[cfg(feature = "tracing")]
618        tracing::info!(target: TARGET_COPY, direction = "in", sql_truncated = %truncate_str(sql, 200), "Starting COPY IN operation");
619        self.transition(ConnectionState::CopyIn)?;
620
621        self.codec
622            .send(
623                &mut self.transport,
624                &FrontendMessage::Query { sql: sql.into() },
625            )
626            .await?;
627
628        loop {
629            let msg = self.codec.read_message(&mut self.transport).await?;
630            if self.handle_async_message(&msg) {
631                continue;
632            }
633            match msg {
634                BackendMessage::CopyInResponse(body) => {
635                    let format = body.format();
636                    let mut column_formats = Vec::new();
637                    let mut iter = body.column_formats();
638                    while let Some(fmt) = iter.next()? {
639                        column_formats.push(fmt);
640                    }
641                    break Ok(CopyIn {
642                        conn: self,
643                        format,
644                        column_formats,
645                        done: false,
646                    });
647                }
648                BackendMessage::ErrorResponse(body) => {
649                    let server_err = PgServerError::from_error_body(&body).map_err(PgError::Io)?;
650                    self.read_until_ready().await?;
651                    self.state = ConnectionState::Idle;
652                    break Err(PgError::Server(Box::new(server_err)));
653                }
654                _ => {
655                    self.read_until_ready().await?;
656                    self.state = ConnectionState::Idle;
657                    break Err(PgError::Protocol(
658                        crate::protocol::ProtocolError::ProtocolViolation(
659                            "expected CopyInResponse after COPY query".into(),
660                        ),
661                    ));
662                }
663            }
664        }
665    }
666
667    /// Start a COPY OUT operation.
668    ///
669    /// The `sql` should be a `COPY ... TO STDOUT` statement.
670    ///
671    /// # Example
672    /// ```ignore
673    /// let mut copy = conn.copy_out("COPY users TO STDOUT").await?;
674    /// while let Some(chunk) = copy.read_next().await? {
675    ///     println!("{}", String::from_utf8_lossy(&chunk));
676    /// }
677    /// ```
678    #[must_use = "copy errors should be checked"]
679    pub async fn copy_out(&mut self, sql: &str) -> Result<CopyOut<'_>> {
680        #[cfg(feature = "tracing")]
681        tracing::info!(target: TARGET_COPY, direction = "out", sql_truncated = %truncate_str(sql, 200), "Starting COPY OUT operation");
682        self.transition(ConnectionState::CopyOut)?;
683
684        self.codec
685            .send(
686                &mut self.transport,
687                &FrontendMessage::Query { sql: sql.into() },
688            )
689            .await?;
690
691        loop {
692            let msg = self.codec.read_message(&mut self.transport).await?;
693            if self.handle_async_message(&msg) {
694                continue;
695            }
696            match msg {
697                BackendMessage::CopyOutResponse(body) => {
698                    let format = body.format();
699                    let mut column_formats = Vec::new();
700                    let mut iter = body.column_formats();
701                    while let Some(fmt) = iter.next()? {
702                        column_formats.push(fmt);
703                    }
704                    break Ok(CopyOut {
705                        conn: self,
706                        format,
707                        column_formats,
708                        done: false,
709                    });
710                }
711                BackendMessage::ErrorResponse(body) => {
712                    let server_err = PgServerError::from_error_body(&body).map_err(PgError::Io)?;
713                    self.read_until_ready().await?;
714                    self.state = ConnectionState::Idle;
715                    break Err(PgError::Server(Box::new(server_err)));
716                }
717                _ => {
718                    self.read_until_ready().await?;
719                    self.state = ConnectionState::Idle;
720                    break Err(PgError::Protocol(
721                        crate::protocol::ProtocolError::ProtocolViolation(
722                            "expected CopyOutResponse after COPY query".into(),
723                        ),
724                    ));
725                }
726            }
727        }
728    }
729}
730
731// ---------------------------------------------------------------------------
732// Unit tests
733// ---------------------------------------------------------------------------
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use crate::auth::{Codec, ServerParams};
739    use crate::config::Config;
740    use crate::connection::ConnectionState;
741    use crate::protocol::TransactionStatus;
742    use crate::transport::{BufferedTransport, ClientTransport, MockTransport, PgTransport};
743    use std::collections::VecDeque;
744
745    fn make_connection(read_data: Vec<u8>) -> Connection {
746        let transport = PgTransport::Plain(BufferedTransport::new(ClientTransport::Mock(
747            MockTransport::new(read_data),
748        )));
749        Connection {
750            transport,
751            codec: Codec::new(),
752            server_params: ServerParams::default(),
753            state: ConnectionState::Idle,
754            config: Config::new(),
755            transaction_status: TransactionStatus::Idle,
756            notification_queue: VecDeque::new(),
757            notice_handler: None,
758            statement_counter: 0,
759            needs_recovery: false,
760            health: crate::reconnect::session::ConnectionHealth::new(),
761            session_state: crate::reconnect::session::SessionState::new(),
762        }
763    }
764
765    fn build_copy_in_response(format: u8, col_formats: &[u16]) -> Vec<u8> {
766        let mut buf = vec![b'G'];
767        let mut body = Vec::new();
768        body.push(format);
769        body.extend_from_slice(&(col_formats.len() as i16).to_be_bytes());
770        for fmt in col_formats {
771            body.extend_from_slice(&fmt.to_be_bytes());
772        }
773        let len = (body.len() + 4) as i32;
774        buf.extend_from_slice(&len.to_be_bytes());
775        buf.extend_from_slice(&body);
776        buf
777    }
778
779    fn build_copy_out_response(format: u8, col_formats: &[u16]) -> Vec<u8> {
780        let mut buf = vec![b'H'];
781        let mut body = Vec::new();
782        body.push(format);
783        body.extend_from_slice(&(col_formats.len() as i16).to_be_bytes());
784        for fmt in col_formats {
785            body.extend_from_slice(&fmt.to_be_bytes());
786        }
787        let len = (body.len() + 4) as i32;
788        buf.extend_from_slice(&len.to_be_bytes());
789        buf.extend_from_slice(&body);
790        buf
791    }
792
793    fn build_copy_data(data: &[u8]) -> Vec<u8> {
794        let mut buf = vec![b'd'];
795        let len = (data.len() + 4) as i32;
796        buf.extend_from_slice(&len.to_be_bytes());
797        buf.extend_from_slice(data);
798        buf
799    }
800
801    fn build_copy_done() -> Vec<u8> {
802        vec![b'c', 0, 0, 0, 4]
803    }
804
805    fn build_command_complete_msg(tag: &str) -> Vec<u8> {
806        let mut buf = vec![b'C'];
807        let mut body = Vec::new();
808        body.extend_from_slice(tag.as_bytes());
809        body.push(0);
810        let len = (body.len() + 4) as i32;
811        buf.extend_from_slice(&len.to_be_bytes());
812        buf.extend_from_slice(&body);
813        buf
814    }
815
816    fn build_ready_for_query(status: u8) -> Vec<u8> {
817        vec![b'Z', 0, 0, 0, 5, status]
818    }
819
820    fn build_error_response(msg: &str) -> Vec<u8> {
821        let mut buf = vec![b'E'];
822        let mut body = Vec::new();
823        body.push(b'S');
824        body.extend_from_slice(b"ERROR\0");
825        body.push(b'M');
826        body.extend_from_slice(msg.as_bytes());
827        body.push(0);
828        body.push(0);
829        let len = (body.len() + 4) as i32;
830        buf.extend_from_slice(&len.to_be_bytes());
831        buf.extend_from_slice(&body);
832        buf
833    }
834
835    // -----------------------------------------------------------------------
836    // CopyFormat
837    // -----------------------------------------------------------------------
838
839    #[test]
840    fn test_copy_format_text() {
841        assert_eq!(CopyFormat::Text.to_sql_options(), "");
842    }
843
844    #[test]
845    fn test_copy_format_csv() {
846        let fmt = CopyFormat::Csv {
847            delimiter: ',',
848            null: "\\N".to_string(),
849            header: true,
850            quote: '"',
851            escape: '"',
852        };
853        let sql = fmt.to_sql_options();
854        assert!(sql.contains("FORMAT csv"));
855        assert!(sql.contains("HEADER true"));
856    }
857
858    #[test]
859    fn test_copy_format_binary() {
860        assert_eq!(CopyFormat::Binary.to_sql_options(), "WITH (FORMAT binary)");
861    }
862
863    // -----------------------------------------------------------------------
864    // CopyIn (mock transport)
865    // -----------------------------------------------------------------------
866
867    #[tokio::test]
868    async fn test_copy_in_success() {
869        let mut data = Vec::new();
870        data.extend_from_slice(&build_copy_in_response(0, &[0, 0]));
871        data.extend_from_slice(&build_command_complete_msg("COPY 2"));
872        data.extend_from_slice(&build_ready_for_query(b'I'));
873
874        let mut conn = make_connection(data);
875        let mut copy = conn.copy_in("COPY users FROM STDIN").await.unwrap();
876        assert_eq!(copy.format(), 0);
877        assert_eq!(copy.column_formats(), &[0, 0]);
878
879        copy.write_row(&["1", "alice"]).await.unwrap();
880        copy.write_row(&["2", "bob"]).await.unwrap();
881
882        let rows = copy.finish().await.unwrap();
883        assert_eq!(rows, 2);
884        assert_eq!(conn.state, ConnectionState::Idle);
885    }
886
887    #[tokio::test]
888    async fn test_copy_in_cancel() {
889        let mut data = Vec::new();
890        data.extend_from_slice(&build_copy_in_response(0, &[]));
891        data.extend_from_slice(&build_command_complete_msg("ROLLBACK"));
892        data.extend_from_slice(&build_ready_for_query(b'I'));
893
894        let mut conn = make_connection(data);
895        let copy = conn.copy_in("COPY users FROM STDIN").await.unwrap();
896        copy.cancel("user requested").await.unwrap();
897        assert_eq!(conn.state, ConnectionState::Idle);
898
899        // Verify CopyFail was sent
900        if let PgTransport::Plain(buf) = &conn.transport {
901            let mock = buf.inner();
902            #[allow(unreachable_patterns)]
903            let m = match mock {
904                ClientTransport::Mock(m) => m,
905                _ => panic!("expected mock transport in unit test"),
906            };
907            assert!(m.written().windows(1).any(|w| w[0] == b'f'));
908        }
909    }
910
911    #[tokio::test]
912    async fn test_copy_in_error_response() {
913        let mut data = Vec::new();
914        data.extend_from_slice(&build_error_response("syntax error"));
915        data.extend_from_slice(&build_ready_for_query(b'I'));
916
917        let mut conn = make_connection(data);
918        let result = conn.copy_in("COPY users FROM STDIN").await;
919        assert!(matches!(result, Err(Error::Server(_))));
920        drop(result);
921        assert_eq!(conn.state, ConnectionState::Idle);
922    }
923
924    // -----------------------------------------------------------------------
925    // CopyOut (mock transport)
926    // -----------------------------------------------------------------------
927
928    #[tokio::test]
929    async fn test_copy_out_success() {
930        let mut data = Vec::new();
931        data.extend_from_slice(&build_copy_out_response(0, &[0, 0]));
932        data.extend_from_slice(&build_copy_data(b"1\talice\n"));
933        data.extend_from_slice(&build_copy_data(b"2\tbob\n"));
934        data.extend_from_slice(&build_copy_done());
935        data.extend_from_slice(&build_command_complete_msg("COPY 2"));
936        data.extend_from_slice(&build_ready_for_query(b'I'));
937
938        let mut conn = make_connection(data);
939        let mut copy = conn.copy_out("COPY users TO STDOUT").await.unwrap();
940        assert_eq!(copy.format(), 0);
941        assert_eq!(copy.column_formats(), &[0, 0]);
942
943        let chunk1 = copy.read_next().await.unwrap().unwrap();
944        assert_eq!(chunk1, b"1\talice\n");
945
946        let chunk2 = copy.read_next().await.unwrap().unwrap();
947        assert_eq!(chunk2, b"2\tbob\n");
948
949        let done = copy.read_next().await.unwrap();
950        assert!(done.is_none());
951        drop(copy);
952        assert_eq!(conn.state, ConnectionState::Idle);
953    }
954
955    #[tokio::test]
956    async fn test_copy_out_read_all() {
957        let mut data = Vec::new();
958        data.extend_from_slice(&build_copy_out_response(0, &[]));
959        data.extend_from_slice(&build_copy_data(b"hello"));
960        data.extend_from_slice(&build_copy_data(b" world"));
961        data.extend_from_slice(&build_copy_done());
962        data.extend_from_slice(&build_command_complete_msg("COPY 2"));
963        data.extend_from_slice(&build_ready_for_query(b'I'));
964
965        let mut conn = make_connection(data);
966        let mut copy = conn.copy_out("COPY users TO STDOUT").await.unwrap();
967        let all = copy.read_all().await.unwrap();
968        assert_eq!(all, b"hello world");
969    }
970
971    #[tokio::test]
972    async fn test_copy_out_for_each() {
973        let mut data = Vec::new();
974        data.extend_from_slice(&build_copy_out_response(0, &[]));
975        data.extend_from_slice(&build_copy_data(b"a"));
976        data.extend_from_slice(&build_copy_data(b"b"));
977        data.extend_from_slice(&build_copy_done());
978        data.extend_from_slice(&build_command_complete_msg("COPY 2"));
979        data.extend_from_slice(&build_ready_for_query(b'I'));
980
981        let mut conn = make_connection(data);
982        let mut copy = conn.copy_out("COPY users TO STDOUT").await.unwrap();
983        let mut acc = Vec::new();
984        copy.for_each(|chunk| {
985            acc.extend_from_slice(chunk);
986            Ok(())
987        })
988        .await
989        .unwrap();
990        assert_eq!(acc, b"ab");
991    }
992
993    #[tokio::test]
994    async fn test_copy_out_error_response() {
995        let mut data = Vec::new();
996        data.extend_from_slice(&build_error_response("relation does not exist"));
997        data.extend_from_slice(&build_ready_for_query(b'I'));
998
999        let mut conn = make_connection(data);
1000        let result = conn.copy_out("COPY missing TO STDOUT").await;
1001        assert!(matches!(result, Err(Error::Server(_))));
1002        drop(result);
1003        assert_eq!(conn.state, ConnectionState::Idle);
1004    }
1005
1006    // -----------------------------------------------------------------------
1007    // CSV parsing
1008    // -----------------------------------------------------------------------
1009
1010    #[test]
1011    fn test_parse_csv_line_simple() {
1012        let fields = super::parse_csv_line("1,alice,hello", ',', '"');
1013        assert_eq!(fields, vec!["1", "alice", "hello"]);
1014    }
1015
1016    #[test]
1017    fn test_parse_csv_line_quoted() {
1018        let fields = super::parse_csv_line("1,\"alice\",\"hello world\"", ',', '"');
1019        assert_eq!(fields, vec!["1", "alice", "hello world"]);
1020    }
1021
1022    #[test]
1023    fn test_parse_csv_line_escaped_quote() {
1024        let fields = super::parse_csv_line("1,\"says \"\"hi\"\"\",ok", ',', '"');
1025        assert_eq!(fields, vec!["1", "says \"hi\"", "ok"]);
1026    }
1027
1028    #[test]
1029    fn test_parse_csv_line_empty_fields() {
1030        let fields = super::parse_csv_line("1,,3", ',', '"');
1031        assert_eq!(fields, vec!["1", "", "3"]);
1032    }
1033
1034    #[test]
1035    fn test_parse_csv_line_single_field() {
1036        let fields = super::parse_csv_line("hello", ',', '"');
1037        assert_eq!(fields, vec!["hello"]);
1038    }
1039
1040    #[test]
1041    fn test_parse_csv_line_delimiter_in_quotes() {
1042        let fields = super::parse_csv_line("1,\"a, b, c\",3", ',', '"');
1043        assert_eq!(fields, vec!["1", "a, b, c", "3"]);
1044    }
1045
1046    #[test]
1047    fn test_parse_csv_line_tab_delimiter() {
1048        let fields = super::parse_csv_line("1\talice\thello", '\t', '"');
1049        assert_eq!(fields, vec!["1", "alice", "hello"]);
1050    }
1051
1052    // -----------------------------------------------------------------------
1053    // write_csv_row_with_null (mock transport)
1054    // -----------------------------------------------------------------------
1055
1056    #[tokio::test]
1057    async fn test_write_csv_row_with_null() {
1058        let mut data = Vec::new();
1059        data.extend_from_slice(&build_copy_in_response(0, &[]));
1060        data.extend_from_slice(&build_command_complete_msg("COPY 3"));
1061        data.extend_from_slice(&build_ready_for_query(b'I'));
1062
1063        let mut conn = make_connection(data);
1064        let mut copy = conn.copy_in("COPY t FROM STDIN").await.unwrap();
1065
1066        // Row with all values
1067        copy.write_csv_row_with_null(&[Some("1"), Some("alice"), Some("hello")], ',', '"', "")
1068            .await
1069            .unwrap();
1070
1071        // Row with NULL
1072        copy.write_csv_row_with_null(&[Some("2"), Some("bob"), None], ',', '"', "")
1073            .await
1074            .unwrap();
1075
1076        // Row with custom NULL string
1077        copy.write_csv_row_with_null(&[Some("3"), None, Some("world")], ',', '"', "\\N")
1078            .await
1079            .unwrap();
1080
1081        let rows = copy.finish().await.unwrap();
1082        assert_eq!(rows, 3);
1083
1084        // Verify the written data
1085        if let PgTransport::Plain(buf) = &conn.transport {
1086            let mock = buf.inner();
1087            #[allow(unreachable_patterns)]
1088            let m = match mock {
1089                ClientTransport::Mock(m) => m,
1090                _ => panic!("expected mock transport in unit test"),
1091            };
1092            let written = m.written();
1093            // Find CopyData messages (type 'd')
1094            let mut copy_data_parts: Vec<Vec<u8>> = Vec::new();
1095            let mut i = 0;
1096            while i < written.len() {
1097                if written[i] == b'd' {
1098                    // CopyData message
1099                    let len = i32::from_be_bytes([
1100                        written[i + 1],
1101                        written[i + 2],
1102                        written[i + 3],
1103                        written[i + 4],
1104                    ]);
1105                    let data_len = len as usize - 4;
1106                    copy_data_parts.push(written[i + 5..i + 5 + data_len].to_vec());
1107                    i += 5 + data_len;
1108                } else {
1109                    i += 1;
1110                }
1111            }
1112
1113            // Check row 1: 1,alice,hello
1114            assert_eq!(&copy_data_parts[0], b"1,alice,hello\n");
1115            // Check row 2: 2,bob, (empty string for NULL)
1116            assert_eq!(&copy_data_parts[1], b"2,bob,\n");
1117            // Check row 3: 3,\N,world (custom NULL string)
1118            assert_eq!(&copy_data_parts[2], b"3,\\N,world\n");
1119        }
1120    }
1121
1122    // -----------------------------------------------------------------------
1123    // CopyOut for_each_row (mock transport)
1124    // -----------------------------------------------------------------------
1125
1126    #[tokio::test]
1127    async fn test_copy_out_for_each_row() {
1128        let mut data = Vec::new();
1129        data.extend_from_slice(&build_copy_out_response(0, &[]));
1130        data.extend_from_slice(&build_copy_data(b"1\talice\n2\tbob\n3\tcharlie\n"));
1131        data.extend_from_slice(&build_copy_done());
1132        data.extend_from_slice(&build_command_complete_msg("COPY 3"));
1133        data.extend_from_slice(&build_ready_for_query(b'I'));
1134
1135        let mut conn = make_connection(data);
1136        let mut copy = conn.copy_out("COPY users TO STDOUT").await.unwrap();
1137
1138        let mut rows: Vec<Vec<String>> = Vec::new();
1139        copy.for_each_row(|fields| {
1140            rows.push(fields.iter().map(|f| f.to_string()).collect());
1141            Ok(())
1142        })
1143        .await
1144        .unwrap();
1145
1146        assert_eq!(rows.len(), 3);
1147        assert_eq!(rows[0], vec!["1", "alice"]);
1148        assert_eq!(rows[1], vec!["2", "bob"]);
1149        assert_eq!(rows[2], vec!["3", "charlie"]);
1150    }
1151
1152    #[tokio::test]
1153    async fn test_copy_out_for_each_csv_row() {
1154        let mut data = Vec::new();
1155        data.extend_from_slice(&build_copy_out_response(0, &[]));
1156        data.extend_from_slice(&build_copy_data(b"1,alice\n2,\"bob\"\n3,charlie\n"));
1157        data.extend_from_slice(&build_copy_done());
1158        data.extend_from_slice(&build_command_complete_msg("COPY 3"));
1159        data.extend_from_slice(&build_ready_for_query(b'I'));
1160
1161        let mut conn = make_connection(data);
1162        let mut copy = conn.copy_out("COPY users TO STDOUT").await.unwrap();
1163
1164        let mut rows: Vec<Vec<String>> = Vec::new();
1165        copy.for_each_csv_row(',', '"', |fields| {
1166            rows.push(fields.to_vec());
1167            Ok(())
1168        })
1169        .await
1170        .unwrap();
1171
1172        assert_eq!(rows.len(), 3);
1173        assert_eq!(rows[0], vec!["1", "alice"]);
1174        assert_eq!(rows[1], vec!["2", "bob"]);
1175        assert_eq!(rows[2], vec!["3", "charlie"]);
1176    }
1177}