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