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    /// Reads the projected columns as these types rather than as the ones the sample chose.
107    ///
108    /// A read that covers several files produces one stream and a stream has one schema, and no
109    /// single file's sample is that schema. Every file is sniffed on its own and the answers are
110    /// combined by [`crate::across`], so each file is then told what the whole read settled on,
111    /// including the first one. Without it a file whose column happens to hold nothing but whole
112    /// numbers hands up a BIGINT column into a stream that is DOUBLE because some other file in the
113    /// set held a decimal.
114    ///
115    /// This is not a cast of what was read. The type is what the text is converted with, so saying
116    /// it before any row is read converts once rather than converting to the wrong type and again to
117    /// the right one. A value that then does not fit is the conversion error, named and lined the
118    /// way any other one is.
119    ///
120    /// # Errors
121    ///
122    /// When the list is not as long as the projection.
123    pub fn retype(&mut self, types: &[LogicalType]) -> Result<()> {
124        if types.len() != self.projection.len() {
125            return Err(Error::io(format!(
126                "{} types for a projection of {} columns",
127                types.len(),
128                self.projection.len()
129            )));
130        }
131        for (&at, ty) in self.projection.iter().zip(types) {
132            self.fields[at].ty = ty.clone();
133        }
134        Ok(())
135    }
136
137    /// How this file is punctuated, which is what the sniffer decided.
138    #[must_use]
139    pub const fn dialect(&self) -> Dialect {
140        self.dialect
141    }
142
143    /// The next chunk, or `None` at the end of the file.
144    ///
145    /// # Errors
146    ///
147    /// A read error, a malformed record, or a value that does not fit the type the sample chose
148    /// for its column.
149    pub fn next_chunk(&mut self) -> Result<Option<Chunk>> {
150        let mut rows: Vec<Vec<Option<String>>> = Vec::new();
151        while rows.len() < VECTOR_SIZE {
152            match self.next_record()? {
153                Some(fields) => rows.push(fields),
154                None => break,
155            }
156        }
157        if rows.is_empty() {
158            return Ok(None);
159        }
160        let mut columns = Vec::with_capacity(self.projection.len());
161        for &at in &self.projection {
162            let field = &self.fields[at];
163            let mut values = Vec::with_capacity(rows.len());
164            for (row, held) in rows.iter().enumerate() {
165                let text = held.get(at).and_then(Option::as_deref);
166                values.push(self.convert(
167                    text,
168                    field,
169                    self.line - rows.len() as u64 + row as u64,
170                )?);
171            }
172            columns.push(Vector::from_values(field.ty.clone(), &values)?);
173        }
174        Ok(Some(Chunk::with_rows(columns, rows.len())?))
175    }
176
177    /// One value, cast from its text to the column's type.
178    fn convert(&self, text: Option<&str>, field: &Field, line: u64) -> Result<Value> {
179        let Some(text) = text else { return Ok(Value::Null) };
180        if field.ty == LogicalType::Varchar {
181            return Ok(Value::Varchar(text.to_string()));
182        }
183        let value = Value::Varchar(text.to_string());
184        match cast_value(&value, &field.ty, false) {
185            Ok(converted) => Ok(converted),
186            Err(_) => Err(Error::conversion(self.conversion_error(text, field, line))),
187        }
188    }
189
190    /// DuckDB's message for a value that does not fit the type its column was sniffed as.
191    ///
192    /// Reproduced whole, including the block of settings at the bottom, because that block is the
193    /// answer to the question the message raises. Somebody reading it wants to know what was
194    /// guessed and how to override the guess, and a shorter message would send them to the
195    /// documentation to find out.
196    fn conversion_error(&self, text: &str, field: &Field, line: u64) -> String {
197        format!(
198            "CSV Error on Line: {line}\nOriginal Line: {text}\nError when converting column \
199             \"{}\". Could not convert string \"{text}\" to '{}'\n\nColumn {} is being converted \
200             as type {}\nThis type was auto-detected from the CSV file.\nPossible solutions:\n* \
201             Override the type for this column manually by setting the type explicitly, e.g., \
202             types={{'{}': 'VARCHAR'}}\n* Set the sample size to a larger value to enable the \
203             auto-detection to scan more values, e.g., sample_size=-1\n* Use a COPY statement to \
204             automatically derive types from an existing table.\n* Check whether the null string \
205             value is set correctly (e.g., nullstr = 'N/A')\n\n  file = {}\n  delimiter = {} \
206             (Auto-Detected)\n  quote = {} (Auto-Detected)\n  escape = {} (Auto-Detected)\n  \
207             header = {} (Auto-Detected)\n  sample_size = {}\n",
208            field.name,
209            field.ty,
210            field.name,
211            field.ty,
212            field.name,
213            self.path,
214            Dialect::shown(Some(self.dialect.delimiter)),
215            Dialect::shown(self.dialect.quote),
216            Dialect::shown(self.dialect.escape),
217            self.dialect.header,
218            infer::SAMPLE,
219        )
220    }
221
222    /// The next record, as one entry per field, with an empty field as a null.
223    fn next_record(&mut self) -> Result<Option<Vec<Option<String>>>> {
224        let Some(()) = self.advance()? else { return Ok(None) };
225        Ok(Some(
226            self.scratch
227                .iter()
228                .map(|text| if text.is_empty() { None } else { Some(text.clone()) })
229                .collect(),
230        ))
231    }
232
233    /// Reads one record into the scratch, filling the buffer when it has to.
234    fn advance(&mut self) -> Result<Option<()>> {
235        loop {
236            let mut scratch = std::mem::take(&mut self.scratch);
237            let outcome = crate::scan::record(
238                &self.buffer,
239                self.at,
240                self.dialect,
241                self.drained,
242                &mut scratch,
243            );
244            self.scratch = scratch;
245            match outcome? {
246                Some(next) => {
247                    self.at = next;
248                    self.line += 1;
249                    return Ok(Some(()));
250                }
251                None if self.drained => return Ok(None),
252                None => self.fill()?,
253            }
254        }
255    }
256
257    /// Reads one record and throws it away, which is what a header is.
258    fn skip_record(&mut self) -> Result<()> {
259        self.advance()?;
260        Ok(())
261    }
262
263    /// Drops what has been read and reads another block onto the end.
264    fn fill(&mut self) -> Result<()> {
265        self.buffer.drain(..self.at);
266        self.at = 0;
267        let held = self.buffer.len();
268        self.buffer.resize(held + BLOCK, 0);
269        let read = self.file.read_at(self.offset, &mut self.buffer[held..])?;
270        self.buffer.truncate(held + read);
271        self.offset += read as u64;
272        if read == 0 {
273            self.drained = true;
274        }
275        Ok(())
276    }
277
278    /// The records the sniffer gets to look at, which is the sample or the file, whichever is
279    /// shorter.
280    fn sample_rows(&self, sample: &[u8]) -> Result<Vec<Vec<Option<String>>>> {
281        let mut rows = Vec::new();
282        let mut fields = Vec::new();
283        let mut at = 0;
284        while rows.len() <= infer::SAMPLE {
285            // The end of the block is not the end of the file, so a record the block cut in half is
286            // simply not part of the sample.
287            let Some(next) = crate::scan::record(sample, at, self.dialect, false, &mut fields)?
288            else {
289                break;
290            };
291            at = next;
292            rows.push(
293                fields
294                    .iter()
295                    .map(|text| if text.is_empty() { None } else { Some(text.clone()) })
296                    .collect(),
297            );
298        }
299        Ok(rows)
300    }
301}
302
303/// Whether the first row is a header, and what the columns are called and typed.
304///
305/// The rule is DuckDB's and both halves of it were measured. A file whose columns are all `VARCHAR`
306/// once the first row is set aside has a header, because two rows of words is a header and a row.
307/// Otherwise the first row is a header exactly when it does not fit the types the rest of the file
308/// 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.
309fn describe(rows: &[Vec<Option<String>>]) -> (bool, Vec<Field>) {
310    let width = rows.iter().map(Vec::len).max().unwrap_or(0);
311    let body = types(&rows[1.min(rows.len())..], width);
312    let all_text = body.iter().all(|ty| *ty == LogicalType::Varchar);
313    let first_fits = rows.first().is_some_and(|first| {
314        first.iter().zip(&body).all(|(text, ty)| match text {
315            None => true,
316            Some(text) => infer::fits(text, ty),
317        })
318    });
319    let header = rows.len() > 1 && (all_text || !first_fits);
320    if !header {
321        let types = types(rows, width);
322        let fields = types
323            .into_iter()
324            .enumerate()
325            .map(|(at, ty)| Field::new(format!("column{at}"), ty))
326            .collect();
327        return (false, fields);
328    }
329    let names = unique(&rows[0], width);
330    let fields = body.into_iter().zip(names).map(|(ty, name)| Field::new(name, ty)).collect();
331    (true, fields)
332}
333
334/// The column names a header row gives, with the collisions resolved the way DuckDB resolves them.
335///
336/// A header is text somebody typed and nothing stops it naming two columns the same thing, so the
337/// second one gets `_1`, and the count goes up until the name is free. It has to count rather than
338/// stop at one, because the suffix can collide too: a file whose header is `a,a,a_1` comes back as
339/// `a`, `a_1`, `a_1_1` from the binary, and it is the second column that took the name the third one
340/// was written with.
341///
342/// The comparison ignores case and the written case is kept, which was measured: `a,a,A` comes back
343/// as `a`, `a_1`, `A_2`, so `A` collided with `a` and then `A_1` collided with `a_1`. An empty
344/// header cell is a column with no name, and it falls back to the generated one rather than to an
345/// empty string that no query could write.
346fn unique(header: &[Option<String>], width: usize) -> Vec<String> {
347    let mut taken: Vec<String> = Vec::with_capacity(width);
348    for at in 0..width {
349        let base = match header.get(at).and_then(Option::as_deref) {
350            Some(written) => written.to_string(),
351            None => format!("column{at}"),
352        };
353        let mut name = base.clone();
354        let mut next = 1;
355        while taken.iter().any(|held| held.eq_ignore_ascii_case(&name)) {
356            name = format!("{base}_{next}");
357            next += 1;
358        }
359        taken.push(name);
360    }
361    taken
362}
363
364/// The type of each of `width` columns, over these rows.
365fn types(rows: &[Vec<Option<String>>], width: usize) -> Vec<LogicalType> {
366    (0..width)
367        .map(|at| {
368            let values: Vec<Option<&str>> =
369                rows.iter().map(|row| row.get(at).and_then(Option::as_deref)).collect();
370            infer::column(&values)
371        })
372        .collect()
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use rudb_io::{Filesystem, OpenMode, SimFilesystem};
379    use std::path::Path;
380
381    fn read(text: &str) -> Reader {
382        let filesystem = SimFilesystem::new();
383        let path = Path::new("/t.csv");
384        let file = filesystem.open(path, OpenMode::Create).expect("creates");
385        file.write_at(0, text.as_bytes()).expect("writes");
386        drop(file);
387        let file = filesystem.open(path, OpenMode::Read).expect("opens");
388        Reader::open(file, "/t.csv").expect("sniffs")
389    }
390
391    fn names_and_types(reader: &Reader) -> Vec<(String, String)> {
392        reader.fields().iter().map(|f| (f.name.clone(), f.ty.to_string())).collect()
393    }
394
395    fn all(reader: &mut Reader) -> Vec<Vec<Value>> {
396        let mut rows = Vec::new();
397        while let Some(chunk) = reader.next_chunk().expect("reads") {
398            for row in 0..chunk.len() {
399                rows.push((0..chunk.width()).map(|at| chunk.value_at(row, at)).collect());
400            }
401        }
402        rows
403    }
404
405    #[test]
406    fn a_header_that_names_two_columns_the_same_thing_counts_the_second_one_up() {
407        let names: Vec<String> =
408            read("a,a,A,a_1\n1,2,3,4\nx,y,z,w\n").fields().into_iter().map(|f| f.name).collect();
409        // Measured against the binary, all four of them. The last one is the interesting one: the
410        // second column took `a_1`, which is the name the fourth column was written with, so the
411        // fourth has to keep counting from its own name rather than from `a`.
412        assert_eq!(names, ["a", "a_1", "A_2", "a_1_1"]);
413    }
414
415    #[test]
416    fn a_header_and_three_types_are_what_duckdb_sniffs_for_the_same_bytes() {
417        let reader = read("a,b,c\n1,x,2.5\n2,y,3.5\n");
418        assert_eq!(
419            names_and_types(&reader),
420            [
421                ("a".to_string(), "BIGINT".to_string()),
422                ("b".to_string(), "VARCHAR".to_string()),
423                ("c".to_string(), "DOUBLE".to_string()),
424            ]
425        );
426    }
427
428    #[test]
429    fn a_file_with_no_header_gets_the_names_duckdb_gives_it() {
430        let reader = read("1,x\n2,y\n");
431        assert_eq!(
432            names_and_types(&reader),
433            [
434                ("column0".to_string(), "BIGINT".to_string()),
435                ("column1".to_string(), "VARCHAR".to_string()),
436            ]
437        );
438    }
439
440    #[test]
441    fn two_rows_of_words_are_a_header_and_a_row() {
442        let reader = read("a,b\nc,d\n");
443        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["a", "b"]);
444    }
445
446    #[test]
447    fn one_column_of_words_under_a_row_of_numbers_is_still_a_header() {
448        // `a,2` over `3,4`. One column disagreeing is enough, and the second column is then named
449        // `2`, which is the text that was in it.
450        let reader = read("a,2\n3,4\n");
451        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["a", "2"]);
452    }
453
454    #[test]
455    fn the_rows_are_the_rows_of_the_file() {
456        let mut reader = read("a,b\n1,x\n2,y\n");
457        assert_eq!(
458            all(&mut reader),
459            [
460                vec![Value::BigInt(1), Value::Varchar("x".into())],
461                vec![Value::BigInt(2), Value::Varchar("y".into())],
462            ]
463        );
464    }
465
466    #[test]
467    fn an_empty_field_is_a_null_whether_it_was_quoted_or_not() {
468        // Measured. `allow_quoted_nulls` is on by default, so `""` is a null and not the empty
469        // string, which is the one place a quoted field and a bare one agree about being nothing.
470        let mut reader = read("a,b\n1,\n\"\",y\n");
471        assert_eq!(
472            all(&mut reader),
473            [vec![Value::BigInt(1), Value::Null], vec![Value::Null, Value::Varchar("y".into())],]
474        );
475    }
476
477    #[test]
478    fn a_projection_picks_columns_out_by_position_and_can_reorder_them() {
479        let mut reader = read("a,b,c\n1,x,2.5\n");
480        reader.project(&[2, 0]).expect("projects");
481        assert_eq!(reader.fields().iter().map(|f| f.name.clone()).collect::<Vec<_>>(), ["c", "a"]);
482        assert_eq!(all(&mut reader), [vec![Value::Double(2.5), Value::BigInt(1)]]);
483    }
484
485    #[test]
486    fn a_projection_of_nothing_still_counts_the_rows() {
487        let mut reader = read("a,b\n1,x\n2,y\n3,z\n");
488        reader.project(&[]).expect("projects");
489        let chunk = reader.next_chunk().expect("reads").expect("a chunk");
490        assert_eq!(chunk.len(), 3);
491        assert_eq!(chunk.width(), 0);
492    }
493
494    #[test]
495    fn a_pipe_separated_file_reads_as_one() {
496        let mut reader = read("a|b\n1|x\n");
497        assert_eq!(reader.dialect().delimiter, b'|');
498        assert_eq!(all(&mut reader), [vec![Value::BigInt(1), Value::Varchar("x".into())]]);
499    }
500
501    #[test]
502    fn a_quoted_field_with_a_delimiter_in_it_is_one_value() {
503        let mut reader = read("a,b\n1,\"x,y\"\n");
504        assert_eq!(all(&mut reader), [vec![Value::BigInt(1), Value::Varchar("x,y".into())]]);
505    }
506
507    #[test]
508    fn more_rows_than_fit_one_chunk_arrive_as_more_than_one_chunk() {
509        let mut text = String::from("a\n");
510        for row in 0..VECTOR_SIZE + 5 {
511            text.push_str(&format!("{row}\n"));
512        }
513        let mut reader = read(&text);
514        let first = reader.next_chunk().expect("reads").expect("a chunk");
515        assert_eq!(first.len(), VECTOR_SIZE);
516        let second = reader.next_chunk().expect("reads").expect("a second chunk");
517        assert_eq!(second.len(), 5);
518        assert!(reader.next_chunk().expect("reads").is_none());
519    }
520
521    #[test]
522    fn a_value_the_sniffer_never_saw_is_an_error_rather_than_a_wider_column() {
523        // The value has to be past the sample, because a value inside it would have widened the
524        // column to VARCHAR and there would be nothing to fail. Widening after the fact is not an
525        // option: the chunks before this one have already gone out with the narrow type on them.
526        let mut text = String::from("c\n");
527        for row in 0..infer::SAMPLE {
528            text.push_str(&format!("{row}\n"));
529        }
530        text.push_str("oops\n");
531        let mut reader = read(&text);
532        assert_eq!(reader.fields()[0].ty, LogicalType::BigInt);
533        let error = all_or_error(&mut reader).unwrap_err();
534        let line = infer::SAMPLE + 2;
535        assert!(error.message().starts_with(&format!("CSV Error on Line: {line}")), "{error}");
536        assert!(
537            error.message().contains("Could not convert string \"oops\" to 'BIGINT'"),
538            "{error}"
539        );
540        assert!(error.message().contains("sample_size = 20480"), "{error}");
541    }
542
543    #[test]
544    fn a_file_told_a_wider_type_than_it_sniffed_reads_its_whole_numbers_as_that_type() {
545        // What a glob does to every file it names. This file on its own is BIGINT and the set it
546        // belongs to is DOUBLE because some other file in it holds a decimal, so the column comes
547        // out DOUBLE and the rows come with it rather than the reader being overruled afterwards.
548        let mut reader = read("a\n1\n2\n");
549        assert_eq!(reader.fields()[0].ty, LogicalType::BigInt);
550        reader.retype(&[LogicalType::Double]).expect("one type for one column");
551        assert_eq!(reader.fields()[0].ty, LogicalType::Double);
552        assert_eq!(all(&mut reader), [[Value::Double(1.0)], [Value::Double(2.0)]]);
553    }
554
555    #[test]
556    fn a_type_list_that_is_not_as_long_as_the_projection_is_refused() {
557        let mut reader = read("a,b\n1,two\n");
558        let error = reader.retype(&[LogicalType::Double]).unwrap_err();
559        assert!(error.message().contains("1 types for a projection of 2 columns"), "{error}");
560    }
561
562    fn all_or_error(reader: &mut Reader) -> Result<Vec<Vec<Value>>> {
563        let mut rows = Vec::new();
564        while let Some(chunk) = reader.next_chunk()? {
565            for row in 0..chunk.len() {
566                rows.push((0..chunk.width()).map(|at| chunk.value_at(row, at)).collect());
567            }
568        }
569        Ok(rows)
570    }
571}