Skip to main content

rudb_csv/
scan.rs

1//! Bytes into fields.
2//!
3//! One function, which reads one record. It is separate from the reader above it because the
4//! sniffer runs it too: working out the delimiter means splitting the sample under each candidate
5//! and seeing which one gives every line the same number of fields, and that has to be the same
6//! splitting the reader will do or the sniffer answers a question nobody asked.
7//!
8//! A record ends at a newline that is not inside a quoted field, and the three line endings are all
9//! accepted whatever the file mostly uses. DuckDB reports the one it found and reads either, and a
10//! file that a text editor has half converted is a real thing.
11//!
12//! Fields arrive as owned strings. That is a copy per field and it is the wrong shape for a fast
13//! scan, which wants a range into the buffer and a copy only for the fields that had an escape in
14//! them. The borrowed form is the change to make when there is a number saying it matters, and it is
15//! a change to this one function.
16
17use rudb_common::{Error, Result};
18
19use crate::dialect::Dialect;
20
21/// Reads the record starting at `from` into `out`, and answers where the next one starts.
22///
23/// `None` means the buffer does not hold a whole record: either it ran out mid line and more bytes
24/// may follow, or a quoted field was left open. With `eof` set there are no more bytes, so a last
25/// line with no newline on the end is a record like any other and only an open quote is short.
26///
27/// # Errors
28///
29/// When a quoted field has something other than a delimiter or a line ending after its closing
30/// quote, which is a file that is not the file it claims to be and reading past it would invent
31/// data.
32pub fn record(
33    bytes: &[u8],
34    from: usize,
35    dialect: Dialect,
36    eof: bool,
37    out: &mut Vec<String>,
38) -> Result<Option<usize>> {
39    if from >= bytes.len() {
40        // No bytes left is no record, and it is not a record of one empty field. The difference
41        // matters at the end of a file, where a reader that took the second reading would hand back
42        // an empty row forever, and it matters for the last line of a file that ends in a newline,
43        // which is not a row of nothing.
44        return Ok(None);
45    }
46    let quote = dialect.quote_byte();
47    let escape = dialect.escape_byte();
48    let mut at = from;
49    let mut count = 0;
50    let mut field = Vec::new();
51    loop {
52        field.clear();
53        if bytes.get(at) == Some(&quote) {
54            at += 1;
55            loop {
56                let Some(&byte) = bytes.get(at) else { return Ok(None) };
57                if byte == escape && bytes.get(at + 1) == Some(&quote) {
58                    field.push(quote);
59                    at += 2;
60                    continue;
61                }
62                if byte == quote {
63                    at += 1;
64                    break;
65                }
66                field.push(byte);
67                at += 1;
68            }
69            match bytes.get(at) {
70                None if !eof => return Ok(None),
71                None => {}
72                Some(&byte) if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' => {}
73                Some(&byte) => {
74                    return Err(Error::io(format!(
75                        "a quoted value is followed by '{}' rather than by a delimiter or the end \
76                         of the line",
77                        byte as char
78                    )));
79                }
80            }
81        } else {
82            while let Some(&byte) = bytes.get(at) {
83                if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' {
84                    break;
85                }
86                field.push(byte);
87                at += 1;
88            }
89            if at >= bytes.len() && !eof {
90                return Ok(None);
91            }
92        }
93        place(out, count, &field);
94        count += 1;
95        match bytes.get(at) {
96            Some(&byte) if byte == dialect.delimiter => at += 1,
97            Some(b'\r') => {
98                at += 1;
99                if bytes.get(at) == Some(&b'\n') {
100                    at += 1;
101                } else if at >= bytes.len() && !eof {
102                    // A trailing carriage return may be the first half of a `\r\n` that has not
103                    // arrived, and guessing wrong here splits one record into two.
104                    return Ok(None);
105                }
106                break;
107            }
108            Some(b'\n') => {
109                at += 1;
110                break;
111            }
112            Some(_) => unreachable!("a field stops at a delimiter, a line ending or the end"),
113            None => break,
114        }
115    }
116    out.truncate(count);
117    Ok(Some(at))
118}
119
120/// Writes the field at `at`, reusing the string that is already there.
121///
122/// A CSV file is bytes and the encoding is not stated anywhere in it. UTF-8 comes through unchanged
123/// because no byte of a multi byte sequence can be a delimiter or a quote. A byte that is not valid
124/// UTF-8 becomes the replacement character rather than an error, which is what one bad byte in a
125/// text file deserves, and which is also what DuckDB does when it is not told to check.
126fn place(out: &mut Vec<String>, at: usize, field: &[u8]) {
127    let text = String::from_utf8_lossy(field);
128    match out.get_mut(at) {
129        Some(held) => {
130            held.clear();
131            held.push_str(&text);
132        }
133        None => out.push(text.into_owned()),
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn split(bytes: &[u8], dialect: Dialect) -> Vec<Vec<String>> {
142        let mut rows = Vec::new();
143        let mut fields = Vec::new();
144        let mut at = 0;
145        while at < bytes.len() {
146            let next = record(bytes, at, dialect, true, &mut fields)
147                .expect("splits")
148                .expect("a whole record");
149            rows.push(fields.clone());
150            at = next;
151        }
152        rows
153    }
154
155    fn comma() -> Dialect {
156        Dialect { delimiter: b',', quote: Some(b'"'), escape: Some(b'"'), header: true }
157    }
158
159    #[test]
160    fn a_line_of_fields_is_the_fields_of_that_line() {
161        assert_eq!(split(b"a,b,c\n1,2,3\n", comma()), [["a", "b", "c"], ["1", "2", "3"]]);
162    }
163
164    #[test]
165    fn the_last_line_does_not_need_a_newline_on_it() {
166        assert_eq!(split(b"a,b\n1,2", comma()), [["a", "b"], ["1", "2"]]);
167    }
168
169    #[test]
170    fn all_three_line_endings_end_a_line() {
171        assert_eq!(split(b"a\r\nb\rc\n", comma()), [["a"], ["b"], ["c"]]);
172    }
173
174    #[test]
175    fn a_quoted_field_may_hold_the_delimiter_and_a_newline() {
176        assert_eq!(split(b"1,\"x,y\"\n", comma()), [["1", "x,y"]]);
177        assert_eq!(split(b"1,\"x\ny\"\n", comma()), [["1", "x\ny"]]);
178    }
179
180    #[test]
181    fn a_doubled_quote_inside_a_quoted_field_is_one_quote() {
182        assert_eq!(split(b"1,\"say \"\"hi\"\"\"\n", comma()), [["1", "say \"hi\""]]);
183    }
184
185    #[test]
186    fn an_empty_field_is_an_empty_string_here_and_becomes_a_null_above() {
187        assert_eq!(split(b"1,,3\n", comma()), [["1", "", "3"]]);
188        assert_eq!(split(b"1,\"\",3\n", comma()), [["1", "", "3"]]);
189    }
190
191    #[test]
192    fn a_trailing_delimiter_makes_a_last_empty_field() {
193        assert_eq!(split(b"1|x|\n", Dialect { delimiter: b'|', ..comma() }), [["1", "x", ""]]);
194    }
195
196    #[test]
197    fn a_quote_in_the_middle_of_a_bare_field_is_just_a_character() {
198        assert_eq!(split(b"1,he said \"hi\"\n", comma()), [["1", "he said \"hi\""]]);
199    }
200
201    #[test]
202    fn a_record_that_the_buffer_does_not_hold_all_of_is_not_a_record_yet() {
203        let mut fields = Vec::new();
204        assert_eq!(record(b"a,b", 0, comma(), false, &mut fields).unwrap(), None);
205        assert_eq!(record(b"a,\"b", 0, comma(), true, &mut fields).unwrap(), None);
206        assert_eq!(record(b"a,b\n", 0, comma(), false, &mut fields).unwrap(), Some(4));
207    }
208
209    #[test]
210    fn rubbish_after_a_closing_quote_is_an_error_rather_than_a_guess() {
211        let mut fields = Vec::new();
212        let error = record(b"\"x\"y,2\n", 0, comma(), true, &mut fields).unwrap_err();
213        assert!(error.message().contains("quoted value"), "{error}");
214    }
215
216    #[test]
217    fn utf8_survives_being_read_one_byte_at_a_time() {
218        assert_eq!(split("a,héllo\n".as_bytes(), comma()), [["a", "héllo"]]);
219    }
220}