Skip to main content

mz_pgcopy/
copy.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::borrow::Cow;
11use std::io;
12
13use bytes::BytesMut;
14use itertools::Itertools;
15use mz_pgrepr::TextEncodeSettings;
16use mz_repr::{
17    Datum, RelationDesc, Row, RowArena, RowRef, SharedRow, SqlColumnType, SqlRelationType,
18    SqlScalarType,
19};
20use proptest::prelude::{Arbitrary, any};
21use proptest::strategy::{BoxedStrategy, Strategy};
22use serde::{Deserialize, Serialize};
23
24static END_OF_COPY_MARKER: &[u8] = b"\\.";
25
26fn encode_copy_row_binary(
27    row: &RowRef,
28    typ: &SqlRelationType,
29    out: &mut Vec<u8>,
30) -> Result<(), io::Error> {
31    const NULL_BYTES: [u8; 4] = (-1i32).to_be_bytes();
32
33    // 16-bit int of number of tuples.
34    let count = i16::try_from(typ.column_types.len()).map_err(|_| {
35        io::Error::new(
36            io::ErrorKind::InvalidData,
37            "column count does not fit into an i16",
38        )
39    })?;
40
41    out.extend(count.to_be_bytes());
42    let mut buf = BytesMut::new();
43    for (field, typ) in row
44        .iter()
45        .zip_eq(&typ.column_types)
46        .map(|(datum, typ)| (mz_pgrepr::Value::from_datum(datum, &typ.scalar_type), typ))
47    {
48        match field {
49            None => out.extend(NULL_BYTES),
50            Some(field) => {
51                buf.clear();
52                field.encode_binary(&mz_pgrepr::Type::from(&typ.scalar_type), &mut buf)?;
53                out.extend(
54                    i32::try_from(buf.len())
55                        .map_err(|_| {
56                            io::Error::new(
57                                io::ErrorKind::InvalidData,
58                                "field length does not fit into an i32",
59                            )
60                        })?
61                        .to_be_bytes(),
62                );
63                out.extend(&buf);
64            }
65        }
66    }
67    Ok(())
68}
69
70fn encode_copy_row_text(
71    CopyTextFormatParams { null, delimiter }: &CopyTextFormatParams,
72    row: &RowRef,
73    typ: &SqlRelationType,
74    out: &mut Vec<u8>,
75    settings: TextEncodeSettings,
76) -> Result<(), io::Error> {
77    let null = null.as_bytes();
78    let mut buf = BytesMut::new();
79    for (idx, field) in mz_pgrepr::values_from_row(row, typ).into_iter().enumerate() {
80        if idx > 0 {
81            out.push(*delimiter);
82        }
83        match field {
84            None => out.extend(null),
85            Some(field) => {
86                buf.clear();
87                field.encode_text(&mut buf, settings);
88                for b in &buf {
89                    match b {
90                        b'\\' => out.extend(b"\\\\"),
91                        b'\n' => out.extend(b"\\n"),
92                        b'\r' => out.extend(b"\\r"),
93                        b'\t' => out.extend(b"\\t"),
94                        _ => out.push(*b),
95                    }
96                }
97            }
98        }
99    }
100    out.push(b'\n');
101    Ok(())
102}
103
104fn encode_copy_row_csv(
105    CopyCsvFormatParams {
106        delimiter: delim,
107        quote,
108        escape,
109        header: _,
110        null,
111    }: &CopyCsvFormatParams,
112    row: &RowRef,
113    typ: &SqlRelationType,
114    out: &mut Vec<u8>,
115    settings: TextEncodeSettings,
116) -> Result<(), io::Error> {
117    let null = null.as_bytes();
118    let is_special = |c: &u8| *c == *delim || *c == *quote || *c == b'\r' || *c == b'\n';
119    let mut buf = BytesMut::new();
120    for (idx, field) in mz_pgrepr::values_from_row(row, typ).into_iter().enumerate() {
121        if idx > 0 {
122            out.push(*delim);
123        }
124        match field {
125            None => out.extend(null),
126            Some(field) => {
127                buf.clear();
128                field.encode_text(&mut buf, settings);
129                // A field needs quoting if:
130                //   * It is the only field and the value is exactly the end
131                //     of copy marker.
132                //   * The field contains a special character.
133                //   * The field is exactly the NULL sentinel.
134                if (typ.column_types.len() == 1 && buf == END_OF_COPY_MARKER)
135                    || buf.iter().any(is_special)
136                    || &*buf == null
137                {
138                    // Quote the value by wrapping it in the quote character and
139                    // emitting the escape character before any quote or escape
140                    // characters within.
141                    out.push(*quote);
142                    for b in &buf {
143                        if *b == *quote || *b == *escape {
144                            out.push(*escape);
145                        }
146                        out.push(*b);
147                    }
148                    out.push(*quote);
149                } else {
150                    // The value does not need quoting and can be emitted
151                    // directly.
152                    out.extend(&buf);
153                }
154            }
155        }
156    }
157    out.push(b'\n');
158    Ok(())
159}
160
161pub struct CopyTextFormatParser<'a> {
162    data: &'a [u8],
163    position: usize,
164    column_delimiter: u8,
165    null_string: &'a str,
166    buffer: Vec<u8>,
167}
168
169impl<'a> CopyTextFormatParser<'a> {
170    pub fn new(data: &'a [u8], column_delimiter: u8, null_string: &'a str) -> Self {
171        Self {
172            data,
173            position: 0,
174            column_delimiter,
175            null_string,
176            buffer: Vec::new(),
177        }
178    }
179
180    fn peek(&self) -> Option<u8> {
181        if self.position < self.data.len() {
182            Some(self.data[self.position])
183        } else {
184            None
185        }
186    }
187
188    fn consume_n(&mut self, n: usize) {
189        self.position = std::cmp::min(self.position + n, self.data.len());
190    }
191
192    pub fn is_eof(&self) -> bool {
193        self.peek().is_none() || self.is_end_of_copy_marker()
194    }
195
196    pub fn is_end_of_copy_marker(&self) -> bool {
197        self.check_bytes(END_OF_COPY_MARKER)
198    }
199
200    fn is_end_of_line(&self) -> bool {
201        match self.peek() {
202            Some(b'\n') | None => true,
203            _ => false,
204        }
205    }
206
207    pub fn expect_end_of_line(&mut self) -> Result<(), io::Error> {
208        if self.is_end_of_line() {
209            self.consume_n(1);
210            Ok(())
211        } else {
212            Err(io::Error::new(
213                io::ErrorKind::InvalidData,
214                "extra data after last expected column",
215            ))
216        }
217    }
218
219    fn is_column_delimiter(&self) -> bool {
220        self.check_bytes(&[self.column_delimiter])
221    }
222
223    pub fn expect_column_delimiter(&mut self) -> Result<(), io::Error> {
224        if self.consume_bytes(&[self.column_delimiter]) {
225            Ok(())
226        } else {
227            Err(io::Error::new(
228                io::ErrorKind::InvalidData,
229                "missing data for column",
230            ))
231        }
232    }
233
234    fn check_bytes(&self, bytes: &[u8]) -> bool {
235        self.data
236            .get(self.position..self.position + bytes.len())
237            .map_or(false, |d| d == bytes)
238    }
239
240    fn consume_bytes(&mut self, bytes: &[u8]) -> bool {
241        if self.check_bytes(bytes) {
242            self.consume_n(bytes.len());
243            true
244        } else {
245            false
246        }
247    }
248
249    fn consume_null_string(&mut self) -> bool {
250        if self.null_string.is_empty() {
251            // An empty NULL marker is supported. Look ahead to ensure that is followed by
252            // a column delimiter, an end of line or it is at the end of the data.
253            self.is_column_delimiter()
254                || self.is_end_of_line()
255                || self.is_end_of_copy_marker()
256                || self.is_eof()
257        } else {
258            self.consume_bytes(self.null_string.as_bytes())
259        }
260    }
261
262    pub fn consume_raw_value(&mut self) -> Result<Option<&[u8]>, io::Error> {
263        if self.consume_null_string() {
264            return Ok(None);
265        }
266
267        let mut start = self.position;
268
269        // buffer where unescaped data is accumulated
270        self.buffer.clear();
271
272        while !self.is_eof() && !self.is_end_of_copy_marker() {
273            if self.is_end_of_line() || self.is_column_delimiter() {
274                break;
275            }
276            match self.peek() {
277                Some(b'\\') => {
278                    // Add non-escaped data parsed so far
279                    self.buffer.extend(&self.data[start..self.position]);
280
281                    self.consume_n(1);
282                    match self.peek() {
283                        Some(b'b') => {
284                            self.consume_n(1);
285                            self.buffer.push(8);
286                        }
287                        Some(b'f') => {
288                            self.consume_n(1);
289                            self.buffer.push(12);
290                        }
291                        Some(b'n') => {
292                            self.consume_n(1);
293                            self.buffer.push(b'\n');
294                        }
295                        Some(b'r') => {
296                            self.consume_n(1);
297                            self.buffer.push(b'\r');
298                        }
299                        Some(b't') => {
300                            self.consume_n(1);
301                            self.buffer.push(b'\t');
302                        }
303                        Some(b'v') => {
304                            self.consume_n(1);
305                            self.buffer.push(11);
306                        }
307                        Some(b'x') => {
308                            self.consume_n(1);
309                            match self.peek() {
310                                Some(_c @ b'0'..=b'9')
311                                | Some(_c @ b'A'..=b'F')
312                                | Some(_c @ b'a'..=b'f') => {
313                                    let mut value: u8 = 0;
314                                    let decode_nibble = |b| match b {
315                                        Some(c @ b'a'..=b'f') => Some(c - b'a' + 10),
316                                        Some(c @ b'A'..=b'F') => Some(c - b'A' + 10),
317                                        Some(c @ b'0'..=b'9') => Some(c - b'0'),
318                                        _ => None,
319                                    };
320                                    for _ in 0..2 {
321                                        match decode_nibble(self.peek()) {
322                                            Some(c) => {
323                                                self.consume_n(1);
324                                                value = (value << 4) | c;
325                                            }
326                                            _ => break,
327                                        }
328                                    }
329                                    self.buffer.push(value);
330                                }
331                                _ => {
332                                    self.buffer.push(b'x');
333                                }
334                            }
335                        }
336                        Some(_c @ b'0'..=b'7') => {
337                            let mut value: u8 = 0;
338                            for _ in 0..3 {
339                                match self.peek() {
340                                    Some(c @ b'0'..=b'7') => {
341                                        self.consume_n(1);
342                                        value = (value << 3) | (c - b'0');
343                                    }
344                                    _ => break,
345                                }
346                            }
347                            self.buffer.push(value);
348                        }
349                        Some(c) => {
350                            self.consume_n(1);
351                            self.buffer.push(c);
352                        }
353                        None => {
354                            self.buffer.push(b'\\');
355                        }
356                    }
357
358                    start = self.position;
359                }
360                Some(_) => {
361                    self.consume_n(1);
362                }
363                None => {}
364            }
365        }
366
367        // Return a slice of the original buffer if no escaped characters where processed
368        if self.buffer.is_empty() {
369            Ok(Some(&self.data[start..self.position]))
370        } else {
371            // ... otherwise, add the remaining non-escaped data to the decoding buffer
372            // and return a pointer to it
373            self.buffer.extend(&self.data[start..self.position]);
374            Ok(Some(&self.buffer[..]))
375        }
376    }
377
378    /// Error if more than `num_columns` values in `parser`.
379    pub fn iter_raw(self, num_columns: usize) -> RawIterator<'a> {
380        RawIterator {
381            parser: self,
382            current_column: 0,
383            num_columns,
384            truncate: false,
385        }
386    }
387
388    /// Return no more than `num_columns` values from `parser`.
389    pub fn iter_raw_truncating(self, num_columns: usize) -> RawIterator<'a> {
390        RawIterator {
391            parser: self,
392            current_column: 0,
393            num_columns,
394            truncate: true,
395        }
396    }
397}
398
399pub struct RawIterator<'a> {
400    parser: CopyTextFormatParser<'a>,
401    current_column: usize,
402    num_columns: usize,
403    truncate: bool,
404}
405
406impl<'a> RawIterator<'a> {
407    pub fn next(&mut self) -> Option<Result<Option<&[u8]>, io::Error>> {
408        if self.current_column > self.num_columns {
409            return None;
410        }
411
412        if self.current_column == self.num_columns {
413            if !self.truncate {
414                if let Some(err) = self.parser.expect_end_of_line().err() {
415                    return Some(Err(err));
416                }
417            }
418
419            return None;
420        }
421
422        if self.current_column > 0 {
423            if let Some(err) = self.parser.expect_column_delimiter().err() {
424                return Some(Err(err));
425            }
426        }
427
428        self.current_column += 1;
429        Some(self.parser.consume_raw_value())
430    }
431}
432
433#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
434pub enum CopyFormatParams<'a> {
435    Text(CopyTextFormatParams<'a>),
436    Csv(CopyCsvFormatParams<'a>),
437    Binary,
438    Parquet,
439}
440
441impl CopyFormatParams<'static> {
442    pub fn file_extension(&self) -> &str {
443        match self {
444            &CopyFormatParams::Text(_) => "txt",
445            &CopyFormatParams::Csv(_) => "csv",
446            &CopyFormatParams::Binary => "bin",
447            &CopyFormatParams::Parquet => "parquet",
448        }
449    }
450
451    pub fn requires_header(&self) -> bool {
452        match self {
453            CopyFormatParams::Text(_) => false,
454            CopyFormatParams::Csv(params) => params.header,
455            CopyFormatParams::Binary => false,
456            CopyFormatParams::Parquet => false,
457        }
458    }
459}
460
461/// Decodes the given bytes into `Row`-s based on the given `CopyFormatParams`.
462pub fn decode_copy_format<'a>(
463    data: &[u8],
464    column_types: &[mz_pgrepr::Type],
465    params: CopyFormatParams<'a>,
466) -> Result<Vec<Row>, io::Error> {
467    match params {
468        CopyFormatParams::Text(params) => decode_copy_format_text(data, column_types, params),
469        CopyFormatParams::Csv(params) => decode_copy_format_csv(data, column_types, params),
470        CopyFormatParams::Binary => Err(io::Error::new(
471            io::ErrorKind::Unsupported,
472            "cannot decode as binary format",
473        )),
474        CopyFormatParams::Parquet => {
475            // TODO(cf2): Support Parquet over STDIN.
476            Err(io::Error::new(io::ErrorKind::Unsupported, "parquet format"))
477        }
478    }
479}
480
481/// Encodes the given `Row` into bytes based on the given `CopyFormatParams`.
482///
483/// `settings` affects only the text and CSV formats. Callers that do not encode
484/// on behalf of a session, such as `COPY TO <external destination>`, which is
485/// executed in the dataflow layer, must pass [`TextEncodeSettings::STABLE`].
486pub fn encode_copy_format<'a>(
487    params: &CopyFormatParams<'a>,
488    row: &RowRef,
489    typ: &SqlRelationType,
490    out: &mut Vec<u8>,
491    settings: TextEncodeSettings,
492) -> Result<(), io::Error> {
493    match params {
494        CopyFormatParams::Text(params) => encode_copy_row_text(params, row, typ, out, settings),
495        CopyFormatParams::Csv(params) => encode_copy_row_csv(params, row, typ, out, settings),
496        CopyFormatParams::Binary => encode_copy_row_binary(row, typ, out),
497        CopyFormatParams::Parquet => {
498            // TODO(cf2): Support Parquet over STDIN.
499            Err(io::Error::new(io::ErrorKind::Unsupported, "parquet format"))
500        }
501    }
502}
503
504pub fn encode_copy_format_header<'a>(
505    params: &CopyFormatParams<'a>,
506    desc: &RelationDesc,
507    out: &mut Vec<u8>,
508) -> Result<(), io::Error> {
509    match params {
510        CopyFormatParams::Text(_) => Ok(()),
511        CopyFormatParams::Binary => Ok(()),
512        CopyFormatParams::Csv(params) => {
513            let mut header_row = Row::with_capacity(desc.arity());
514            header_row
515                .packer()
516                .extend(desc.iter_names().map(|s| Datum::from(s.as_str())));
517            let typ = SqlRelationType::new(vec![
518                SqlColumnType {
519                    scalar_type: SqlScalarType::String,
520                    nullable: false,
521                };
522                desc.arity()
523            ]);
524            encode_copy_row_csv(params, &header_row, &typ, out, TextEncodeSettings::STABLE)
525        }
526        CopyFormatParams::Parquet => {
527            // TODO(cf2): Support Parquet over STDIN.
528            Err(io::Error::new(io::ErrorKind::Unsupported, "parquet format"))
529        }
530    }
531}
532
533#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
534pub struct CopyTextFormatParams<'a> {
535    pub null: Cow<'a, str>,
536    pub delimiter: u8,
537}
538
539impl<'a> Default for CopyTextFormatParams<'a> {
540    fn default() -> Self {
541        CopyTextFormatParams {
542            delimiter: b'\t',
543            null: Cow::from("\\N"),
544        }
545    }
546}
547
548pub fn decode_copy_format_text(
549    data: &[u8],
550    column_types: &[mz_pgrepr::Type],
551    CopyTextFormatParams { null, delimiter }: CopyTextFormatParams,
552) -> Result<Vec<Row>, io::Error> {
553    let mut rows = Vec::new();
554
555    // TODO: pass the `CopyTextFormatParams` to the `new` method
556    let mut parser = CopyTextFormatParser::new(data, delimiter, &null);
557    while !parser.is_eof() && !parser.is_end_of_copy_marker() {
558        let mut row = Vec::new();
559        let buf = RowArena::new();
560        for (col, typ) in column_types.iter().enumerate() {
561            if col > 0 {
562                parser.expect_column_delimiter()?;
563            }
564            let raw_value = parser.consume_raw_value()?;
565            if let Some(raw_value) = raw_value {
566                match mz_pgrepr::Value::decode_text(typ, raw_value) {
567                    Ok(value) => {
568                        row.push(
569                            value
570                                .into_datum_decode_error(&buf, typ, "column")
571                                .map_err(|msg| io::Error::new(io::ErrorKind::InvalidData, msg))?,
572                        );
573                    }
574                    Err(err) => {
575                        let msg = format!("unable to decode column: {}", err);
576                        return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
577                    }
578                }
579            } else {
580                row.push(Datum::Null);
581            }
582        }
583        parser.expect_end_of_line()?;
584        rows.push(Row::pack(row));
585    }
586    // Note that if there is any junk data after the end of copy marker, we drop
587    // it on the floor as PG does.
588    Ok(rows)
589}
590
591#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
592pub struct CopyCsvFormatParams<'a> {
593    pub delimiter: u8,
594    pub quote: u8,
595    pub escape: u8,
596    pub header: bool,
597    pub null: Cow<'a, str>,
598}
599
600impl<'a> CopyCsvFormatParams<'a> {
601    pub fn to_owned(&self) -> CopyCsvFormatParams<'static> {
602        CopyCsvFormatParams {
603            delimiter: self.delimiter,
604            quote: self.quote,
605            escape: self.escape,
606            header: self.header,
607            null: Cow::Owned(self.null.to_string()),
608        }
609    }
610}
611
612impl Arbitrary for CopyCsvFormatParams<'static> {
613    type Parameters = ();
614    type Strategy = BoxedStrategy<Self>;
615
616    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
617        (
618            any::<u8>(),
619            any::<u8>(),
620            any::<u8>(),
621            any::<bool>(),
622            any::<String>(),
623        )
624            .prop_map(|(delimiter, diff, escape, header, null)| {
625                // Delimiter and Quote need to be different.
626                let diff = diff.saturating_sub(1).max(1);
627                let quote = delimiter.wrapping_add(diff);
628
629                Self::try_new(
630                    Some(delimiter),
631                    Some(quote),
632                    Some(escape),
633                    Some(header),
634                    Some(null),
635                )
636                .expect("delimiter and quote should be different")
637            })
638            .boxed()
639    }
640}
641
642impl<'a> Default for CopyCsvFormatParams<'a> {
643    fn default() -> Self {
644        CopyCsvFormatParams {
645            delimiter: b',',
646            quote: b'"',
647            escape: b'"',
648            header: false,
649            null: Cow::from(""),
650        }
651    }
652}
653
654impl<'a> CopyCsvFormatParams<'a> {
655    pub fn try_new(
656        delimiter: Option<u8>,
657        quote: Option<u8>,
658        escape: Option<u8>,
659        header: Option<bool>,
660        null: Option<String>,
661    ) -> Result<CopyCsvFormatParams<'a>, String> {
662        let mut params = CopyCsvFormatParams::default();
663
664        if let Some(delimiter) = delimiter {
665            params.delimiter = delimiter;
666        }
667        if let Some(quote) = quote {
668            params.quote = quote;
669            // escape defaults to the value provided for quote
670            params.escape = quote;
671        }
672        if let Some(escape) = escape {
673            params.escape = escape;
674        }
675        if let Some(header) = header {
676            params.header = header;
677        }
678        if let Some(null) = null {
679            params.null = Cow::from(null);
680        }
681
682        if params.quote == params.delimiter {
683            return Err("COPY delimiter and quote must be different".to_string());
684        }
685        Ok(params)
686    }
687}
688
689/// One field decoded out of a CSV record by [`decode_copy_format_csv`]:
690/// `start..end` indexes into the per-record `output` buffer (csv-core
691/// unquotes/unescapes into that buffer), and `quoted` records whether the
692/// field's first input byte was the quote character. The quote flag is what
693/// distinguishes a literal `""` (quoted empty string) from an unquoted empty
694/// field (the default NULL marker), and a quoted `"\."` data row from the bare
695/// `\.` end-of-copy marker.
696struct DecodedField {
697    start: usize,
698    end: usize,
699    quoted: bool,
700}
701
702pub fn decode_copy_format_csv(
703    data: &[u8],
704    column_types: &[mz_pgrepr::Type],
705    CopyCsvFormatParams {
706        delimiter,
707        quote,
708        escape,
709        null,
710        header,
711    }: CopyCsvFormatParams,
712) -> Result<Vec<Row>, io::Error> {
713    let (double_quote, escape) = if quote == escape {
714        (true, None)
715    } else {
716        (false, Some(escape))
717    };
718
719    let mut rdr = csv_core::ReaderBuilder::new()
720        .delimiter(delimiter)
721        .quote(quote)
722        .escape(escape)
723        .double_quote(double_quote)
724        .build();
725
726    let null_as_bytes = null.as_bytes();
727    let mut rows = Vec::new();
728    // We use csv-core (rather than the higher-level csv crate) so we can
729    // recover per-field "was this field quoted?" information by inspecting the
730    // first byte of each field's input. csv unquotes during parsing, which
731    // makes a quoted empty string indistinguishable from an unquoted empty
732    // field — and PostgreSQL COPY ... FORMAT CSV semantics need that
733    // distinction to honor the NULL marker.
734    let mut input = data;
735    let mut output = vec![0u8; data.len().max(1024)];
736    let mut out_pos = 0;
737    let mut fields: Vec<DecodedField> = Vec::new();
738    let mut field_start = 0;
739    let mut field_quoted: Option<bool> = None;
740    let mut skip_header = header;
741    // True at the start of a record (including the very first), where csv-core
742    // may have left an orphaned terminator byte; false for fields that follow a
743    // delimiter within a record.
744    let mut at_record_start = true;
745
746    loop {
747        if field_quoted.is_none() {
748            if at_record_start {
749                // csv-core's default terminator is CRLF, and it reports a
750                // record complete after consuming the `\r`, leaving the
751                // trailing `\n` as the first byte of the next record's input
752                // (and, after a worker chunk is split at a `\r` boundary, a
753                // chunk can likewise begin with that orphan `\n`). csv-core
754                // itself consumes and ignores those stray terminator bytes when
755                // parsing the field, but the quote probe below must skip them
756                // so it inspects the field's real first byte rather than an
757                // orphan — otherwise a quoted field on any non-first CRLF record
758                // is misclassified as unquoted. Only skip at a record boundary:
759                // a `\r`/`\n` after a delimiter legitimately terminates an empty
760                // trailing field and must not be swallowed.
761                while matches!(input.first(), Some(&b'\r') | Some(&b'\n')) {
762                    input = &input[1..];
763                }
764            }
765            field_quoted = Some(input.first() == Some(&quote));
766            field_start = out_pos;
767        }
768
769        let (result, nin, nout) = rdr.read_field(input, &mut output[out_pos..]);
770        input = &input[nin..];
771        out_pos += nout;
772
773        match result {
774            csv_core::ReadFieldResult::Field { record_end } => {
775                fields.push(DecodedField {
776                    start: field_start,
777                    end: out_pos,
778                    quoted: field_quoted.take().unwrap(),
779                });
780                // The next field begins a new record only if this field ended
781                // one; otherwise it follows a delimiter mid-record.
782                at_record_start = record_end;
783
784                if record_end {
785                    if skip_header {
786                        skip_header = false;
787                    } else if let [
788                        DecodedField {
789                            start,
790                            end,
791                            quoted: false,
792                        },
793                    ] = fields[..]
794                        && &output[start..end] == END_OF_COPY_MARKER
795                    {
796                        // Bare `\.` on its own line: end-of-copy marker. A
797                        // quoted `"\."` also decodes to `\.` but is data, so
798                        // only an unquoted match terminates the import.
799                        return Ok(rows);
800                    } else {
801                        match fields.len().cmp(&column_types.len()) {
802                            std::cmp::Ordering::Less => {
803                                return Err(io::Error::new(
804                                    io::ErrorKind::InvalidData,
805                                    "missing data for column",
806                                ));
807                            }
808                            std::cmp::Ordering::Greater => {
809                                return Err(io::Error::new(
810                                    io::ErrorKind::InvalidData,
811                                    "extra data after last expected column",
812                                ));
813                            }
814                            std::cmp::Ordering::Equal => {}
815                        }
816
817                        let mut row_builder = SharedRow::get();
818                        let mut row_packer = row_builder.packer();
819                        for (typ, field) in column_types.iter().zip_eq(fields.iter()) {
820                            let raw_value = &output[field.start..field.end];
821                            if !field.quoted && raw_value == null_as_bytes {
822                                row_packer.push(Datum::Null);
823                            } else {
824                                let s = match std::str::from_utf8(raw_value) {
825                                    Ok(s) => s,
826                                    Err(err) => {
827                                        let msg = format!("invalid utf8 data in column: {}", err);
828                                        return Err(io::Error::new(
829                                            io::ErrorKind::InvalidData,
830                                            msg,
831                                        ));
832                                    }
833                                };
834                                if let Err(err) =
835                                    mz_pgrepr::Value::decode_text_into_row(typ, s, &mut row_packer)
836                                {
837                                    let msg = format!("unable to decode column: {}", err);
838                                    return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
839                                }
840                            }
841                        }
842                        rows.push(row_builder.clone());
843                    }
844                    fields.clear();
845                    out_pos = 0;
846                }
847            }
848            csv_core::ReadFieldResult::OutputFull => {
849                let new_len = output.len().saturating_mul(2).max(out_pos + 1);
850                output.resize(new_len, 0);
851            }
852            csv_core::ReadFieldResult::InputEmpty => {
853                // InputEmpty means csv-core consumed all our input mid-field
854                // and wants more. We have no more to give, so the next
855                // iteration calls it with the now-empty slice; per its
856                // documented termination protocol, that yields the partial
857                // field (if any) as a final `Field`, then `End`.
858            }
859            csv_core::ReadFieldResult::End => return Ok(rows),
860        }
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use mz_ore::collections::CollectionExt;
867    use mz_repr::SqlColumnType;
868    use proptest::prelude::*;
869
870    use super::*;
871
872    #[mz_ore::test]
873    fn test_copy_format_text_parser() {
874        let text = "\t\\nt e\t\\N\t\n\\x60\\xA\\x7D\\x4a\n\\44\\044\\123".as_bytes();
875        let mut parser = CopyTextFormatParser::new(text, b'\t', "\\N");
876        assert!(parser.is_column_delimiter());
877        parser
878            .expect_column_delimiter()
879            .expect("expected column delimiter");
880        assert_eq!(
881            parser
882                .consume_raw_value()
883                .expect("unexpected error")
884                .expect("unexpected empty result"),
885            "\nt e".as_bytes()
886        );
887        parser
888            .expect_column_delimiter()
889            .expect("expected column delimiter");
890        // null value
891        assert!(
892            parser
893                .consume_raw_value()
894                .expect("unexpected error")
895                .is_none()
896        );
897        parser
898            .expect_column_delimiter()
899            .expect("expected column delimiter");
900        assert!(parser.is_end_of_line());
901        parser.expect_end_of_line().expect("expected eol");
902        // hex value
903        assert_eq!(
904            parser
905                .consume_raw_value()
906                .expect("unexpected error")
907                .expect("unexpected empty result"),
908            "`\n}J".as_bytes()
909        );
910        parser.expect_end_of_line().expect("expected eol");
911        // octal value
912        assert_eq!(
913            parser
914                .consume_raw_value()
915                .expect("unexpected error")
916                .expect("unexpected empty result"),
917            "$$S".as_bytes()
918        );
919        assert!(parser.is_eof());
920    }
921
922    #[mz_ore::test]
923    fn test_copy_format_text_empty_null_string() {
924        let text = "\t\n10\t20\n30\t\n40\t".as_bytes();
925        let expect = vec![
926            vec![None, None],
927            vec![Some("10"), Some("20")],
928            vec![Some("30"), None],
929            vec![Some("40"), None],
930        ];
931        let mut parser = CopyTextFormatParser::new(text, b'\t', "");
932        for line in expect {
933            for (i, value) in line.iter().enumerate() {
934                if i > 0 {
935                    parser
936                        .expect_column_delimiter()
937                        .expect("expected column delimiter");
938                }
939                match value {
940                    Some(s) => {
941                        assert!(!parser.consume_null_string());
942                        assert_eq!(
943                            parser
944                                .consume_raw_value()
945                                .expect("unexpected error")
946                                .expect("unexpected empty result"),
947                            s.as_bytes()
948                        );
949                    }
950                    None => {
951                        assert!(parser.consume_null_string());
952                    }
953                }
954            }
955            parser.expect_end_of_line().expect("expected eol");
956        }
957    }
958
959    #[mz_ore::test]
960    fn test_copy_format_text_parser_escapes() {
961        struct TestCase {
962            input: &'static str,
963            expect: &'static [u8],
964        }
965        let tests = vec![
966            TestCase {
967                input: "simple",
968                expect: b"simple",
969            },
970            TestCase {
971                input: r#"new\nline"#,
972                expect: b"new\nline",
973            },
974            TestCase {
975                input: r#"\b\f\n\r\t\v\\"#,
976                expect: b"\x08\x0c\n\r\t\x0b\\",
977            },
978            TestCase {
979                input: r#"\0\12\123"#,
980                expect: &[0, 0o12, 0o123],
981            },
982            TestCase {
983                input: r#"\x1\xaf"#,
984                expect: &[0x01, 0xaf],
985            },
986            TestCase {
987                input: r#"T\n\07\xEV\x0fA\xb2C\1"#,
988                expect: b"T\n\x07\x0eV\x0fA\xb2C\x01",
989            },
990            TestCase {
991                input: r#"\\\""#,
992                expect: b"\\\"",
993            },
994            TestCase {
995                input: r#"\x"#,
996                expect: b"x",
997            },
998            TestCase {
999                input: r#"\xg"#,
1000                expect: b"xg",
1001            },
1002            TestCase {
1003                input: r#"\"#,
1004                expect: b"\\",
1005            },
1006            TestCase {
1007                input: r#"\8"#,
1008                expect: b"8",
1009            },
1010            TestCase {
1011                input: r#"\a"#,
1012                expect: b"a",
1013            },
1014            TestCase {
1015                input: r#"\x\xg\8\xH\x32\s\"#,
1016                expect: b"xxg8xH2s\\",
1017            },
1018        ];
1019
1020        for test in tests {
1021            let mut parser = CopyTextFormatParser::new(test.input.as_bytes(), b'\t', "\\N");
1022            assert_eq!(
1023                parser
1024                    .consume_raw_value()
1025                    .expect("unexpected error")
1026                    .expect("unexpected empty result"),
1027                test.expect,
1028                "input: {}, expect: {:?}",
1029                test.input,
1030                std::str::from_utf8(test.expect),
1031            );
1032            assert!(parser.is_eof());
1033        }
1034    }
1035
1036    #[mz_ore::test]
1037    fn test_copy_csv_format_params() {
1038        assert_eq!(
1039            CopyCsvFormatParams::try_new(Some(b't'), Some(b'q'), None, None, None),
1040            Ok(CopyCsvFormatParams {
1041                delimiter: b't',
1042                quote: b'q',
1043                escape: b'q',
1044                header: false,
1045                null: Cow::from(""),
1046            })
1047        );
1048
1049        assert_eq!(
1050            CopyCsvFormatParams::try_new(
1051                Some(b't'),
1052                Some(b'q'),
1053                Some(b'e'),
1054                Some(true),
1055                Some("null".to_string())
1056            ),
1057            Ok(CopyCsvFormatParams {
1058                delimiter: b't',
1059                quote: b'q',
1060                escape: b'e',
1061                header: true,
1062                null: Cow::from("null"),
1063            })
1064        );
1065
1066        assert_eq!(
1067            CopyCsvFormatParams::try_new(
1068                None,
1069                Some(b','),
1070                Some(b'e'),
1071                Some(true),
1072                Some("null".to_string())
1073            ),
1074            Err("COPY delimiter and quote must be different".to_string())
1075        );
1076    }
1077
1078    #[mz_ore::test]
1079    fn test_copy_csv_row() -> Result<(), io::Error> {
1080        let mut row = Row::default();
1081        let mut packer = row.packer();
1082        packer.push(Datum::from("1,2,\"3\""));
1083        packer.push(Datum::Null);
1084        packer.push(Datum::from(1000u64));
1085        packer.push(Datum::from("qe")); // overridden quote and escape character in test below
1086        packer.push(Datum::from(""));
1087
1088        let typ: SqlRelationType = SqlRelationType::new(vec![
1089            SqlColumnType {
1090                scalar_type: mz_repr::SqlScalarType::String,
1091                nullable: false,
1092            },
1093            SqlColumnType {
1094                scalar_type: mz_repr::SqlScalarType::String,
1095                nullable: true,
1096            },
1097            SqlColumnType {
1098                scalar_type: mz_repr::SqlScalarType::UInt64,
1099                nullable: false,
1100            },
1101            SqlColumnType {
1102                scalar_type: mz_repr::SqlScalarType::String,
1103                nullable: false,
1104            },
1105            SqlColumnType {
1106                scalar_type: mz_repr::SqlScalarType::String,
1107                nullable: false,
1108            },
1109        ]);
1110
1111        let mut out = Vec::new();
1112
1113        struct TestCase<'a> {
1114            params: CopyCsvFormatParams<'a>,
1115            expected: &'static [u8],
1116        }
1117
1118        let tests = [
1119            TestCase {
1120                params: CopyCsvFormatParams::default(),
1121                expected: b"\"1,2,\"\"3\"\"\",,1000,qe,\"\"\n",
1122            },
1123            TestCase {
1124                params: CopyCsvFormatParams {
1125                    null: Cow::from("NULL"),
1126                    quote: b'q',
1127                    escape: b'e',
1128                    ..Default::default()
1129                },
1130                expected: b"q1,2,\"3\"q,NULL,1000,qeqeeq,\n",
1131            },
1132        ];
1133
1134        for TestCase { params, expected } in tests {
1135            out.clear();
1136            let params = CopyFormatParams::Csv(params);
1137            let _ = encode_copy_format(&params, &row, &typ, &mut out, TextEncodeSettings::STABLE);
1138            let output = std::str::from_utf8(&out);
1139            assert_eq!(output, std::str::from_utf8(expected));
1140        }
1141
1142        Ok(())
1143    }
1144
1145    #[mz_ore::test]
1146    fn test_decode_copy_format_csv_end_marker() {
1147        // Bare `\.` on its own line terminates the COPY. A quoted `"\."`
1148        // decodes to the same bytes but must be treated as data. This must
1149        // hold for every line ending (LF, CRLF, CR): csv-core places a CRLF
1150        // record boundary between `\r` and `\n`, so a naive raw-byte check is
1151        // fooled by the orphaned terminator bytes on CRLF/CR input.
1152        let column_types = vec![mz_pgrepr::Type::from(&mz_repr::SqlScalarType::String)];
1153
1154        let decode_strings = |input: &[u8]| -> Vec<String> {
1155            decode_copy_format_csv(input, &column_types, CopyCsvFormatParams::default())
1156                .expect("decode should succeed")
1157                .iter()
1158                .map(|r| match r.iter().next().unwrap() {
1159                    Datum::String(s) => s.to_owned(),
1160                    d => panic!("unexpected datum: {:?}", d),
1161                })
1162                .collect()
1163        };
1164
1165        for eol in [&b"\n"[..], b"\r\n", b"\r"] {
1166            let join = |lines: &[&str]| -> Vec<u8> {
1167                let mut out = Vec::new();
1168                for line in lines {
1169                    out.extend_from_slice(line.as_bytes());
1170                    out.extend_from_slice(eol);
1171                }
1172                out
1173            };
1174
1175            // Quoted "\." is data — all three rows are imported.
1176            assert_eq!(
1177                decode_strings(&join(&["before", "\"\\.\"", "after"])),
1178                vec!["before", "\\.", "after"],
1179                "quoted marker, eol={eol:?}"
1180            );
1181
1182            // Bare `\.` terminates the COPY; rows after it are dropped.
1183            assert_eq!(
1184                decode_strings(&join(&["first", "\\.", "ignored"])),
1185                vec!["first"],
1186                "bare marker, eol={eol:?}"
1187            );
1188        }
1189    }
1190
1191    #[mz_ore::test]
1192    fn test_decode_copy_format_csv_leading_orphan() {
1193        // Worker chunks after the first begin at a `\r` boundary, so for CRLF
1194        // input a chunk's bytes start with the orphan `\n` that csv-core left
1195        // behind. The decoder must still classify the first field's quote
1196        // state from its real first byte, not the stray `\n`. Regression test
1197        // for the per-chunk quote probe.
1198        let column_types = vec![
1199            mz_pgrepr::Type::from(&mz_repr::SqlScalarType::String),
1200            mz_pgrepr::Type::from(&mz_repr::SqlScalarType::String),
1201        ];
1202
1203        let rows = decode_copy_format_csv(
1204            b"\n\"\",x\r\ny,z\r\n",
1205            &column_types,
1206            CopyCsvFormatParams::default(),
1207        )
1208        .expect("decode should succeed");
1209        let got: Vec<Vec<Datum>> = rows.iter().map(|r| r.iter().collect()).collect();
1210        assert_eq!(got.len(), 2);
1211        // Quoted empty first field on the leading-orphan record must decode to
1212        // the empty string, not SQL NULL (the bug would misread it as
1213        // unquoted and match the default empty NULL marker).
1214        assert_eq!(got[0][0], Datum::String(""));
1215        assert_eq!(got[0][1], Datum::String("x"));
1216        assert_eq!(got[1][0], Datum::String("y"));
1217        assert_eq!(got[1][1], Datum::String("z"));
1218    }
1219
1220    #[mz_ore::test]
1221    fn test_decode_copy_format_csv_quoted_null() {
1222        // PG COPY ... FORMAT CSV distinguishes quoted vs unquoted NULL
1223        // markers: unquoted → SQL NULL, quoted → the literal string.
1224        let column_types = vec![
1225            mz_pgrepr::Type::from(&mz_repr::SqlScalarType::String),
1226            mz_pgrepr::Type::from(&mz_repr::SqlScalarType::String),
1227        ];
1228
1229        // Lines as (col_a, col_b) literals, joined per-eol below. The quoted
1230        // vs unquoted distinction must survive CRLF/CR line endings, where
1231        // csv-core leaves an orphaned terminator byte at the start of every
1232        // non-first record.
1233        let cases: &[(CopyCsvFormatParams, &[(&str, &str)], &[[Option<&str>; 2]])] = &[
1234            // Default params: NULL marker is empty string.
1235            (
1236                CopyCsvFormatParams::default(),
1237                &[("a", ""), ("b", "\"\""), ("\"\"", "c")],
1238                &[
1239                    [Some("a"), None],
1240                    [Some("b"), Some("")],
1241                    [Some(""), Some("c")],
1242                ],
1243            ),
1244            // Custom NULL marker "NULL".
1245            (
1246                CopyCsvFormatParams {
1247                    null: Cow::from("NULL"),
1248                    ..Default::default()
1249                },
1250                &[("a", "NULL"), ("b", "\"NULL\""), ("NULL", "c")],
1251                &[
1252                    [Some("a"), None],
1253                    [Some("b"), Some("NULL")],
1254                    [None, Some("c")],
1255                ],
1256            ),
1257        ];
1258
1259        for eol in [&b"\n"[..], b"\r\n", b"\r"] {
1260            for (params, lines, expected) in cases {
1261                let mut input = Vec::new();
1262                for (a, b) in *lines {
1263                    input.extend_from_slice(a.as_bytes());
1264                    input.push(b',');
1265                    input.extend_from_slice(b.as_bytes());
1266                    input.extend_from_slice(eol);
1267                }
1268                let rows = decode_copy_format_csv(&input, &column_types, params.clone())
1269                    .expect("decode should succeed");
1270                assert_eq!(rows.len(), expected.len(), "eol={eol:?}");
1271                for (row, want) in rows.iter().zip_eq(expected.iter()) {
1272                    let got: Vec<Datum> = row.iter().collect();
1273                    assert_eq!(got.len(), 2);
1274                    for (g, w) in got.iter().zip_eq(want.iter()) {
1275                        match (g, w) {
1276                            (Datum::Null, None) => {}
1277                            (Datum::String(s), Some(w)) => assert_eq!(s, w, "eol={eol:?}"),
1278                            _ => panic!("mismatch: got {g:?}, want {w:?}, eol={eol:?}"),
1279                        }
1280                    }
1281                }
1282            }
1283        }
1284    }
1285
1286    proptest! {
1287        #[mz_ore::test]
1288        #[cfg_attr(miri, ignore)]
1289        fn proptest_csv_roundtrips(copy_csv_params: CopyCsvFormatParams)  {
1290            // Given a SqlScalarType and Datum roundtrips it through the CSV COPY format.
1291            let try_roundtrip_datum = |scalar_type: &SqlScalarType, datum| {
1292                let row = Row::pack_slice(&[datum]);
1293                let typ = SqlRelationType::new(vec![
1294                    SqlColumnType {
1295                        scalar_type: scalar_type.clone(),
1296                        nullable: true,
1297                    }
1298                ]);
1299
1300                let mut buf = Vec::new();
1301                let mut csv_params = copy_csv_params.clone();
1302                // TODO: Encoding never writes a header.
1303                csv_params.header = false;
1304                let params = CopyFormatParams::Csv(csv_params);
1305
1306                // Roundtrip the Row through our CSV format.
1307                encode_copy_format(&params, &row, &typ, &mut buf, TextEncodeSettings::STABLE)?;
1308                let column_types = typ
1309                    .column_types
1310                    .iter()
1311                    .map(|x| &x.scalar_type)
1312                    .map(mz_pgrepr::Type::from)
1313                    .collect::<Vec<mz_pgrepr::Type>>();
1314                let result = decode_copy_format(&buf, &column_types, params);
1315
1316                match result {
1317                    Ok(rows) => {
1318                        let out_str = std::str::from_utf8(&buf[..]);
1319
1320                        prop_assert_eq!(
1321                            rows.len(),
1322                            1,
1323                            "unexpected number of rows! {:?}, csv string: {:?}", rows, out_str
1324                        );
1325                        let output = rows.into_element();
1326
1327                        prop_assert_eq!(
1328                            row,
1329                            output,
1330                            "csv string: {:?}, scalar_type: {:?}", out_str, scalar_type
1331                        );
1332                    }
1333                    _ => {
1334                        // ignoring decoding failures
1335                    }
1336                }
1337
1338                Ok(())
1339            };
1340
1341            // Try roundtripping all of our interesting Datums.
1342            for scalar_type in SqlScalarType::enumerate() {
1343                for datum in scalar_type.interesting_datums() {
1344                    // TODO: The decoder cannot differentiate between empty string and null.
1345                    if let Some(value) = mz_pgrepr::Value::from_datum(datum, scalar_type) {
1346                        let mut buf = bytes::BytesMut::new();
1347                        value.encode_text(&mut buf, TextEncodeSettings::STABLE);
1348
1349                        if let Ok(datum_str) = std::str::from_utf8(&buf[..]) {
1350                            if datum_str == copy_csv_params.null {
1351                                continue;
1352                            }
1353                        }
1354                    }
1355
1356                    let updated_datum = match datum {
1357                        // TODO: Fix roundtrip decoding of these types.
1358                        Datum::Timestamp(_) | Datum::TimestampTz(_) | Datum::Null => {
1359                            continue;
1360                        }
1361                        Datum::String(s) => {
1362                            // TODO: The decoder cannot differentiate between empty string and null.
1363                            if s.trim() == copy_csv_params.null || s.trim().is_empty() {
1364                                continue;
1365                            } else {
1366                                Datum::String(s)
1367                            }
1368                        }
1369                        other => other,
1370                    };
1371
1372                    let result = try_roundtrip_datum(scalar_type, updated_datum);
1373                    prop_assert!(result.is_ok(), "failure: {result:?}");
1374                }
1375            }
1376        }
1377    }
1378}