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
221const MAX_COPY_TEXT_ROW_BYTES: usize = 16 * 1024 * 1024;
226
227fn drain_copy_text_rows<F>(pending: &mut Vec<u8>, chunk: &[u8], on_row: &mut F) -> PgResult<()>
228where
229 F: FnMut(Vec<String>) -> PgResult<()>,
230{
231 pending.extend_from_slice(chunk);
232 let mut consumed = 0usize;
236 let result = loop {
237 let Some(len) = pending[consumed..].iter().position(|&b| b == b'\n') else {
238 break Ok(());
239 };
240 let row = parse_copy_text_row(&pending[consumed..consumed + len]);
241 consumed += len + 1;
242 if let Err(e) = row.and_then(&mut *on_row) {
243 break Err(e);
244 }
245 };
246 pending.drain(..consumed);
248 result?;
249 if pending.len() > MAX_COPY_TEXT_ROW_BYTES {
253 let buffered = pending.len();
254 pending.clear();
255 return Err(PgError::Protocol(format!(
256 "COPY text row exceeds {} bytes without a newline ({} buffered)",
257 MAX_COPY_TEXT_ROW_BYTES, buffered
258 )));
259 }
260 Ok(())
261}
262
263fn flush_pending_copy_text_row(pending: &[u8]) -> PgResult<()> {
264 if pending.is_empty() {
265 return Ok(());
266 }
267 Err(PgError::Protocol(
268 "COPY text stream ended with a truncated row without final newline".to_string(),
269 ))
270}
271
272impl PgConnection {
273 pub(crate) async fn copy_in_fast(
277 &mut self,
278 table: &str,
279 columns: &[String],
280 rows: &[Vec<qail_core::ast::Value>],
281 ) -> PgResult<u64> {
282 use crate::protocol::try_encode_copy_batch;
283
284 let cols: Vec<String> = columns
285 .iter()
286 .map(|c| quote_copy_column_ident(c))
287 .collect::<PgResult<_>>()?;
288 let sql = format!(
289 "COPY {} ({}) FROM STDIN",
290 quote_copy_table_ref(table)?,
291 cols.join(", ")
292 );
293
294 let batch_data = try_encode_copy_batch(rows)?;
297
298 let bytes = PgEncoder::try_encode_query_string(&sql)?;
300 self.send_bytes(&bytes).await?;
301
302 let mut startup_error: Option<PgError> = None;
304 loop {
305 let msg = self.recv().await?;
306 match msg {
307 BackendMessage::CopyInResponse { .. } => {
308 if let Some(err) = startup_error {
309 return return_with_desync(self, err);
310 }
311 break;
312 }
313 BackendMessage::ReadyForQuery(_) => {
314 return return_with_desync(
315 self,
316 startup_error.unwrap_or_else(|| {
317 PgError::Protocol(
318 "COPY IN failed before CopyInResponse (unexpected ReadyForQuery)"
319 .to_string(),
320 )
321 }),
322 );
323 }
324 BackendMessage::ErrorResponse(err) => {
325 if startup_error.is_none() {
326 startup_error = Some(PgError::QueryServer(err.into()));
327 }
328 }
329 msg if is_ignorable_session_message(&msg) => {}
330 other => {
331 return return_with_desync(
332 self,
333 unexpected_backend_message("copy-in startup", &other),
334 );
335 }
336 }
337 }
338
339 self.send_copy_data(&batch_data).await?;
341
342 self.send_copy_done().await?;
344
345 let mut affected = 0u64;
347 let mut final_error: Option<PgError> = None;
348 let mut saw_command_complete = false;
349 loop {
350 let msg = self.recv().await?;
351 match msg {
352 BackendMessage::CommandComplete(tag) => {
353 if saw_command_complete {
354 return return_with_desync(
355 self,
356 PgError::Protocol(
357 "COPY IN received duplicate CommandComplete".to_string(),
358 ),
359 );
360 }
361 saw_command_complete = true;
362 if final_error.is_none() {
363 match parse_affected_rows(&tag) {
364 Ok(parsed) => affected = parsed,
365 Err(err) => return return_with_desync(self, err),
366 }
367 }
368 }
369 BackendMessage::ReadyForQuery(_) => {
370 if let Some(err) = final_error {
371 return Err(err);
372 }
373 if !saw_command_complete {
374 return return_with_desync(
375 self,
376 PgError::Protocol(
377 "COPY IN completion missing CommandComplete before ReadyForQuery"
378 .to_string(),
379 ),
380 );
381 }
382 return Ok(affected);
383 }
384 BackendMessage::ErrorResponse(err) => {
385 if final_error.is_none() {
386 final_error = Some(PgError::QueryServer(err.into()));
387 }
388 }
389 msg if is_ignorable_session_message(&msg) => {}
390 other => {
391 return return_with_desync(
392 self,
393 unexpected_backend_message("copy-in completion", &other),
394 );
395 }
396 }
397 }
398 }
399
400 pub async fn copy_in_raw(
407 &mut self,
408 table: &str,
409 columns: &[String],
410 data: &[u8],
411 ) -> PgResult<u64> {
412 let cols: Vec<String> = columns
413 .iter()
414 .map(|c| quote_copy_column_ident(c))
415 .collect::<PgResult<_>>()?;
416 let sql = format!(
417 "COPY {} ({}) FROM STDIN",
418 quote_copy_table_ref(table)?,
419 cols.join(", ")
420 );
421
422 let bytes = PgEncoder::try_encode_query_string(&sql)?;
424 self.send_bytes(&bytes).await?;
425
426 let mut startup_error: Option<PgError> = None;
428 loop {
429 let msg = self.recv().await?;
430 match msg {
431 BackendMessage::CopyInResponse { .. } => {
432 if let Some(err) = startup_error {
433 return return_with_desync(self, err);
434 }
435 break;
436 }
437 BackendMessage::ReadyForQuery(_) => {
438 return return_with_desync(
439 self,
440 startup_error.unwrap_or_else(|| {
441 PgError::Protocol(
442 "COPY IN failed before CopyInResponse (unexpected ReadyForQuery)"
443 .to_string(),
444 )
445 }),
446 );
447 }
448 BackendMessage::ErrorResponse(err) => {
449 if startup_error.is_none() {
450 startup_error = Some(PgError::QueryServer(err.into()));
451 }
452 }
453 msg if is_ignorable_session_message(&msg) => {}
454 other => {
455 return return_with_desync(
456 self,
457 unexpected_backend_message("copy-in raw startup", &other),
458 );
459 }
460 }
461 }
462
463 self.send_copy_data(data).await?;
465
466 self.send_copy_done().await?;
468
469 let mut affected = 0u64;
471 let mut final_error: Option<PgError> = None;
472 let mut saw_command_complete = false;
473 loop {
474 let msg = self.recv().await?;
475 match msg {
476 BackendMessage::CommandComplete(tag) => {
477 if saw_command_complete {
478 return return_with_desync(
479 self,
480 PgError::Protocol(
481 "COPY IN raw received duplicate CommandComplete".to_string(),
482 ),
483 );
484 }
485 saw_command_complete = true;
486 if final_error.is_none() {
487 match parse_affected_rows(&tag) {
488 Ok(parsed) => affected = parsed,
489 Err(err) => return return_with_desync(self, err),
490 }
491 }
492 }
493 BackendMessage::ReadyForQuery(_) => {
494 if let Some(err) = final_error {
495 return Err(err);
496 }
497 if !saw_command_complete {
498 return return_with_desync(
499 self,
500 PgError::Protocol(
501 "COPY IN raw completion missing CommandComplete before ReadyForQuery"
502 .to_string(),
503 ),
504 );
505 }
506 return Ok(affected);
507 }
508 BackendMessage::ErrorResponse(err) => {
509 if final_error.is_none() {
510 final_error = Some(PgError::QueryServer(err.into()));
511 }
512 }
513 msg if is_ignorable_session_message(&msg) => {}
514 other => {
515 return return_with_desync(
516 self,
517 unexpected_backend_message("copy-in raw completion", &other),
518 );
519 }
520 }
521 }
522 }
523
524 pub(crate) async fn send_copy_data(&mut self, data: &[u8]) -> PgResult<()> {
526 let total_len = data
527 .len()
528 .checked_add(4)
529 .ok_or_else(|| PgError::Protocol("CopyData frame length overflow".to_string()))?;
530 let len = i32::try_from(total_len)
531 .map_err(|_| PgError::Protocol("CopyData frame exceeds i32::MAX".to_string()))?;
532
533 let mut buf = BytesMut::with_capacity(1 + 4 + data.len());
535 buf.extend_from_slice(b"d");
536 buf.extend_from_slice(&len.to_be_bytes());
537 buf.extend_from_slice(data);
538 self.send_bytes(&buf).await?;
539 Ok(())
540 }
541
542 async fn send_copy_done(&mut self) -> PgResult<()> {
543 self.send_bytes(&[b'c', 0, 0, 0, 4]).await?;
545 Ok(())
546 }
547
548 async fn start_copy_out(&mut self, sql: &str, context: &str) -> PgResult<()> {
549 let bytes = PgEncoder::try_encode_query_string(sql)?;
550 self.send_bytes(&bytes).await?;
551
552 let mut startup_error: Option<PgError> = None;
553 loop {
554 let msg = self.recv().await?;
555 match msg {
556 BackendMessage::CopyOutResponse { .. } => {
557 if let Some(err) = startup_error {
558 return return_with_desync(self, err);
559 }
560 return Ok(());
561 }
562 BackendMessage::ReadyForQuery(_) => {
563 return return_with_desync(
564 self,
565 startup_error.unwrap_or_else(|| {
566 PgError::Protocol(format!(
567 "{} failed before CopyOutResponse (unexpected ReadyForQuery)",
568 context
569 ))
570 }),
571 );
572 }
573 BackendMessage::ErrorResponse(err) => {
574 if startup_error.is_none() {
575 startup_error = Some(PgError::QueryServer(err.into()));
576 }
577 }
578 msg if is_ignorable_session_message(&msg) => {}
579 other => {
580 return return_with_desync(self, unexpected_backend_message(context, &other));
581 }
582 }
583 }
584 }
585
586 async fn stream_copy_out_chunks<F, Fut>(
587 &mut self,
588 context: &str,
589 mut on_chunk: F,
590 ) -> PgResult<()>
591 where
592 F: FnMut(Vec<u8>) -> Fut,
593 Fut: Future<Output = PgResult<()>>,
594 {
595 let mut stream_error: Option<PgError> = None;
596 let mut callback_error: Option<PgError> = None;
597 let mut saw_copy_done = false;
598 let mut saw_command_complete = false;
599
600 loop {
601 let msg = self.recv().await?;
602 match msg {
603 BackendMessage::CopyData(chunk) => {
604 if saw_copy_done {
605 return return_with_desync(
606 self,
607 PgError::Protocol(format!(
608 "{} received CopyData after CopyDone",
609 context
610 )),
611 );
612 }
613 if stream_error.is_none()
614 && callback_error.is_none()
615 && let Err(e) = on_chunk(chunk).await
616 {
617 callback_error = Some(e);
618 }
619 }
620 BackendMessage::CopyDone => {
621 if saw_copy_done {
622 return return_with_desync(
623 self,
624 PgError::Protocol(format!("{} received duplicate CopyDone", context)),
625 );
626 }
627 saw_copy_done = true;
628 }
629 BackendMessage::CommandComplete(_) => {
630 if !saw_copy_done {
631 return return_with_desync(
632 self,
633 PgError::Protocol(format!(
634 "{} received CommandComplete before CopyDone",
635 context
636 )),
637 );
638 }
639 if saw_command_complete {
640 return return_with_desync(
641 self,
642 PgError::Protocol(format!(
643 "{} received duplicate CommandComplete",
644 context
645 )),
646 );
647 }
648 saw_command_complete = true;
649 }
650 BackendMessage::ReadyForQuery(_) => {
651 if let Some(err) = stream_error {
652 return Err(err);
653 }
654 if let Some(err) = callback_error {
655 return Err(err);
656 }
657 if !saw_copy_done {
658 return return_with_desync(
659 self,
660 PgError::Protocol(format!(
661 "{} missing CopyDone before ReadyForQuery",
662 context
663 )),
664 );
665 }
666 if !saw_command_complete {
667 return return_with_desync(
668 self,
669 PgError::Protocol(format!(
670 "{} missing CommandComplete before ReadyForQuery",
671 context
672 )),
673 );
674 }
675 return Ok(());
676 }
677 BackendMessage::ErrorResponse(err) => {
678 if stream_error.is_none() {
679 stream_error = Some(PgError::QueryServer(err.into()));
680 }
681 }
682 msg if is_ignorable_session_message(&msg) => {}
683 other => {
684 return return_with_desync(self, unexpected_backend_message(context, &other));
685 }
686 }
687 }
688 }
689
690 pub async fn copy_export(&mut self, cmd: &Qail) -> PgResult<Vec<Vec<String>>> {
705 let mut rows = Vec::new();
706 self.copy_export_stream_rows(cmd, |row| {
707 rows.push(row);
708 Ok(())
709 })
710 .await?;
711 Ok(rows)
712 }
713
714 pub async fn copy_export_stream_raw<F, Fut>(&mut self, cmd: &Qail, on_chunk: F) -> PgResult<()>
719 where
720 F: FnMut(Vec<u8>) -> Fut,
721 Fut: Future<Output = PgResult<()>>,
722 {
723 let sql = encode_copy_export_sql(cmd)?;
724
725 self.copy_out_raw_stream(&sql, on_chunk).await
726 }
727
728 pub async fn copy_export_stream_rows<F>(&mut self, cmd: &Qail, mut on_row: F) -> PgResult<()>
733 where
734 F: FnMut(Vec<String>) -> PgResult<()>,
735 {
736 let mut pending = Vec::new();
737 self.copy_export_stream_raw(cmd, |chunk| {
738 let res = drain_copy_text_rows(&mut pending, &chunk, &mut on_row);
739 std::future::ready(res)
740 })
741 .await?;
742 flush_pending_copy_text_row(&pending)
743 }
744
745 pub(crate) async fn copy_out_raw(&mut self, sql: &str) -> PgResult<Vec<u8>> {
753 let mut data = Vec::new();
754 self.copy_out_raw_stream(sql, |chunk| {
755 data.extend_from_slice(&chunk);
756 std::future::ready(Ok(()))
757 })
758 .await?;
759 Ok(data)
760 }
761
762 pub(crate) async fn copy_out_raw_stream<F, Fut>(
767 &mut self,
768 sql: &str,
769 on_chunk: F,
770 ) -> PgResult<()>
771 where
772 F: FnMut(Vec<u8>) -> Fut,
773 Fut: Future<Output = PgResult<()>>,
774 {
775 self.start_copy_out(sql, "copy-out raw startup").await?;
776 self.stream_copy_out_chunks("copy-out raw stream", on_chunk)
777 .await
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::{
784 drain_copy_text_rows, encode_copy_export_sql, flush_pending_copy_text_row,
785 parse_copy_text_row, quote_copy_column_ident, quote_copy_table_ref, return_with_desync,
786 };
787 use crate::driver::{PgConnection, PgError, PgResult};
788 use qail_core::ast::{Operator, Qail};
789
790 #[cfg(unix)]
791 fn test_conn() -> PgConnection {
792 use crate::driver::connection::StatementCache;
793 use crate::driver::stream::PgStream;
794 use bytes::BytesMut;
795 use std::collections::{HashMap, VecDeque};
796 use std::num::NonZeroUsize;
797 use tokio::net::UnixStream;
798
799 let (unix_stream, _peer) = UnixStream::pair().expect("unix stream pair");
800 PgConnection {
801 stream: PgStream::Unix(unix_stream),
802 buffer: BytesMut::with_capacity(1024),
803 write_buf: BytesMut::with_capacity(1024),
804 sql_buf: BytesMut::with_capacity(256),
805 params_buf: Vec::new(),
806 prepared_statements: HashMap::new(),
807 stmt_cache: StatementCache::new(NonZeroUsize::new(2).expect("non-zero")),
808 column_info_cache: HashMap::new(),
809 process_id: 0,
810 cancel_key_bytes: Vec::new(),
811 requested_protocol_minor: PgConnection::default_protocol_minor(),
812 negotiated_protocol_minor: PgConnection::default_protocol_minor(),
813 notifications: VecDeque::new(),
814 replication_stream_active: false,
815 replication_mode_enabled: false,
816 last_replication_wal_end: None,
817 io_desynced: false,
818 pending_statement_closes: Vec::new(),
819 draining_statement_closes: false,
820 }
821 }
822
823 #[test]
824 fn parse_copy_text_row_splits_tabs() {
825 let row = parse_copy_text_row(b"a\tb\tc").unwrap();
826 assert_eq!(row, vec!["a", "b", "c"]);
827 }
828
829 #[test]
830 fn parse_copy_text_row_trims_cr() {
831 let row = parse_copy_text_row(b"a\tb\r").unwrap();
832 assert_eq!(row, vec!["a", "b"]);
833 }
834
835 #[test]
836 fn parse_copy_text_row_unescapes_copy_text_values() {
837 let row = parse_copy_text_row(b"a\\tb\tline\\nnext\tc\\\\d").unwrap();
838 assert_eq!(row, vec!["a\tb", "line\nnext", "c\\d"]);
839 }
840
841 #[test]
842 fn parse_copy_text_row_rejects_copy_null_marker() {
843 let err = parse_copy_text_row(b"a\t\\N\tb").expect_err("COPY NULL must not be lossy");
844 assert!(
845 err.to_string()
846 .contains("COPY text NULL cannot be represented"),
847 "{err}"
848 );
849 }
850
851 #[test]
852 fn parse_copy_text_row_rejects_invalid_utf8() {
853 let err = parse_copy_text_row(&[0xff]).expect_err("invalid UTF-8 must fail");
854 assert!(
855 err.to_string()
856 .contains("COPY text field is not valid UTF-8")
857 );
858 }
859
860 #[test]
861 fn parse_copy_text_row_rejects_incomplete_escape() {
862 let err = parse_copy_text_row(b"bad\\").expect_err("trailing backslash must fail");
863 assert!(err.to_string().contains("incomplete backslash escape"));
864 }
865
866 #[test]
867 fn parse_copy_text_row_rejects_out_of_range_octal_escape() {
868 let err = parse_copy_text_row(br"\400").expect_err("octal escape > 377 must fail");
869 assert!(err.to_string().contains("out of byte range"));
870 }
871
872 #[test]
873 fn parse_copy_text_row_rejects_hex_escape_without_digits() {
874 let err = parse_copy_text_row(br"\xG").expect_err("hex escape without digits must fail");
875 assert!(err.to_string().contains("hex escape requires"));
876 }
877
878 #[test]
879 fn copy_table_quoting_preserves_schema_qualification() {
880 assert_eq!(
881 quote_copy_table_ref("tenant_a.users").unwrap(),
882 "\"tenant_a\".\"users\""
883 );
884 }
885
886 #[test]
887 fn copy_identifier_quoting_rejects_nul_bytes() {
888 assert!(quote_copy_table_ref("tenant\0.users").is_err());
889 assert!(quote_copy_column_ident("name\0").is_err());
890 }
891
892 #[test]
893 fn copy_export_rejects_parameterized_ast_before_streaming() {
894 let cmd = Qail::export("users").filter("active", Operator::Eq, true);
895 let err = encode_copy_export_sql(&cmd).expect_err("bind params cannot be ignored");
896
897 assert!(matches!(err, PgError::Encode(msg) if msg.contains("parameterized export")));
898 }
899
900 #[cfg(unix)]
901 #[tokio::test]
902 async fn copy_return_with_desync_marks_protocol_error() {
903 let mut conn = test_conn();
904
905 let err = return_with_desync::<()>(
906 &mut conn,
907 PgError::Protocol("copy protocol ordering broke".to_string()),
908 )
909 .expect_err("protocol error must be returned");
910
911 assert!(err.to_string().contains("copy protocol ordering broke"));
912 assert!(conn.is_io_desynced());
913 }
914
915 #[test]
916 fn drain_copy_text_rows_handles_chunk_boundaries() {
917 let mut pending = Vec::new();
918 let mut rows: Vec<Vec<String>> = Vec::new();
919
920 drain_copy_text_rows(&mut pending, b"a\tb\nc", &mut |row: Vec<String>| {
921 rows.push(row);
922 Ok(())
923 })
924 .unwrap();
925 assert_eq!(rows, vec![vec!["a".to_string(), "b".to_string()]]);
926 assert_eq!(pending, b"c");
927
928 drain_copy_text_rows(&mut pending, b"\td\n", &mut |row: Vec<String>| {
929 rows.push(row);
930 Ok(())
931 })
932 .unwrap();
933 assert_eq!(
934 rows,
935 vec![
936 vec!["a".to_string(), "b".to_string()],
937 vec!["c".to_string(), "d".to_string()]
938 ]
939 );
940 assert!(pending.is_empty());
941 }
942
943 #[test]
944 fn flush_pending_copy_text_row_rejects_final_partial_line() {
945 let pending = b"x\ty".to_vec();
946 let err = flush_pending_copy_text_row(&pending)
947 .expect_err("partial final COPY row must fail closed");
948 assert!(matches!(err, PgError::Protocol(msg) if msg.contains("truncated row")));
949 assert_eq!(pending, b"x\ty");
950 }
951
952 #[test]
953 fn callback_error_bubbles_from_row_drainer() {
954 let mut pending = Vec::new();
955 let mut on_row =
956 |_row: Vec<String>| -> PgResult<()> { Err(PgError::Query("fail".to_string())) };
957
958 let err = drain_copy_text_rows(&mut pending, b"a\tb\n", &mut on_row).unwrap_err();
959 assert!(matches!(err, PgError::Query(msg) if msg == "fail"));
960 }
961
962 #[test]
963 fn row_error_consumes_that_row_and_keeps_the_rest_pending() {
964 let mut pending = Vec::new();
965 let mut seen = 0usize;
966 let mut on_row = |_row: Vec<String>| -> PgResult<()> {
967 seen += 1;
968 if seen == 2 {
969 return Err(PgError::Query("second".to_string()));
970 }
971 Ok(())
972 };
973 let err =
974 drain_copy_text_rows(&mut pending, b"a\tb\nc\td\ne\tf\ng", &mut on_row).unwrap_err();
975 assert!(matches!(err, PgError::Query(msg) if msg == "second"));
976 assert_eq!(pending, b"e\tf\ng");
977
978 let mut pending = Vec::new();
979 let mut on_row = |_row: Vec<String>| -> PgResult<()> { Ok(()) };
980 drain_copy_text_rows(&mut pending, b"a\tb\n\\x\nc\td", &mut on_row)
981 .expect_err("an invalid escape must fail the row");
982 assert_eq!(pending, b"c\td");
983 }
984
985 #[test]
986 fn drain_copy_text_rows_rejects_newline_free_row_over_cap() {
987 let mut pending = Vec::new();
988 let mut on_row = |_row: Vec<String>| -> PgResult<()> {
989 panic!("no row should complete without a newline")
990 };
991 let chunk = vec![b'x'; super::MAX_COPY_TEXT_ROW_BYTES + 1];
992
993 let err = drain_copy_text_rows(&mut pending, &chunk, &mut on_row).unwrap_err();
994
995 assert!(matches!(err, PgError::Protocol(msg) if msg.contains("exceeds")));
996 assert!(
997 pending.is_empty(),
998 "overflowed accumulator must be released"
999 );
1000 }
1001
1002 #[test]
1003 fn drain_copy_text_rows_caps_accumulation_across_chunks() {
1004 let mut pending = Vec::new();
1005 let mut on_row = |_row: Vec<String>| -> PgResult<()> {
1006 panic!("no row should complete without a newline")
1007 };
1008 let chunk = vec![b'x'; super::MAX_COPY_TEXT_ROW_BYTES / 2 + 1];
1009
1010 drain_copy_text_rows(&mut pending, &chunk, &mut on_row).unwrap();
1011 let err = drain_copy_text_rows(&mut pending, &chunk, &mut on_row).unwrap_err();
1012
1013 assert!(matches!(err, PgError::Protocol(msg) if msg.contains("exceeds")));
1014 assert!(pending.is_empty());
1015 }
1016
1017 #[test]
1018 fn drain_copy_text_rows_accepts_large_frame_of_complete_rows() {
1019 let mut pending = Vec::new();
1020 let mut rows = 0usize;
1021 let mut on_row = |_row: Vec<String>| -> PgResult<()> {
1022 rows += 1;
1023 Ok(())
1024 };
1025 let row_count = super::MAX_COPY_TEXT_ROW_BYTES / 4 + 1;
1028 let chunk = b"a\tb\n".repeat(row_count);
1029 assert!(chunk.len() > super::MAX_COPY_TEXT_ROW_BYTES);
1030
1031 drain_copy_text_rows(&mut pending, &chunk, &mut on_row).unwrap();
1032
1033 assert_eq!(rows, row_count);
1034 assert!(pending.is_empty());
1035 }
1036}