Skip to main content

rudb_csv/
reader.rs

1//! A CSV file as chunks.
2//!
3//! The same shape as `rudb-parquet`'s reader on purpose, because the operator above them is the same
4//! operator with a different constructor: open, ask what the columns are, say which of them you
5//! want, then pull chunks until there are none. A caller that can read one can read the other.
6//!
7//! The file is read in blocks and a record that straddles a block boundary is carried into the next
8//! one, so a file larger than memory reads the same as a small one. The sample the sniffer looks at
9//! is the first block, which is also the first block the reader then goes on to use, so opening a
10//! file reads its front once.
11
12use rudb_common::{Error, Field, LogicalType, Result, Value};
13use rudb_io::File;
14use rudb_kernels::cast_value;
15use rudb_vector::{Chunk, VECTOR_SIZE, Vector};
16
17use crate::dialect::{self, Dialect};
18use crate::infer;
19
20/// How much is read at a time, and how much the sniffer gets to look at.
21///
22/// A megabyte holds well over the twenty thousand rows of the sample for any file with ordinary
23/// rows in it, and for a file with enormous rows the sniffer sees fewer of them and says so by
24/// getting a wider type rather than by failing.
25const BLOCK: usize = 1 << 20;
26
27/// A CSV file, positioned at a record boundary.
28#[derive(Debug)]
29pub struct Reader {
30    file: Box<dyn File>,
31    path: String,
32    dialect: Dialect,
33    fields: Vec<Field>,
34    projection: Vec<usize>,
35    buffer: Vec<u8>,
36    at: usize,
37    offset: u64,
38    drained: bool,
39    line: u64,
40    scratch: Vec<String>,
41}
42
43impl Reader {
44    /// Opens a file, works out how it is written, and positions it at the first row.
45    ///
46    /// The path is kept because the error a bad value produces names it, the way DuckDB's does.
47    ///
48    /// # Errors
49    ///
50    /// When the file cannot be read, and when the first block of it does not hold one whole record,
51    /// which is a single line longer than a megabyte and is not a CSV file anybody meant to write.
52    pub fn open(file: Box<dyn File>, path: &str) -> Result<Self> {
53        let mut reader = Self {
54            file,
55            path: path.to_string(),
56            dialect: Dialect::comma_separated(),
57            fields: Vec::new(),
58            projection: Vec::new(),
59            buffer: Vec::new(),
60            at: 0,
61            offset: 0,
62            drained: false,
63            line: 1,
64            scratch: Vec::new(),
65        };
66        reader.fill()?;
67        let sample = reader.buffer.clone();
68        let quote = dialect::quote(&sample);
69        let delimiter = dialect::delimiter(&sample, quote)?;
70        reader.dialect = Dialect { delimiter, quote, escape: quote, header: false };
71        let rows = reader.sample_rows(&sample)?;
72        let (header, fields) = describe(&rows);
73        reader.dialect.header = header;
74        reader.fields = fields;
75        reader.projection = (0..reader.fields.len()).collect();
76        if header {
77            reader.skip_record()?;
78        }
79        Ok(reader)
80    }
81
82    /// The columns this reader will produce, in order.
83    #[must_use]
84    pub fn fields(&self) -> Vec<Field> {
85        self.projection.iter().map(|&at| self.fields[at].clone()).collect()
86    }
87
88    /// Reads only these columns, by position in the file, in this order.
89    ///
90    /// # Errors
91    ///
92    /// When a position is past the end of the file's columns.
93    pub fn project(&mut self, columns: &[usize]) -> Result<()> {
94        for &column in columns {
95            if column >= self.fields.len() {
96                return Err(Error::io(format!(
97                    "column {column} is past the {} the file has",
98                    self.fields.len()
99                )));
100            }
101        }
102        self.projection = columns.to_vec();
103        Ok(())
104    }
105
106    /// How this file is punctuated, which is what the sniffer decided.
107    #[must_use]
108    pub const fn dialect(&self) -> Dialect {
109        self.dialect
110    }
111
112    /// The next chunk, or `None` at the end of the file.
113    ///
114    /// # Errors
115    ///
116    /// A read error, a malformed record, or a value that does not fit the type the sample chose
117    /// for its column.
118    pub fn next_chunk(&mut self) -> Result<Option<Chunk>> {
119        let mut rows: Vec<Vec<Option<String>>> = Vec::new();
120        while rows.len() < VECTOR_SIZE {
121            match self.next_record()? {
122                Some(fields) => rows.push(fields),
123                None => break,
124            }
125        }
126        if rows.is_empty() {
127            return Ok(None);
128        }
129        let mut columns = Vec::with_capacity(self.projection.len());
130        for &at in &self.projection {
131            let field = &self.fields[at];
132            let mut values = Vec::with_capacity(rows.len());
133            for (row, held) in rows.iter().enumerate() {
134                let text = held.get(at).and_then(Option::as_deref);
135                values.push(self.convert(
136                    text,
137                    field,
138                    self.line - rows.len() as u64 + row as u64,
139                )?);
140            }
141            columns.push(Vector::from_values(field.ty.clone(), &values)?);
142        }
143        Ok(Some(Chunk::with_rows(columns, rows.len())?))
144    }
145
146    /// One value, cast from its text to the column's type.
147    fn convert(&self, text: Option<&str>, field: &Field, line: u64) -> Result<Value> {
148        let Some(text) = text else { return Ok(Value::Null) };
149        if field.ty == LogicalType::Varchar {
150            return Ok(Value::Varchar(text.to_string()));
151        }
152        let value = Value::Varchar(text.to_string());
153        match cast_value(&value, &field.ty, false) {
154            Ok(converted) => Ok(converted),
155            Err(_) => Err(Error::conversion(self.conversion_error(text, field, line))),
156        }
157    }
158
159    /// DuckDB's message for a value that does not fit the type its column was sniffed as.
160    ///
161    /// Reproduced whole, including the block of settings at the bottom, because that block is the
162    /// answer to the question the message raises. Somebody reading it wants to know what was
163    /// guessed and how to override the guess, and a shorter message would send them to the
164    /// documentation to find out.
165    fn conversion_error(&self, text: &str, field: &Field, line: u64) -> String {
166        format!(
167            "CSV Error on Line: {line}\nOriginal Line: {text}\nError when converting column \
168             \"{}\". Could not convert string \"{text}\" to '{}'\n\nColumn {} is being converted \
169             as type {}\nThis type was auto-detected from the CSV file.\nPossible solutions:\n* \
170             Override the type for this column manually by setting the type explicitly, e.g., \
171             types={{'{}': 'VARCHAR'}}\n* Set the sample size to a larger value to enable the \
172             auto-detection to scan more values, e.g., sample_size=-1\n* Use a COPY statement to \
173             automatically derive types from an existing table.\n* Check whether the null string \
174             value is set correctly (e.g., nullstr = 'N/A')\n\n  file = {}\n  delimiter = {} \
175             (Auto-Detected)\n  quote = {} (Auto-Detected)\n  escape = {} (Auto-Detected)\n  \
176             header = {} (Auto-Detected)\n  sample_size = {}\n",
177            field.name,
178            field.ty,
179            field.name,
180            field.ty,
181            field.name,
182            self.path,
183            Dialect::shown(Some(self.dialect.delimiter)),
184            Dialect::shown(self.dialect.quote),
185            Dialect::shown(self.dialect.escape),
186            self.dialect.header,
187            infer::SAMPLE,
188        )
189    }
190
191    /// The next record, as one entry per field, with an empty field as a null.
192    fn next_record(&mut self) -> Result<Option<Vec<Option<String>>>> {
193        let Some(()) = self.advance()? else { return Ok(None) };
194        Ok(Some(
195            self.scratch
196                .iter()
197                .map(|text| if text.is_empty() { None } else { Some(text.clone()) })
198                .collect(),
199        ))
200    }
201
202    /// Reads one record into the scratch, filling the buffer when it has to.
203    fn advance(&mut self) -> Result<Option<()>> {
204        loop {
205            let mut scratch = std::mem::take(&mut self.scratch);
206            let outcome = crate::scan::record(
207                &self.buffer,
208                self.at,
209                self.dialect,
210                self.drained,
211                &mut scratch,
212            );
213            self.scratch = scratch;
214            match outcome? {
215                Some(next) => {
216                    self.at = next;
217                    self.line += 1;
218                    return Ok(Some(()));
219                }
220                None if self.drained => return Ok(None),
221                None => self.fill()?,
222            }
223        }
224    }
225
226    /// Reads one record and throws it away, which is what a header is.
227    fn skip_record(&mut self) -> Result<()> {
228        self.advance()?;
229        Ok(())
230    }
231
232    /// Drops what has been read and reads another block onto the end.
233    fn fill(&mut self) -> Result<()> {
234        self.buffer.drain(..self.at);
235        self.at = 0;
236        let held = self.buffer.len();
237        self.buffer.resize(held + BLOCK, 0);
238        let read = self.file.read_at(self.offset, &mut self.buffer[held..])?;
239        self.buffer.truncate(held + read);
240        self.offset += read as u64;
241        if read == 0 {
242            self.drained = true;
243        }
244        Ok(())
245    }
246
247    /// The records the sniffer gets to look at, which is the sample or the file, whichever is
248    /// shorter.
249    fn sample_rows(&self, sample: &[u8]) -> Result<Vec<Vec<Option<String>>>> {
250        let mut rows = Vec::new();
251        let mut fields = Vec::new();
252        let mut at = 0;
253        while rows.len() <= infer::SAMPLE {
254            // The end of the block is not the end of the file, so a record the block cut in half is
255            // simply not part of the sample.
256            let Some(next) = crate::scan::record(sample, at, self.dialect, false, &mut fields)?
257            else {
258                break;
259            };
260            at = next;
261            rows.push(
262                fields
263                    .iter()
264                    .map(|text| if text.is_empty() { None } else { Some(text.clone()) })
265                    .collect(),
266            );
267        }
268        Ok(rows)
269    }
270}
271
272/// Whether the first row is a header, and what the columns are called and typed.
273///
274/// The rule is DuckDB's and both halves of it were measured. A file whose columns are all `VARCHAR`
275/// once the first row is set aside has a header, because two rows of words is a header and a row.
276/// Otherwise the first row is a header exactly when it does not fit the types the rest of the file
277/// has, which is what makes `1,2` over `3,4` a file of two rows and `a,b` over `1,2` a file of one.
278fn describe(rows: &[Vec<Option<String>>]) -> (bool, Vec<Field>) {
279    let width = rows.iter().map(Vec::len).max().unwrap_or(0);
280    let body = types(&rows[1.min(rows.len())..], width);
281    let all_text = body.iter().all(|ty| *ty == LogicalType::Varchar);
282    let first_fits = rows.first().is_some_and(|first| {
283        first.iter().zip(&body).all(|(text, ty)| match text {
284            None => true,
285            Some(text) => infer::fits(text, ty),
286        })
287    });
288    let header = rows.len() > 1 && (all_text || !first_fits);
289    if !header {
290        let types = types(rows, width);
291        let fields = types
292            .into_iter()
293            .enumerate()
294            .map(|(at, ty)| Field::new(format!("column{at}"), ty))
295            .collect();
296        return (false, fields);
297    }
298    let names = unique(&rows[0], width);
299    let fields = body.into_iter().zip(names).map(|(ty, name)| Field::new(name, ty)).collect();
300    (true, fields)
301}
302
303/// The column names a header row gives, with the collisions resolved the way DuckDB resolves them.
304///
305/// A header is text somebody typed and nothing stops it naming two columns the same thing, so the
306/// second one gets `_1`, and the count goes up until the name is free. It has to count rather than
307/// stop at one, because the suffix can collide too: a file whose header is `a,a,a_1` comes back as
308/// `a`, `a_1`, `a_1_1` from the binary, and it is the second column that took the name the third one
309/// was written with.
310///
311/// The comparison ignores case and the written case is kept, which was measured: `a,a,A` comes back
312/// as `a`, `a_1`, `A_2`, so `A` collided with `a` and then `A_1` collided with `a_1`. An empty
313/// header cell is a column with no name, and it falls back to the generated one rather than to an
314/// empty string that no query could write.
315fn unique(header: &[Option<String>], width: usize) -> Vec<String> {
316    let mut taken: Vec<String> = Vec::with_capacity(width);
317    for at in 0..width {
318        let base = match header.get(at).and_then(Option::as_deref) {
319            Some(written) => written.to_string(),
320            None => format!("column{at}"),
321        };
322        let mut name = base.clone();
323        let mut next = 1;
324        while taken.iter().any(|held| held.eq_ignore_ascii_case(&name)) {
325            name = format!("{base}_{next}");
326            next += 1;
327        }
328        taken.push(name);
329    }
330    taken
331}
332
333/// The type of each of `width` columns, over these rows.
334fn types(rows: &[Vec<Option<String>>], width: usize) -> Vec<LogicalType> {
335    (0..width)
336        .map(|at| {
337            let values: Vec<Option<&str>> =
338                rows.iter().map(|row| row.get(at).and_then(Option::as_deref)).collect();
339            infer::column(&values)
340        })
341        .collect()
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use rudb_io::{Filesystem, OpenMode, SimFilesystem};
348    use std::path::Path;
349
350    fn read(text: &str) -> Reader {
351        let filesystem = SimFilesystem::new();
352        let path = Path::new("/t.csv");
353        let file = filesystem.open(path, OpenMode::Create).expect("creates");
354        file.write_at(0, text.as_bytes()).expect("writes");
355        drop(file);
356        let file = filesystem.open(path, OpenMode::Read).expect("opens");
357        Reader::open(file, "/t.csv").expect("sniffs")
358    }
359
360    fn names_and_types(reader: &Reader) -> Vec<(String, String)> {
361        reader.fields().iter().map(|f| (f.name.clone(), f.ty.to_string())).collect()
362    }
363
364    fn all(reader: &mut Reader) -> Vec<Vec<Value>> {
365        let mut rows = Vec::new();
366        while let Some(chunk) = reader.next_chunk().expect("reads") {
367            for row in 0..chunk.len() {
368                rows.push((0..chunk.width()).map(|at| chunk.value_at(row, at)).collect());
369            }
370        }
371        rows
372    }
373
374    #[test]
375    fn a_header_that_names_two_columns_the_same_thing_counts_the_second_one_up() {
376        let names: Vec<String> =
377            read("a,a,A,a_1\n1,2,3,4\nx,y,z,w\n").fields().into_iter().map(|f| f.name).collect();
378        // Measured against the binary, all four of them. The last one is the interesting one: the
379        // second column took `a_1`, which is the name the fourth column was written with, so the
380        // fourth has to keep counting from its own name rather than from `a`.
381        assert_eq!(names, ["a", "a_1", "A_2", "a_1_1"]);
382    }
383
384    #[test]
385    fn a_header_and_three_types_are_what_duckdb_sniffs_for_the_same_bytes() {
386        let reader = read("a,b,c\n1,x,2.5\n2,y,3.5\n");
387        assert_eq!(
388            names_and_types(&reader),
389            [
390                ("a".to_string(), "BIGINT".to_string()),
391                ("b".to_string(), "VARCHAR".to_string()),
392                ("c".to_string(), "DOUBLE".to_string()),
393            ]
394        );
395    }
396
397    #[test]
398    fn a_file_with_no_header_gets_the_names_duckdb_gives_it() {
399        let reader = read("1,x\n2,y\n");
400        assert_eq!(
401            names_and_types(&reader),
402            [
403                ("column0".to_string(), "BIGINT".to_string()),
404                ("column1".to_string(), "VARCHAR".to_string()),
405            ]
406        );
407    }
408
409    #[test]
410    fn two_rows_of_words_are_a_header_and_a_row() {
411        let reader = read("a,b\nc,d\n");
412        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["a", "b"]);
413    }
414
415    #[test]
416    fn one_column_of_words_under_a_row_of_numbers_is_still_a_header() {
417        // `a,2` over `3,4`. One column disagreeing is enough, and the second column is then named
418        // `2`, which is the text that was in it.
419        let reader = read("a,2\n3,4\n");
420        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["a", "2"]);
421    }
422
423    #[test]
424    fn the_rows_are_the_rows_of_the_file() {
425        let mut reader = read("a,b\n1,x\n2,y\n");
426        assert_eq!(
427            all(&mut reader),
428            [
429                vec![Value::BigInt(1), Value::Varchar("x".into())],
430                vec![Value::BigInt(2), Value::Varchar("y".into())],
431            ]
432        );
433    }
434
435    #[test]
436    fn an_empty_field_is_a_null_whether_it_was_quoted_or_not() {
437        // Measured. `allow_quoted_nulls` is on by default, so `""` is a null and not the empty
438        // string, which is the one place a quoted field and a bare one agree about being nothing.
439        let mut reader = read("a,b\n1,\n\"\",y\n");
440        assert_eq!(
441            all(&mut reader),
442            [vec![Value::BigInt(1), Value::Null], vec![Value::Null, Value::Varchar("y".into())],]
443        );
444    }
445
446    #[test]
447    fn a_projection_picks_columns_out_by_position_and_can_reorder_them() {
448        let mut reader = read("a,b,c\n1,x,2.5\n");
449        reader.project(&[2, 0]).expect("projects");
450        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["c", "a"]);
451        assert_eq!(all(&mut reader), [vec![Value::Double(2.5), Value::BigInt(1)]]);
452    }
453
454    #[test]
455    fn a_projection_of_nothing_still_counts_the_rows() {
456        let mut reader = read("a,b\n1,x\n2,y\n3,z\n");
457        reader.project(&[]).expect("projects");
458        let chunk = reader.next_chunk().expect("reads").expect("a chunk");
459        assert_eq!(chunk.len(), 3);
460        assert_eq!(chunk.width(), 0);
461    }
462
463    #[test]
464    fn a_pipe_separated_file_reads_as_one() {
465        let mut reader = read("a|b\n1|x\n");
466        assert_eq!(reader.dialect().delimiter, b'|');
467        assert_eq!(all(&mut reader), [vec![Value::BigInt(1), Value::Varchar("x".into())]]);
468    }
469
470    #[test]
471    fn a_quoted_field_with_a_delimiter_in_it_is_one_value() {
472        let mut reader = read("a,b\n1,\"x,y\"\n");
473        assert_eq!(all(&mut reader), [vec![Value::BigInt(1), Value::Varchar("x,y".into())]]);
474    }
475
476    #[test]
477    fn more_rows_than_fit_one_chunk_arrive_as_more_than_one_chunk() {
478        let mut text = String::from("a\n");
479        for row in 0..VECTOR_SIZE + 5 {
480            text.push_str(&format!("{row}\n"));
481        }
482        let mut reader = read(&text);
483        let first = reader.next_chunk().expect("reads").expect("a chunk");
484        assert_eq!(first.len(), VECTOR_SIZE);
485        let second = reader.next_chunk().expect("reads").expect("a second chunk");
486        assert_eq!(second.len(), 5);
487        assert!(reader.next_chunk().expect("reads").is_none());
488    }
489
490    #[test]
491    fn a_value_the_sniffer_never_saw_is_an_error_rather_than_a_wider_column() {
492        // The value has to be past the sample, because a value inside it would have widened the
493        // column to VARCHAR and there would be nothing to fail. Widening after the fact is not an
494        // option: the chunks before this one have already gone out with the narrow type on them.
495        let mut text = String::from("c\n");
496        for row in 0..infer::SAMPLE {
497            text.push_str(&format!("{row}\n"));
498        }
499        text.push_str("oops\n");
500        let mut reader = read(&text);
501        assert_eq!(reader.fields()[0].ty, LogicalType::BigInt);
502        let error = all_or_error(&mut reader).unwrap_err();
503        let line = infer::SAMPLE + 2;
504        assert!(error.message().starts_with(&format!("CSV Error on Line: {line}")), "{error}");
505        assert!(
506            error.message().contains("Could not convert string \"oops\" to 'BIGINT'"),
507            "{error}"
508        );
509        assert!(error.message().contains("sample_size = 20480"), "{error}");
510    }
511
512    fn all_or_error(reader: &mut Reader) -> Result<Vec<Vec<Value>>> {
513        let mut rows = Vec::new();
514        while let Some(chunk) = reader.next_chunk()? {
515            for row in 0..chunk.len() {
516                rows.push((0..chunk.width()).map(|at| chunk.value_at(row, at)).collect());
517            }
518        }
519        Ok(rows)
520    }
521}