1use super::{
5 PgConnection, PgError, PgResult, is_ignorable_session_message, parse_affected_rows,
6 unexpected_backend_message,
7};
8use crate::protocol::{AstEncoder, BackendMessage, PgEncoder};
9use bytes::BytesMut;
10use qail_core::ast::{Action, Qail};
11use std::future::Future;
12
13pub(crate) fn quote_copy_column_ident(ident: &str) -> PgResult<String> {
15 if ident.is_empty() {
16 return Err(PgError::Query(
17 "COPY column identifier is empty".to_string(),
18 ));
19 }
20 if ident.contains('\0') {
21 return Err(PgError::Query(
22 "COPY column identifier contains NUL byte".to_string(),
23 ));
24 }
25 Ok(format!("\"{}\"", ident.replace('"', "\"\"")))
26}
27
28pub(crate) fn quote_copy_table_ref(table: &str) -> PgResult<String> {
30 if table.is_empty() {
31 return Err(PgError::Query("COPY table identifier is empty".to_string()));
32 }
33 if table.contains('\0') {
34 return Err(PgError::Query(
35 "COPY table identifier contains NUL byte".to_string(),
36 ));
37 }
38
39 table
40 .split('.')
41 .map(|part| {
42 let part = part.trim();
43 if part.is_empty() {
44 return Err(PgError::Query(
45 "COPY table identifier contains an empty path segment".to_string(),
46 ));
47 }
48 quote_copy_column_ident(part)
49 })
50 .collect::<PgResult<Vec<_>>>()
51 .map(|parts| parts.join("."))
52}
53
54fn parse_copy_text_row(line: &[u8]) -> PgResult<Vec<String>> {
55 let line = if line.ends_with(b"\r") {
56 &line[..line.len().saturating_sub(1)]
57 } else {
58 line
59 };
60
61 let mut fields = Vec::new();
62 let mut start = 0;
63 for (idx, byte) in line.iter().enumerate() {
64 if *byte == b'\t' {
65 fields.push(decode_copy_text_field(&line[start..idx])?);
66 start = idx + 1;
67 }
68 }
69 fields.push(decode_copy_text_field(&line[start..])?);
70 Ok(fields)
71}
72
73fn decode_copy_text_field(field: &[u8]) -> PgResult<String> {
74 if field == b"\\N" {
75 return Err(PgError::Protocol(
76 "COPY text NULL cannot be represented by Vec<String>; use copy_export_stream_raw for nullable exports"
77 .to_string(),
78 ));
79 }
80
81 let mut out = Vec::with_capacity(field.len());
82 let mut idx = 0;
83 while idx < field.len() {
84 if field[idx] != b'\\' {
85 out.push(field[idx]);
86 idx += 1;
87 continue;
88 }
89
90 let Some(&escaped) = field.get(idx + 1) else {
91 return Err(PgError::Protocol(
92 "COPY text field ends with incomplete backslash escape".to_string(),
93 ));
94 };
95
96 match escaped {
97 b'b' => {
98 out.push(0x08);
99 idx += 2;
100 }
101 b'f' => {
102 out.push(0x0c);
103 idx += 2;
104 }
105 b'n' => {
106 out.push(b'\n');
107 idx += 2;
108 }
109 b'r' => {
110 out.push(b'\r');
111 idx += 2;
112 }
113 b't' => {
114 out.push(b'\t');
115 idx += 2;
116 }
117 b'v' => {
118 out.push(0x0b);
119 idx += 2;
120 }
121 b'\\' => {
122 out.push(b'\\');
123 idx += 2;
124 }
125 b'0'..=b'7' => {
126 let mut value = 0u16;
127 let mut next = idx + 1;
128 for _ in 0..3 {
129 let Some(&digit) = field.get(next) else {
130 break;
131 };
132 if !(b'0'..=b'7').contains(&digit) {
133 break;
134 }
135 value = (value * 8) + u16::from(digit - b'0');
136 next += 1;
137 }
138 if value > u16::from(u8::MAX) {
139 return Err(PgError::Protocol(format!(
140 "COPY text octal escape is out of byte range: \\{:o}",
141 value
142 )));
143 }
144 out.push(value as u8);
145 idx = next;
146 }
147 b'x' => {
148 let mut value = 0u8;
149 let mut next = idx + 2;
150 let mut digits = 0;
151 while digits < 2 {
152 let Some(&digit) = field.get(next) else {
153 break;
154 };
155 let Some(nibble) = hex_nibble(digit) else {
156 break;
157 };
158 value = (value << 4) | nibble;
159 next += 1;
160 digits += 1;
161 }
162 if digits == 0 {
163 return Err(PgError::Protocol(
164 "COPY text hex escape requires at least one hex digit".to_string(),
165 ));
166 } else {
167 out.push(value);
168 idx = next;
169 }
170 }
171 other => {
172 out.push(other);
173 idx += 2;
174 }
175 }
176 }
177
178 String::from_utf8(out)
179 .map_err(|e| PgError::Protocol(format!("COPY text field is not valid UTF-8: {}", e)))
180}
181
182fn hex_nibble(byte: u8) -> Option<u8> {
183 match byte {
184 b'0'..=b'9' => Some(byte - b'0'),
185 b'a'..=b'f' => Some(byte - b'a' + 10),
186 b'A'..=b'F' => Some(byte - b'A' + 10),
187 _ => None,
188 }
189}
190
191#[inline]
192fn return_with_desync<T>(conn: &mut PgConnection, err: PgError) -> PgResult<T> {
193 if matches!(
194 err,
195 PgError::Protocol(_) | PgError::Connection(_) | PgError::Timeout(_)
196 ) {
197 conn.mark_io_desynced();
198 }
199 Err(err)
200}
201
202fn encode_copy_export_sql(cmd: &Qail) -> PgResult<String> {
203 if cmd.action != Action::Export {
204 return Err(PgError::Query(
205 "copy_export requires Qail::Export action".to_string(),
206 ));
207 }
208
209 let (sql, params) =
210 AstEncoder::encode_cmd_sql(cmd).map_err(|e| PgError::Encode(e.to_string()))?;
211 if !params.is_empty() {
212 return Err(PgError::Encode(format!(
213 "copy_export cannot encode parameterized export with {} bind parameter(s); use an unfiltered export, a prefiltered database view, or a raw COPY statement with trusted SQL",
214 params.len()
215 )));
216 }
217
218 Ok(sql)
219}
220
221fn drain_copy_text_rows<F>(pending: &mut Vec<u8>, chunk: &[u8], on_row: &mut F) -> PgResult<()>
222where
223 F: FnMut(Vec<String>) -> PgResult<()>,
224{
225 pending.extend_from_slice(chunk);
226 while let Some(pos) = pending.iter().position(|&b| b == b'\n') {
227 let line = pending[..pos].to_vec();
228 pending.drain(..=pos);
229 let row = parse_copy_text_row(&line)?;
230 on_row(row)?;
231 }
232 Ok(())
233}
234
235fn flush_pending_copy_text_row(pending: &[u8]) -> PgResult<()> {
236 if pending.is_empty() {
237 return Ok(());
238 }
239 Err(PgError::Protocol(
240 "COPY text stream ended with a truncated row without final newline".to_string(),
241 ))
242}
243
244impl PgConnection {
245 pub(crate) async fn copy_in_fast(
249 &mut self,
250 table: &str,
251 columns: &[String],
252 rows: &[Vec<qail_core::ast::Value>],
253 ) -> PgResult<u64> {
254 use crate::protocol::try_encode_copy_batch;
255
256 let cols: Vec<String> = columns
257 .iter()
258 .map(|c| quote_copy_column_ident(c))
259 .collect::<PgResult<_>>()?;
260 let sql = format!(
261 "COPY {} ({}) FROM STDIN",
262 quote_copy_table_ref(table)?,
263 cols.join(", ")
264 );
265
266 let batch_data = try_encode_copy_batch(rows)?;
269
270 let bytes = PgEncoder::try_encode_query_string(&sql)?;
272 self.send_bytes(&bytes).await?;
273
274 let mut startup_error: Option<PgError> = None;
276 loop {
277 let msg = self.recv().await?;
278 match msg {
279 BackendMessage::CopyInResponse { .. } => {
280 if let Some(err) = startup_error {
281 return return_with_desync(self, err);
282 }
283 break;
284 }
285 BackendMessage::ReadyForQuery(_) => {
286 return return_with_desync(
287 self,
288 startup_error.unwrap_or_else(|| {
289 PgError::Protocol(
290 "COPY IN failed before CopyInResponse (unexpected ReadyForQuery)"
291 .to_string(),
292 )
293 }),
294 );
295 }
296 BackendMessage::ErrorResponse(err) => {
297 if startup_error.is_none() {
298 startup_error = Some(PgError::QueryServer(err.into()));
299 }
300 }
301 msg if is_ignorable_session_message(&msg) => {}
302 other => {
303 return return_with_desync(
304 self,
305 unexpected_backend_message("copy-in startup", &other),
306 );
307 }
308 }
309 }
310
311 self.send_copy_data(&batch_data).await?;
313
314 self.send_copy_done().await?;
316
317 let mut affected = 0u64;
319 let mut final_error: Option<PgError> = None;
320 let mut saw_command_complete = false;
321 loop {
322 let msg = self.recv().await?;
323 match msg {
324 BackendMessage::CommandComplete(tag) => {
325 if saw_command_complete {
326 return return_with_desync(
327 self,
328 PgError::Protocol(
329 "COPY IN received duplicate CommandComplete".to_string(),
330 ),
331 );
332 }
333 saw_command_complete = true;
334 if final_error.is_none() {
335 match parse_affected_rows(&tag) {
336 Ok(parsed) => affected = parsed,
337 Err(err) => return return_with_desync(self, err),
338 }
339 }
340 }
341 BackendMessage::ReadyForQuery(_) => {
342 if let Some(err) = final_error {
343 return Err(err);
344 }
345 if !saw_command_complete {
346 return return_with_desync(
347 self,
348 PgError::Protocol(
349 "COPY IN completion missing CommandComplete before ReadyForQuery"
350 .to_string(),
351 ),
352 );
353 }
354 return Ok(affected);
355 }
356 BackendMessage::ErrorResponse(err) => {
357 if final_error.is_none() {
358 final_error = Some(PgError::QueryServer(err.into()));
359 }
360 }
361 msg if is_ignorable_session_message(&msg) => {}
362 other => {
363 return return_with_desync(
364 self,
365 unexpected_backend_message("copy-in completion", &other),
366 );
367 }
368 }
369 }
370 }
371
372 pub async fn copy_in_raw(
379 &mut self,
380 table: &str,
381 columns: &[String],
382 data: &[u8],
383 ) -> PgResult<u64> {
384 let cols: Vec<String> = columns
385 .iter()
386 .map(|c| quote_copy_column_ident(c))
387 .collect::<PgResult<_>>()?;
388 let sql = format!(
389 "COPY {} ({}) FROM STDIN",
390 quote_copy_table_ref(table)?,
391 cols.join(", ")
392 );
393
394 let bytes = PgEncoder::try_encode_query_string(&sql)?;
396 self.send_bytes(&bytes).await?;
397
398 let mut startup_error: Option<PgError> = None;
400 loop {
401 let msg = self.recv().await?;
402 match msg {
403 BackendMessage::CopyInResponse { .. } => {
404 if let Some(err) = startup_error {
405 return return_with_desync(self, err);
406 }
407 break;
408 }
409 BackendMessage::ReadyForQuery(_) => {
410 return return_with_desync(
411 self,
412 startup_error.unwrap_or_else(|| {
413 PgError::Protocol(
414 "COPY IN failed before CopyInResponse (unexpected ReadyForQuery)"
415 .to_string(),
416 )
417 }),
418 );
419 }
420 BackendMessage::ErrorResponse(err) => {
421 if startup_error.is_none() {
422 startup_error = Some(PgError::QueryServer(err.into()));
423 }
424 }
425 msg if is_ignorable_session_message(&msg) => {}
426 other => {
427 return return_with_desync(
428 self,
429 unexpected_backend_message("copy-in raw startup", &other),
430 );
431 }
432 }
433 }
434
435 self.send_copy_data(data).await?;
437
438 self.send_copy_done().await?;
440
441 let mut affected = 0u64;
443 let mut final_error: Option<PgError> = None;
444 let mut saw_command_complete = false;
445 loop {
446 let msg = self.recv().await?;
447 match msg {
448 BackendMessage::CommandComplete(tag) => {
449 if saw_command_complete {
450 return return_with_desync(
451 self,
452 PgError::Protocol(
453 "COPY IN raw received duplicate CommandComplete".to_string(),
454 ),
455 );
456 }
457 saw_command_complete = true;
458 if final_error.is_none() {
459 match parse_affected_rows(&tag) {
460 Ok(parsed) => affected = parsed,
461 Err(err) => return return_with_desync(self, err),
462 }
463 }
464 }
465 BackendMessage::ReadyForQuery(_) => {
466 if let Some(err) = final_error {
467 return Err(err);
468 }
469 if !saw_command_complete {
470 return return_with_desync(
471 self,
472 PgError::Protocol(
473 "COPY IN raw completion missing CommandComplete before ReadyForQuery"
474 .to_string(),
475 ),
476 );
477 }
478 return Ok(affected);
479 }
480 BackendMessage::ErrorResponse(err) => {
481 if final_error.is_none() {
482 final_error = Some(PgError::QueryServer(err.into()));
483 }
484 }
485 msg if is_ignorable_session_message(&msg) => {}
486 other => {
487 return return_with_desync(
488 self,
489 unexpected_backend_message("copy-in raw completion", &other),
490 );
491 }
492 }
493 }
494 }
495
496 pub(crate) async fn send_copy_data(&mut self, data: &[u8]) -> PgResult<()> {
498 let total_len = data
499 .len()
500 .checked_add(4)
501 .ok_or_else(|| PgError::Protocol("CopyData frame length overflow".to_string()))?;
502 let len = i32::try_from(total_len)
503 .map_err(|_| PgError::Protocol("CopyData frame exceeds i32::MAX".to_string()))?;
504
505 let mut buf = BytesMut::with_capacity(1 + 4 + data.len());
507 buf.extend_from_slice(b"d");
508 buf.extend_from_slice(&len.to_be_bytes());
509 buf.extend_from_slice(data);
510 self.send_bytes(&buf).await?;
511 Ok(())
512 }
513
514 async fn send_copy_done(&mut self) -> PgResult<()> {
515 self.send_bytes(&[b'c', 0, 0, 0, 4]).await?;
517 Ok(())
518 }
519
520 async fn start_copy_out(&mut self, sql: &str, context: &str) -> PgResult<()> {
521 let bytes = PgEncoder::try_encode_query_string(sql)?;
522 self.send_bytes(&bytes).await?;
523
524 let mut startup_error: Option<PgError> = None;
525 loop {
526 let msg = self.recv().await?;
527 match msg {
528 BackendMessage::CopyOutResponse { .. } => {
529 if let Some(err) = startup_error {
530 return return_with_desync(self, err);
531 }
532 return Ok(());
533 }
534 BackendMessage::ReadyForQuery(_) => {
535 return return_with_desync(
536 self,
537 startup_error.unwrap_or_else(|| {
538 PgError::Protocol(format!(
539 "{} failed before CopyOutResponse (unexpected ReadyForQuery)",
540 context
541 ))
542 }),
543 );
544 }
545 BackendMessage::ErrorResponse(err) => {
546 if startup_error.is_none() {
547 startup_error = Some(PgError::QueryServer(err.into()));
548 }
549 }
550 msg if is_ignorable_session_message(&msg) => {}
551 other => {
552 return return_with_desync(self, unexpected_backend_message(context, &other));
553 }
554 }
555 }
556 }
557
558 async fn stream_copy_out_chunks<F, Fut>(
559 &mut self,
560 context: &str,
561 mut on_chunk: F,
562 ) -> PgResult<()>
563 where
564 F: FnMut(Vec<u8>) -> Fut,
565 Fut: Future<Output = PgResult<()>>,
566 {
567 let mut stream_error: Option<PgError> = None;
568 let mut callback_error: Option<PgError> = None;
569 let mut saw_copy_done = false;
570 let mut saw_command_complete = false;
571
572 loop {
573 let msg = self.recv().await?;
574 match msg {
575 BackendMessage::CopyData(chunk) => {
576 if saw_copy_done {
577 return return_with_desync(
578 self,
579 PgError::Protocol(format!(
580 "{} received CopyData after CopyDone",
581 context
582 )),
583 );
584 }
585 if stream_error.is_none()
586 && callback_error.is_none()
587 && let Err(e) = on_chunk(chunk).await
588 {
589 callback_error = Some(e);
590 }
591 }
592 BackendMessage::CopyDone => {
593 if saw_copy_done {
594 return return_with_desync(
595 self,
596 PgError::Protocol(format!("{} received duplicate CopyDone", context)),
597 );
598 }
599 saw_copy_done = true;
600 }
601 BackendMessage::CommandComplete(_) => {
602 if !saw_copy_done {
603 return return_with_desync(
604 self,
605 PgError::Protocol(format!(
606 "{} received CommandComplete before CopyDone",
607 context
608 )),
609 );
610 }
611 if saw_command_complete {
612 return return_with_desync(
613 self,
614 PgError::Protocol(format!(
615 "{} received duplicate CommandComplete",
616 context
617 )),
618 );
619 }
620 saw_command_complete = true;
621 }
622 BackendMessage::ReadyForQuery(_) => {
623 if let Some(err) = stream_error {
624 return Err(err);
625 }
626 if let Some(err) = callback_error {
627 return Err(err);
628 }
629 if !saw_copy_done {
630 return return_with_desync(
631 self,
632 PgError::Protocol(format!(
633 "{} missing CopyDone before ReadyForQuery",
634 context
635 )),
636 );
637 }
638 if !saw_command_complete {
639 return return_with_desync(
640 self,
641 PgError::Protocol(format!(
642 "{} missing CommandComplete before ReadyForQuery",
643 context
644 )),
645 );
646 }
647 return Ok(());
648 }
649 BackendMessage::ErrorResponse(err) => {
650 if stream_error.is_none() {
651 stream_error = Some(PgError::QueryServer(err.into()));
652 }
653 }
654 msg if is_ignorable_session_message(&msg) => {}
655 other => {
656 return return_with_desync(self, unexpected_backend_message(context, &other));
657 }
658 }
659 }
660 }
661
662 pub async fn copy_export(&mut self, cmd: &Qail) -> PgResult<Vec<Vec<String>>> {
671 let mut rows = Vec::new();
672 self.copy_export_stream_rows(cmd, |row| {
673 rows.push(row);
674 Ok(())
675 })
676 .await?;
677 Ok(rows)
678 }
679
680 pub async fn copy_export_stream_raw<F, Fut>(&mut self, cmd: &Qail, on_chunk: F) -> PgResult<()>
685 where
686 F: FnMut(Vec<u8>) -> Fut,
687 Fut: Future<Output = PgResult<()>>,
688 {
689 let sql = encode_copy_export_sql(cmd)?;
690
691 self.copy_out_raw_stream(&sql, on_chunk).await
692 }
693
694 pub async fn copy_export_stream_rows<F>(&mut self, cmd: &Qail, mut on_row: F) -> PgResult<()>
699 where
700 F: FnMut(Vec<String>) -> PgResult<()>,
701 {
702 let mut pending = Vec::new();
703 self.copy_export_stream_raw(cmd, |chunk| {
704 let res = drain_copy_text_rows(&mut pending, &chunk, &mut on_row);
705 std::future::ready(res)
706 })
707 .await?;
708 flush_pending_copy_text_row(&pending)
709 }
710
711 pub(crate) async fn copy_out_raw(&mut self, sql: &str) -> PgResult<Vec<u8>> {
719 let mut data = Vec::new();
720 self.copy_out_raw_stream(sql, |chunk| {
721 data.extend_from_slice(&chunk);
722 std::future::ready(Ok(()))
723 })
724 .await?;
725 Ok(data)
726 }
727
728 pub(crate) async fn copy_out_raw_stream<F, Fut>(
733 &mut self,
734 sql: &str,
735 on_chunk: F,
736 ) -> PgResult<()>
737 where
738 F: FnMut(Vec<u8>) -> Fut,
739 Fut: Future<Output = PgResult<()>>,
740 {
741 self.start_copy_out(sql, "copy-out raw startup").await?;
742 self.stream_copy_out_chunks("copy-out raw stream", on_chunk)
743 .await
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::{
750 drain_copy_text_rows, encode_copy_export_sql, flush_pending_copy_text_row,
751 parse_copy_text_row, quote_copy_column_ident, quote_copy_table_ref, return_with_desync,
752 };
753 use crate::driver::{PgConnection, PgError, PgResult};
754 use qail_core::ast::{Operator, Qail};
755
756 #[cfg(unix)]
757 fn test_conn() -> PgConnection {
758 use crate::driver::connection::StatementCache;
759 use crate::driver::stream::PgStream;
760 use bytes::BytesMut;
761 use std::collections::{HashMap, VecDeque};
762 use std::num::NonZeroUsize;
763 use tokio::net::UnixStream;
764
765 let (unix_stream, _peer) = UnixStream::pair().expect("unix stream pair");
766 PgConnection {
767 stream: PgStream::Unix(unix_stream),
768 buffer: BytesMut::with_capacity(1024),
769 write_buf: BytesMut::with_capacity(1024),
770 sql_buf: BytesMut::with_capacity(256),
771 params_buf: Vec::new(),
772 prepared_statements: HashMap::new(),
773 stmt_cache: StatementCache::new(NonZeroUsize::new(2).expect("non-zero")),
774 column_info_cache: HashMap::new(),
775 process_id: 0,
776 cancel_key_bytes: Vec::new(),
777 requested_protocol_minor: PgConnection::default_protocol_minor(),
778 negotiated_protocol_minor: PgConnection::default_protocol_minor(),
779 notifications: VecDeque::new(),
780 replication_stream_active: false,
781 replication_mode_enabled: false,
782 last_replication_wal_end: None,
783 io_desynced: false,
784 pending_statement_closes: Vec::new(),
785 draining_statement_closes: false,
786 }
787 }
788
789 #[test]
790 fn parse_copy_text_row_splits_tabs() {
791 let row = parse_copy_text_row(b"a\tb\tc").unwrap();
792 assert_eq!(row, vec!["a", "b", "c"]);
793 }
794
795 #[test]
796 fn parse_copy_text_row_trims_cr() {
797 let row = parse_copy_text_row(b"a\tb\r").unwrap();
798 assert_eq!(row, vec!["a", "b"]);
799 }
800
801 #[test]
802 fn parse_copy_text_row_unescapes_copy_text_values() {
803 let row = parse_copy_text_row(b"a\\tb\tline\\nnext\tc\\\\d").unwrap();
804 assert_eq!(row, vec!["a\tb", "line\nnext", "c\\d"]);
805 }
806
807 #[test]
808 fn parse_copy_text_row_rejects_copy_null_marker() {
809 let err = parse_copy_text_row(b"a\t\\N\tb").expect_err("COPY NULL must not be lossy");
810 assert!(
811 err.to_string()
812 .contains("COPY text NULL cannot be represented"),
813 "{err}"
814 );
815 }
816
817 #[test]
818 fn parse_copy_text_row_rejects_invalid_utf8() {
819 let err = parse_copy_text_row(&[0xff]).expect_err("invalid UTF-8 must fail");
820 assert!(
821 err.to_string()
822 .contains("COPY text field is not valid UTF-8")
823 );
824 }
825
826 #[test]
827 fn parse_copy_text_row_rejects_incomplete_escape() {
828 let err = parse_copy_text_row(b"bad\\").expect_err("trailing backslash must fail");
829 assert!(err.to_string().contains("incomplete backslash escape"));
830 }
831
832 #[test]
833 fn parse_copy_text_row_rejects_out_of_range_octal_escape() {
834 let err = parse_copy_text_row(br"\400").expect_err("octal escape > 377 must fail");
835 assert!(err.to_string().contains("out of byte range"));
836 }
837
838 #[test]
839 fn parse_copy_text_row_rejects_hex_escape_without_digits() {
840 let err = parse_copy_text_row(br"\xG").expect_err("hex escape without digits must fail");
841 assert!(err.to_string().contains("hex escape requires"));
842 }
843
844 #[test]
845 fn copy_table_quoting_preserves_schema_qualification() {
846 assert_eq!(
847 quote_copy_table_ref("tenant_a.users").unwrap(),
848 "\"tenant_a\".\"users\""
849 );
850 }
851
852 #[test]
853 fn copy_identifier_quoting_rejects_nul_bytes() {
854 assert!(quote_copy_table_ref("tenant\0.users").is_err());
855 assert!(quote_copy_column_ident("name\0").is_err());
856 }
857
858 #[test]
859 fn copy_export_rejects_parameterized_ast_before_streaming() {
860 let cmd = Qail::export("users").filter("active", Operator::Eq, true);
861 let err = encode_copy_export_sql(&cmd).expect_err("bind params cannot be ignored");
862
863 assert!(matches!(err, PgError::Encode(msg) if msg.contains("parameterized export")));
864 }
865
866 #[cfg(unix)]
867 #[tokio::test]
868 async fn copy_return_with_desync_marks_protocol_error() {
869 let mut conn = test_conn();
870
871 let err = return_with_desync::<()>(
872 &mut conn,
873 PgError::Protocol("copy protocol ordering broke".to_string()),
874 )
875 .expect_err("protocol error must be returned");
876
877 assert!(err.to_string().contains("copy protocol ordering broke"));
878 assert!(conn.is_io_desynced());
879 }
880
881 #[test]
882 fn drain_copy_text_rows_handles_chunk_boundaries() {
883 let mut pending = Vec::new();
884 let mut rows: Vec<Vec<String>> = Vec::new();
885
886 drain_copy_text_rows(&mut pending, b"a\tb\nc", &mut |row: Vec<String>| {
887 rows.push(row);
888 Ok(())
889 })
890 .unwrap();
891 assert_eq!(rows, vec![vec!["a".to_string(), "b".to_string()]]);
892 assert_eq!(pending, b"c");
893
894 drain_copy_text_rows(&mut pending, b"\td\n", &mut |row: Vec<String>| {
895 rows.push(row);
896 Ok(())
897 })
898 .unwrap();
899 assert_eq!(
900 rows,
901 vec![
902 vec!["a".to_string(), "b".to_string()],
903 vec!["c".to_string(), "d".to_string()]
904 ]
905 );
906 assert!(pending.is_empty());
907 }
908
909 #[test]
910 fn flush_pending_copy_text_row_rejects_final_partial_line() {
911 let pending = b"x\ty".to_vec();
912 let err = flush_pending_copy_text_row(&pending)
913 .expect_err("partial final COPY row must fail closed");
914 assert!(matches!(err, PgError::Protocol(msg) if msg.contains("truncated row")));
915 assert_eq!(pending, b"x\ty");
916 }
917
918 #[test]
919 fn callback_error_bubbles_from_row_drainer() {
920 let mut pending = Vec::new();
921 let mut on_row =
922 |_row: Vec<String>| -> PgResult<()> { Err(PgError::Query("fail".to_string())) };
923
924 let err = drain_copy_text_rows(&mut pending, b"a\tb\n", &mut on_row).unwrap_err();
925 assert!(matches!(err, PgError::Query(msg) if msg == "fail"));
926 }
927}