1use super::CsvImportLimits;
18use hawdb_core::{HawDBError, Result};
19use std::io::Read;
20
21pub const CSV_READ_BUFFER_BYTES: usize = 64 * 1024;
23
24const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
25
26pub type CsvField = Option<String>;
29
30pub struct CsvRecordReader<R> {
35 input: R,
36 buffer: Box<[u8]>,
37 position: usize,
38 filled: usize,
39 eof: bool,
40 started: bool,
41 terminator: u8,
42 limits: CsvImportLimits,
43 bytes_read: u64,
44 line: u64,
46 records: u64,
47}
48
49impl<R: Read> CsvRecordReader<R> {
50 pub fn new(input: R, terminator: u8, limits: CsvImportLimits) -> Result<Self> {
51 if !terminator.is_ascii() || matches!(terminator, b'"' | b'\r' | b'\n') {
52 return Err(HawDBError::Semantic(
53 "FIELDTERMINATOR must be one ASCII character other than a double quote or line break"
54 .to_string(),
55 ));
56 }
57 Ok(Self {
58 input,
59 buffer: vec![0; CSV_READ_BUFFER_BYTES].into_boxed_slice(),
60 position: 0,
61 filled: 0,
62 eof: false,
63 started: false,
64 terminator,
65 limits,
66 bytes_read: 0,
67 line: 1,
68 records: 0,
69 })
70 }
71
72 pub fn records(&self) -> u64 {
74 self.records
75 }
76
77 pub fn next_record(&mut self) -> Result<Option<Vec<CsvField>>> {
79 if !self.started {
80 self.started = true;
81 self.skip_bom()?;
82 }
83 loop {
84 let Some(first) = self.peek()? else {
85 return Ok(None);
86 };
87 if matches!(first, b'\n' | b'\r') {
88 self.consume_line_break(first)?;
89 continue;
90 }
91 let start_line = self.line;
92 let record = self.read_record(start_line)?;
93 self.records += 1;
94 return Ok(Some(record));
95 }
96 }
97
98 fn read_record(&mut self, start_line: u64) -> Result<Vec<CsvField>> {
99 let mut fields = Vec::new();
100 let mut record_bytes = 0usize;
101 loop {
102 if fields.len() == self.limits.max_fields_per_record {
103 return Err(self.error(
104 start_line,
105 format!(
106 "has more than max_fields_per_record {} fields",
107 self.limits.max_fields_per_record
108 ),
109 ));
110 }
111 let mut field = Vec::new();
112 let quoted = self.peek()? == Some(b'"');
113 let end = if quoted {
114 self.bump();
115 self.read_quoted(&mut field, &mut record_bytes, start_line)?
116 } else {
117 self.read_unquoted(&mut field, &mut record_bytes, start_line)?
118 };
119 let text = String::from_utf8(field).map_err(|_| {
120 self.error(
121 start_line,
122 format!("field {} is not valid UTF-8", fields.len() + 1),
123 )
124 })?;
125 fields.push((quoted || !text.is_empty()).then_some(text));
126 if end == FieldEnd::Record {
127 return Ok(fields);
128 }
129 }
130 }
131
132 fn read_unquoted(
133 &mut self,
134 field: &mut Vec<u8>,
135 record_bytes: &mut usize,
136 start_line: u64,
137 ) -> Result<FieldEnd> {
138 loop {
139 let Some(byte) = self.peek()? else {
140 return Ok(FieldEnd::Record);
141 };
142 self.bump();
143 match byte {
144 byte if byte == self.terminator => return Ok(FieldEnd::Field),
145 b'\n' | b'\r' => {
146 self.finish_line_break(byte)?;
147 return Ok(FieldEnd::Record);
148 }
149 byte => self.push(field, byte, record_bytes, start_line)?,
151 }
152 }
153 }
154
155 fn read_quoted(
156 &mut self,
157 field: &mut Vec<u8>,
158 record_bytes: &mut usize,
159 start_line: u64,
160 ) -> Result<FieldEnd> {
161 loop {
162 let Some(byte) = self.peek()? else {
163 return Err(self.error(start_line, "has an unterminated quoted field".to_string()));
164 };
165 self.bump();
166 match byte {
167 b'"' if self.peek()? == Some(b'"') => {
168 self.bump();
169 self.push(field, b'"', record_bytes, start_line)?;
170 }
171 b'"' => break,
172 b'\n' => {
173 self.line += 1;
174 self.push(field, byte, record_bytes, start_line)?;
175 }
176 b'\r' => {
177 if self.peek()? != Some(b'\n') {
178 self.line += 1;
179 }
180 self.push(field, byte, record_bytes, start_line)?;
181 }
182 byte => self.push(field, byte, record_bytes, start_line)?,
183 }
184 }
185 match self.peek()? {
186 None => Ok(FieldEnd::Record),
187 Some(byte) if byte == self.terminator => {
188 self.bump();
189 Ok(FieldEnd::Field)
190 }
191 Some(byte @ (b'\n' | b'\r')) => {
192 self.consume_line_break(byte)?;
193 Ok(FieldEnd::Record)
194 }
195 Some(_) => Err(self.error(
196 start_line,
197 "has a character after a closing quote; quote the whole field and double embedded quotes"
198 .to_string(),
199 )),
200 }
201 }
202
203 fn push(
204 &self,
205 field: &mut Vec<u8>,
206 byte: u8,
207 record_bytes: &mut usize,
208 start_line: u64,
209 ) -> Result<()> {
210 *record_bytes += 1;
211 if *record_bytes > self.limits.max_record_bytes {
212 return Err(self.error(
213 start_line,
214 format!("exceeds max_record_bytes {}", self.limits.max_record_bytes),
215 ));
216 }
217 field.push(byte);
218 Ok(())
219 }
220
221 fn consume_line_break(&mut self, byte: u8) -> Result<()> {
222 self.bump();
223 self.finish_line_break(byte)
224 }
225
226 fn finish_line_break(&mut self, byte: u8) -> Result<()> {
228 if byte == b'\r' && self.peek()? == Some(b'\n') {
229 self.bump();
230 }
231 self.line += 1;
232 Ok(())
233 }
234
235 fn skip_bom(&mut self) -> Result<()> {
236 while self.filled - self.position < UTF8_BOM.len() && !self.eof {
237 self.fill()?;
238 }
239 if self.buffer[self.position..self.filled].starts_with(&UTF8_BOM) {
240 self.position += UTF8_BOM.len();
241 }
242 Ok(())
243 }
244
245 fn peek(&mut self) -> Result<Option<u8>> {
246 if self.position == self.filled {
247 if self.eof {
248 return Ok(None);
249 }
250 self.position = 0;
251 self.filled = 0;
252 self.fill()?;
253 if self.filled == 0 {
254 return Ok(None);
255 }
256 }
257 Ok(Some(self.buffer[self.position]))
258 }
259
260 fn bump(&mut self) {
261 self.position += 1;
262 }
263
264 fn fill(&mut self) -> Result<()> {
266 if self.position > 0 {
267 self.buffer.copy_within(self.position..self.filled, 0);
268 self.filled -= self.position;
269 self.position = 0;
270 }
271 let read = loop {
272 match self.input.read(&mut self.buffer[self.filled..]) {
273 Ok(read) => break read,
274 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
275 Err(error) => {
276 return Err(HawDBError::Execution(format!(
277 "LOAD CSV failed to read the source: {error}"
278 )))
279 }
280 }
281 };
282 if read == 0 {
283 self.eof = true;
284 return Ok(());
285 }
286 self.bytes_read = self.bytes_read.saturating_add(read as u64);
287 if self.bytes_read > self.limits.max_file_bytes {
288 return Err(HawDBError::Execution(format!(
289 "LOAD CSV source exceeds max_file_bytes {}",
290 self.limits.max_file_bytes
291 )));
292 }
293 self.filled += read;
294 Ok(())
295 }
296
297 fn error(&self, start_line: u64, detail: String) -> HawDBError {
298 HawDBError::Execution(format!(
299 "LOAD CSV record {} starting at line {start_line} {detail}",
300 self.records + 1
301 ))
302 }
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306enum FieldEnd {
307 Field,
308 Record,
309}