ddb_core/file_io/
csv_reader.rs1use crate::engine::row::Row;
3use crate::error::{DdbError, Result};
4use crate::functions::Value;
5use memmap2::Mmap;
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::path::Path;
9
10const MMAP_THRESHOLD: u64 = 10 * 1024 * 1024;
12
13enum ReaderBackend {
14 Buffered(BufReader<File>),
15 MemoryMapped {
16 mmap: Mmap,
17 position: usize,
18 },
19}
20
21pub struct CsvReader {
22 backend: ReaderBackend,
23 delimiter: char,
24 column_names: Vec<String>,
25 line_number: usize,
26 has_header: bool,
27}
28
29impl CsvReader {
30 pub fn new<P: AsRef<Path>>(path: P, delimiter: char, has_header: bool) -> Result<Self> {
33 let path_ref = path.as_ref();
34 let file = File::open(path_ref)?;
35 let metadata = file.metadata()?;
36 let file_size = metadata.len();
37
38 let backend = if file_size >= MMAP_THRESHOLD {
39 let mmap = unsafe { Mmap::map(&file)? };
41 ReaderBackend::MemoryMapped {
42 mmap,
43 position: 0,
44 }
45 } else {
46 ReaderBackend::Buffered(BufReader::new(file))
48 };
49
50 Ok(Self {
51 backend,
52 delimiter,
53 column_names: Vec::new(),
54 line_number: 0,
55 has_header,
56 })
57 }
58
59 pub fn with_columns<P: AsRef<Path>>(
61 path: P,
62 delimiter: char,
63 columns: Vec<String>,
64 ) -> Result<Self> {
65 let mut reader = Self::new(path, delimiter, false)?;
66 reader.column_names = columns;
67 Ok(reader)
68 }
69
70 pub fn column_names(&self) -> &[String] {
72 &self.column_names
73 }
74
75 pub fn next_row(&mut self) -> Result<Option<Row>> {
77 let line_opt = match &mut self.backend {
78 ReaderBackend::Buffered(reader) => {
79 let mut line = String::new();
80 match reader.read_line(&mut line) {
81 Ok(0) => None,
82 Ok(_) => Some(line.trim_end_matches(&['\r', '\n'][..]).to_string()),
83 Err(e) => return Err(DdbError::IoError(e)),
84 }
85 }
86 ReaderBackend::MemoryMapped { mmap, position } => {
87 Self::read_line_from_mmap(mmap, position)?
88 }
89 };
90
91 match line_opt {
92 None => Ok(None), Some(line) => {
94 self.line_number += 1;
95
96 if self.line_number == 1 && self.has_header {
98 self.column_names = self.parse_line(&line);
99 return self.next_row(); }
101
102 if self.column_names.is_empty() {
104 let values = self.parse_line(&line);
105 self.column_names = (0..values.len())
106 .map(|i| format!("col{}", i))
107 .collect();
108 }
109
110 let values = self.parse_line(&line);
111 let row = self.create_row(values)?;
112 Ok(Some(row))
113 }
114 }
115 }
116
117 fn read_line_from_mmap(mmap: &Mmap, position: &mut usize) -> Result<Option<String>> {
119 if *position >= mmap.len() {
120 return Ok(None);
121 }
122
123 let start = *position;
125 let mut end = start;
126
127 while end < mmap.len() && mmap[end] != b'\n' {
128 end += 1;
129 }
130
131 let line_bytes = &mmap[start..end];
133 let mut line = String::from_utf8_lossy(line_bytes).to_string();
134
135 if line.ends_with('\r') {
137 line.pop();
138 }
139
140 *position = if end < mmap.len() { end + 1 } else { end };
142
143 Ok(Some(line))
144 }
145
146 fn parse_line(&self, line: &str) -> Vec<String> {
148 let mut fields = Vec::new();
149 let mut current_field = String::new();
150 let mut in_quotes = false;
151 let mut chars = line.chars().peekable();
152
153 while let Some(ch) = chars.next() {
154 if ch == '"' {
155 in_quotes = !in_quotes;
156 } else if ch == self.delimiter && !in_quotes {
157 fields.push(current_field.clone());
158 current_field.clear();
159 } else {
160 current_field.push(ch);
161 }
162 }
163
164 fields.push(current_field);
166
167 fields
168 }
169
170 fn create_row(&self, fields: Vec<String>) -> Result<Row> {
172 if fields.len() != self.column_names.len() {
173 return Err(DdbError::InvalidColumnCount {
174 expected: self.column_names.len(),
175 actual: fields.len(),
176 });
177 }
178
179 let mut row = Row::new();
180 for (name, field) in self.column_names.iter().zip(fields.iter()) {
181 let value = Value::from_str(field);
182 row.set(name, value);
183 }
184
185 Ok(row)
186 }
187
188 pub fn line_number(&self) -> usize {
190 self.line_number
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::io::Write;
198 use tempfile::NamedTempFile;
199
200 #[test]
201 fn test_csv_reader_with_header() {
202 let mut file = NamedTempFile::new().unwrap();
204 writeln!(file, "id,name,age").unwrap();
205 writeln!(file, "1,Alice,30").unwrap();
206 writeln!(file, "2,Bob,25").unwrap();
207 file.flush().unwrap();
208
209 let mut reader = CsvReader::new(file.path(), ',', true).unwrap();
210
211 let row1 = reader.next_row().unwrap().unwrap();
213 assert_eq!(row1.get("id"), Some(&Value::Integer(1)));
214 assert_eq!(row1.get("name"), Some(&Value::String("Alice".to_string())));
215 assert_eq!(row1.get("age"), Some(&Value::Integer(30)));
216
217 let row2 = reader.next_row().unwrap().unwrap();
219 assert_eq!(row2.get("id"), Some(&Value::Integer(2)));
220
221 assert!(reader.next_row().unwrap().is_none());
223 }
224
225 #[test]
226 fn test_csv_reader_without_header() {
227 let mut file = NamedTempFile::new().unwrap();
228 writeln!(file, "1,Alice,30").unwrap();
229 writeln!(file, "2,Bob,25").unwrap();
230 file.flush().unwrap();
231
232 let columns = vec!["id".to_string(), "name".to_string(), "age".to_string()];
233 let mut reader = CsvReader::with_columns(file.path(), ',', columns).unwrap();
234
235 let row = reader.next_row().unwrap().unwrap();
236 assert_eq!(row.get("id"), Some(&Value::Integer(1)));
237 assert_eq!(row.get("name"), Some(&Value::String("Alice".to_string())));
238 }
239
240 #[test]
241 fn test_csv_reader_quoted_fields() {
242 let mut file = NamedTempFile::new().unwrap();
243 writeln!(file, "name,description").unwrap();
244 writeln!(file, r#"Test,"A field, with comma""#).unwrap();
245 file.flush().unwrap();
246
247 let mut reader = CsvReader::new(file.path(), ',', true).unwrap();
248 let row = reader.next_row().unwrap().unwrap();
249
250 assert_eq!(row.get("name"), Some(&Value::String("Test".to_string())));
251 assert_eq!(row.get("description"), Some(&Value::String("A field, with comma".to_string())));
252 }
253}