ddb_core/file_io/
csv_reader.rs

1// CSV/Delimited file reader
2use crate::engine::row::Row;
3use crate::error::{DdbError, Result};
4use crate::functions::Value;
5use std::fs::File;
6use std::io::{BufRead, BufReader};
7use std::path::Path;
8
9pub struct CsvReader {
10    reader: BufReader<File>,
11    delimiter: char,
12    column_names: Vec<String>,
13    line_number: usize,
14    has_header: bool,
15}
16
17impl CsvReader {
18    /// Create a new CSV reader with specified delimiter
19    pub fn new<P: AsRef<Path>>(path: P, delimiter: char, has_header: bool) -> Result<Self> {
20        let file = File::open(path)?;
21        let reader = BufReader::new(file);
22
23        Ok(Self {
24            reader,
25            delimiter,
26            column_names: Vec::new(),
27            line_number: 0,
28            has_header,
29        })
30    }
31
32    /// Create a CSV reader with explicit column names (no header in file)
33    pub fn with_columns<P: AsRef<Path>>(
34        path: P,
35        delimiter: char,
36        columns: Vec<String>,
37    ) -> Result<Self> {
38        let mut reader = Self::new(path, delimiter, false)?;
39        reader.column_names = columns;
40        Ok(reader)
41    }
42
43    /// Get the column names
44    pub fn column_names(&self) -> &[String] {
45        &self.column_names
46    }
47
48    /// Read the next row
49    pub fn next_row(&mut self) -> Result<Option<Row>> {
50        let mut line = String::new();
51
52        match self.reader.read_line(&mut line) {
53            Ok(0) => Ok(None), // EOF
54            Ok(_) => {
55                self.line_number += 1;
56                line = line.trim_end_matches(&['\r', '\n'][..]).to_string();
57
58                // If this is the first line and we have a header, parse column names
59                if self.line_number == 1 && self.has_header {
60                    self.column_names = self.parse_line(&line);
61                    return self.next_row(); // Read next line for actual data
62                }
63
64                // If we don't have column names, generate them
65                if self.column_names.is_empty() {
66                    let values = self.parse_line(&line);
67                    self.column_names = (0..values.len())
68                        .map(|i| format!("col{}", i))
69                        .collect();
70                }
71
72                let values = self.parse_line(&line);
73                let row = self.create_row(values)?;
74                Ok(Some(row))
75            }
76            Err(e) => Err(DdbError::IoError(e)),
77        }
78    }
79
80    /// Parse a line into fields
81    fn parse_line(&self, line: &str) -> Vec<String> {
82        let mut fields = Vec::new();
83        let mut current_field = String::new();
84        let mut in_quotes = false;
85        let mut chars = line.chars().peekable();
86
87        while let Some(ch) = chars.next() {
88            if ch == '"' {
89                in_quotes = !in_quotes;
90            } else if ch == self.delimiter && !in_quotes {
91                fields.push(current_field.clone());
92                current_field.clear();
93            } else {
94                current_field.push(ch);
95            }
96        }
97
98        // Add the last field
99        fields.push(current_field);
100
101        fields
102    }
103
104    /// Create a Row from parsed field values
105    fn create_row(&self, fields: Vec<String>) -> Result<Row> {
106        if fields.len() != self.column_names.len() {
107            return Err(DdbError::InvalidColumnCount {
108                expected: self.column_names.len(),
109                actual: fields.len(),
110            });
111        }
112
113        let mut row = Row::new();
114        for (name, field) in self.column_names.iter().zip(fields.iter()) {
115            let value = Value::from_str(field);
116            row.set(name, value);
117        }
118
119        Ok(row)
120    }
121
122    /// Get current line number
123    pub fn line_number(&self) -> usize {
124        self.line_number
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::io::Write;
132    use tempfile::NamedTempFile;
133
134    #[test]
135    fn test_csv_reader_with_header() {
136        // Create a temporary CSV file
137        let mut file = NamedTempFile::new().unwrap();
138        writeln!(file, "id,name,age").unwrap();
139        writeln!(file, "1,Alice,30").unwrap();
140        writeln!(file, "2,Bob,25").unwrap();
141        file.flush().unwrap();
142
143        let mut reader = CsvReader::new(file.path(), ',', true).unwrap();
144
145        // Read first row
146        let row1 = reader.next_row().unwrap().unwrap();
147        assert_eq!(row1.get("id"), Some(&Value::Integer(1)));
148        assert_eq!(row1.get("name"), Some(&Value::String("Alice".to_string())));
149        assert_eq!(row1.get("age"), Some(&Value::Integer(30)));
150
151        // Read second row
152        let row2 = reader.next_row().unwrap().unwrap();
153        assert_eq!(row2.get("id"), Some(&Value::Integer(2)));
154
155        // EOF
156        assert!(reader.next_row().unwrap().is_none());
157    }
158
159    #[test]
160    fn test_csv_reader_without_header() {
161        let mut file = NamedTempFile::new().unwrap();
162        writeln!(file, "1,Alice,30").unwrap();
163        writeln!(file, "2,Bob,25").unwrap();
164        file.flush().unwrap();
165
166        let columns = vec!["id".to_string(), "name".to_string(), "age".to_string()];
167        let mut reader = CsvReader::with_columns(file.path(), ',', columns).unwrap();
168
169        let row = reader.next_row().unwrap().unwrap();
170        assert_eq!(row.get("id"), Some(&Value::Integer(1)));
171        assert_eq!(row.get("name"), Some(&Value::String("Alice".to_string())));
172    }
173
174    #[test]
175    fn test_csv_reader_quoted_fields() {
176        let mut file = NamedTempFile::new().unwrap();
177        writeln!(file, "name,description").unwrap();
178        writeln!(file, r#"Test,"A field, with comma""#).unwrap();
179        file.flush().unwrap();
180
181        let mut reader = CsvReader::new(file.path(), ',', true).unwrap();
182        let row = reader.next_row().unwrap().unwrap();
183
184        assert_eq!(row.get("name"), Some(&Value::String("Test".to_string())));
185        assert_eq!(row.get("description"), Some(&Value::String("A field, with comma".to_string())));
186    }
187}