ddb_core/file_io/
csv_reader.rs1use 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 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 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 pub fn column_names(&self) -> &[String] {
45 &self.column_names
46 }
47
48 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), Ok(_) => {
55 self.line_number += 1;
56 line = line.trim_end_matches(&['\r', '\n'][..]).to_string();
57
58 if self.line_number == 1 && self.has_header {
60 self.column_names = self.parse_line(&line);
61 return self.next_row(); }
63
64 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 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 fields.push(current_field);
100
101 fields
102 }
103
104 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 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 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 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 let row2 = reader.next_row().unwrap().unwrap();
153 assert_eq!(row2.get("id"), Some(&Value::Integer(2)));
154
155 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}