1use anyhow::{Context, Result};
5use csv::ReaderBuilder;
6use serde_json::Value as JsonValue;
7use std::borrow::Cow;
8use std::collections::{HashMap, HashSet};
9use std::io::{BufRead, BufReader, Read};
10use tracing::{debug, info};
11
12use crate::data::advanced_csv_loader::StringInterner;
13use crate::data::datatable::{DataColumn, DataRow, DataTable, DataType, DataValue};
14
15#[derive(Debug, Clone)]
21pub struct CsvReadOptions {
22 pub delimiter: u8,
23 pub has_headers: bool,
24}
25
26impl Default for CsvReadOptions {
27 fn default() -> Self {
28 Self {
29 delimiter: b',',
30 has_headers: true,
31 }
32 }
33}
34
35pub fn detect_delimiter_from_path(path: &str) -> u8 {
41 let lower = path.to_ascii_lowercase();
42 if lower.ends_with(".tsv") {
43 b'\t'
44 } else if lower.ends_with(".psv") {
45 b'|'
46 } else {
47 b','
48 }
49}
50
51pub fn parse_delimiter_arg(s: &str) -> anyhow::Result<u8> {
61 match s {
62 "\\t" | "\t" => return Ok(b'\t'),
63 "\\n" => return Ok(b'\n'),
64 "\\r" => return Ok(b'\r'),
65 _ => {}
66 }
67 let bytes = s.as_bytes();
68 if bytes.len() == 1 && bytes[0].is_ascii() {
69 return Ok(bytes[0]);
70 }
71 Err(anyhow::anyhow!(
72 "delimiter must be a single ASCII character (or '\\t', '\\n', '\\r'); got {:?}",
73 s
74 ))
75}
76
77pub fn resolve_delimiter(path: &str, explicit: Option<u8>) -> u8 {
84 explicit.unwrap_or_else(|| detect_delimiter_from_path(path))
85}
86
87pub fn strip_field_padding(input: &[u8], delimiter: u8) -> Cow<'_, [u8]> {
126 let is_pad = |b: u8| (b == b' ' || b == b'\t') && b != delimiter;
127 let ends_field =
128 |b: Option<u8>| matches!(b, None | Some(b'\n') | Some(b'\r')) || b == Some(delimiter);
129
130 let mut out: Option<Vec<u8>> = None;
131 let mut i = 0;
132 let mut at_field_start = true;
133 let mut in_quotes = false;
134 let mut after_quoted_field = false;
137
138 while i < input.len() {
139 let b = input[i];
140
141 if in_quotes {
142 if b == b'"' {
144 if input.get(i + 1) == Some(&b'"') {
145 if let Some(o) = out.as_mut() {
146 o.extend_from_slice(&input[i..i + 2]);
147 }
148 i += 2;
149 continue;
150 }
151 in_quotes = false;
152 after_quoted_field = true;
153 }
154 if let Some(o) = out.as_mut() {
155 o.push(b);
156 }
157 i += 1;
158 continue;
159 }
160
161 if is_pad(b) {
162 let mut j = i;
163 while j < input.len() && is_pad(input[j]) {
164 j += 1;
165 }
166 let next = input.get(j).copied();
167 let before_open_quote = at_field_start && next == Some(b'"');
170 let after_close_quote = after_quoted_field && ends_field(next);
171 if before_open_quote || after_close_quote {
172 out.get_or_insert_with(|| input[..i].to_vec());
173 i = j;
174 continue;
175 }
176 if let Some(o) = out.as_mut() {
178 o.extend_from_slice(&input[i..j]);
179 }
180 i = j;
181 at_field_start = false;
182 continue;
183 }
184
185 if b == b'"' && at_field_start {
186 in_quotes = true;
187 }
188 at_field_start = b == delimiter || b == b'\n' || b == b'\r';
189 if at_field_start {
190 after_quoted_field = false;
191 }
192 if let Some(o) = out.as_mut() {
193 o.push(b);
194 }
195 i += 1;
196 }
197
198 out.map_or(Cow::Borrowed(input), Cow::Owned)
199}
200
201fn delimiter_label(d: u8) -> String {
203 match d {
204 b'\t' => "\\t".to_string(),
205 b'\n' => "\\n".to_string(),
206 b'\r' => "\\r".to_string(),
207 b => (b as char).to_string(),
208 }
209}
210
211#[derive(Debug)]
213struct ColumnAnalysis {
214 index: usize,
215 _name: String,
216 _cardinality: usize,
217 _sample_size: usize,
218 _unique_ratio: f64,
219 is_categorical: bool,
220 _avg_string_length: usize,
221}
222
223pub struct StreamCsvLoader {
225 sample_size: usize,
226 cardinality_threshold: f64,
227 interners: HashMap<usize, StringInterner>,
228}
229
230impl StreamCsvLoader {
231 pub fn new() -> Self {
232 Self {
233 sample_size: 1000,
234 cardinality_threshold: 0.3,
235 interners: HashMap::new(),
236 }
237 }
238
239 fn analyze_columns(
241 &self,
242 rows: &[Vec<String>],
243 headers: &csv::StringRecord,
244 ) -> Vec<ColumnAnalysis> {
245 let mut analyses = Vec::new();
246
247 for (col_idx, header) in headers.iter().enumerate() {
248 let mut unique_values = HashSet::new();
249 let mut total_length = 0;
250 let mut non_empty_count = 0;
251
252 for row in rows.iter().take(self.sample_size) {
254 if let Some(value) = row.get(col_idx) {
255 if !value.is_empty() {
256 unique_values.insert(value.clone());
257 total_length += value.len();
258 non_empty_count += 1;
259 }
260 }
261 }
262
263 let cardinality = unique_values.len();
264 let sample_size = rows.len().min(self.sample_size);
265 let unique_ratio = if sample_size > 0 {
266 cardinality as f64 / sample_size as f64
267 } else {
268 1.0
269 };
270
271 let avg_string_length = if non_empty_count > 0 {
272 total_length / non_empty_count
273 } else {
274 0
275 };
276
277 let is_categorical = unique_ratio < self.cardinality_threshold
279 || (avg_string_length < 20 && cardinality < sample_size / 2);
280
281 analyses.push(ColumnAnalysis {
282 index: col_idx,
283 _name: header.to_string(),
284 _cardinality: cardinality,
285 _sample_size: sample_size,
286 _unique_ratio: unique_ratio,
287 is_categorical,
288 _avg_string_length: avg_string_length,
289 });
290 }
291
292 analyses
293 }
294
295 pub fn load_csv_from_reader<R: Read>(
299 &mut self,
300 reader: R,
301 table_name: &str,
302 source_type: &str,
303 source_path: &str,
304 ) -> Result<DataTable> {
305 self.load_csv_from_reader_with_opts(
306 reader,
307 table_name,
308 source_type,
309 source_path,
310 &CsvReadOptions::default(),
311 )
312 }
313
314 pub fn load_csv_from_reader_with_opts<R: Read>(
317 &mut self,
318 mut reader: R,
319 table_name: &str,
320 source_type: &str,
321 source_path: &str,
322 opts: &CsvReadOptions,
323 ) -> Result<DataTable> {
324 info!(
325 "Stream CSV load: Loading {} with optimizations (delimiter={})",
326 source_path,
327 delimiter_label(opts.delimiter)
328 );
329
330 let mut raw_buffer = Vec::new();
332 reader.read_to_end(&mut raw_buffer)?;
333
334 let buffer = strip_field_padding(&raw_buffer, opts.delimiter);
337
338 let mut csv_reader = ReaderBuilder::new()
340 .has_headers(opts.has_headers)
341 .delimiter(opts.delimiter)
342 .from_reader(&buffer[..]);
343
344 let headers = csv_reader.headers()?.clone();
345 let mut table = DataTable::new(table_name);
346
347 table
349 .metadata
350 .insert("source_type".to_string(), source_type.to_string());
351 table
352 .metadata
353 .insert("source_path".to_string(), source_path.to_string());
354 table
355 .metadata
356 .insert("delimiter".to_string(), delimiter_label(opts.delimiter));
357
358 for header in &headers {
360 table.add_column(DataColumn::new(header));
361 }
362
363 let mut string_rows = Vec::new();
365 for result in csv_reader.records() {
366 let record = result?;
367 let row: Vec<String> = record.iter().map(|s| s.to_string()).collect();
368 string_rows.push(row);
369 }
370
371 let analyses = self.analyze_columns(&string_rows, &headers);
373 let categorical_columns: HashSet<usize> = analyses
374 .iter()
375 .filter(|a| a.is_categorical)
376 .map(|a| a.index)
377 .collect();
378
379 info!(
380 "Column analysis: {} of {} columns will use string interning",
381 categorical_columns.len(),
382 analyses.len()
383 );
384
385 for col_idx in &categorical_columns {
387 self.interners.insert(*col_idx, StringInterner::new());
388 }
389
390 let mut line_reader = BufReader::new(&buffer[..]);
392 let mut raw_lines = Vec::new();
393 let mut raw_line = String::new();
394
395 line_reader.read_line(&mut raw_line)?;
397 raw_line.clear();
398
399 for _ in 0..string_rows.len() {
401 line_reader.read_line(&mut raw_line)?;
402 raw_lines.push(raw_line.clone());
403 raw_line.clear();
404 }
405
406 let mut column_types = vec![DataType::Null; headers.len()];
408 let sample_size = string_rows.len().min(100);
409
410 for row in string_rows.iter().take(sample_size) {
411 for (col_idx, value) in row.iter().enumerate() {
412 if !value.is_empty() {
413 let inferred = DataType::infer_from_string(value);
414 column_types[col_idx] = column_types[col_idx].merge(&inferred);
415 }
416 }
417 }
418
419 for (col_idx, column) in table.columns.iter_mut().enumerate() {
421 column.data_type = column_types[col_idx].clone();
422 }
423
424 for (row_idx, string_row) in string_rows.iter().enumerate() {
426 let mut values = Vec::new();
427 let raw_line = &raw_lines[row_idx];
428
429 for (col_idx, value) in string_row.iter().enumerate() {
430 let data_value = if value.is_empty() {
431 if is_null_field(raw_line, col_idx, opts.delimiter as char) {
433 DataValue::Null
434 } else if categorical_columns.contains(&col_idx) {
435 if let Some(interner) = self.interners.get_mut(&col_idx) {
437 DataValue::InternedString(interner.intern(""))
438 } else {
439 DataValue::String(String::new())
440 }
441 } else {
442 DataValue::String(String::new())
443 }
444 } else if categorical_columns.contains(&col_idx)
445 && column_types[col_idx] == DataType::String
446 {
447 if let Some(interner) = self.interners.get_mut(&col_idx) {
449 DataValue::InternedString(interner.intern(value))
450 } else {
451 DataValue::from_string(value, &column_types[col_idx])
452 }
453 } else {
454 DataValue::from_string(value, &column_types[col_idx])
455 };
456 values.push(data_value);
457 }
458 table
459 .add_row(DataRow::new(values))
460 .map_err(|e| anyhow::anyhow!(e))?;
461 }
462
463 for (col_idx, interner) in &self.interners {
465 let stats = interner.stats();
466 if stats.memory_saved_bytes > 0 {
467 debug!(
468 "Column {} interning: {} unique strings, {} references, {} bytes saved",
469 headers.get(*col_idx).unwrap_or(&String::new()),
470 stats.unique_strings,
471 stats.total_references,
472 stats.memory_saved_bytes
473 );
474 }
475 }
476
477 table.infer_column_types();
479
480 Ok(table)
481 }
482}
483
484pub fn load_csv_from_reader<R: Read>(
487 reader: R,
488 table_name: &str,
489 source_type: &str,
490 source_path: &str,
491) -> Result<DataTable> {
492 let mut loader = StreamCsvLoader::new();
493 loader.load_csv_from_reader(reader, table_name, source_type, source_path)
494}
495
496pub fn load_csv_from_reader_with_opts<R: Read>(
499 reader: R,
500 table_name: &str,
501 source_type: &str,
502 source_path: &str,
503 opts: &CsvReadOptions,
504) -> Result<DataTable> {
505 let mut loader = StreamCsvLoader::new();
506 loader.load_csv_from_reader_with_opts(reader, table_name, source_type, source_path, opts)
507}
508
509pub fn parse_json_records(content: &str) -> Result<Vec<JsonValue>> {
517 let trimmed = content.trim_start();
518 if trimmed.starts_with('[') {
519 return serde_json::from_str(content).with_context(|| "Failed to parse JSON array");
520 }
521
522 let mut out = Vec::new();
523 for (idx, raw_line) in content.lines().enumerate() {
524 let line = raw_line.trim();
525 if line.is_empty() {
526 continue;
527 }
528 let value: JsonValue = serde_json::from_str(line)
529 .with_context(|| format!("Failed to parse JSONL at line {}", idx + 1))?;
530 out.push(value);
531 }
532 Ok(out)
533}
534
535pub fn navigate_json_path(value: &JsonValue, path: &str) -> Result<JsonValue> {
556 let parts: Vec<&str> = path.split('.').filter(|p| !p.is_empty()).collect();
557 walk_json_path(value, &parts)
558}
559
560fn walk_json_path(value: &JsonValue, parts: &[&str]) -> Result<JsonValue> {
561 let Some((head, tail)) = parts.split_first() else {
562 return Ok(value.clone());
563 };
564
565 if let Some(name) = head.strip_suffix("[]") {
568 let array_val = if name.is_empty() {
569 value
570 } else {
571 value
572 .get(name)
573 .ok_or_else(|| anyhow::anyhow!("Path '{}' not found in JSON", name))?
574 };
575 let arr = array_val.as_array().ok_or_else(|| {
576 anyhow::anyhow!(
577 "Expected array at '{}' for [] projection, got {}",
578 if name.is_empty() { "<root>" } else { name },
579 json_kind(array_val)
580 )
581 })?;
582 let mut projected = Vec::with_capacity(arr.len());
583 for el in arr {
584 projected.push(walk_json_path(el, tail)?);
585 }
586 return Ok(JsonValue::Array(projected));
587 }
588
589 let next = value
590 .get(head)
591 .ok_or_else(|| anyhow::anyhow!("Path '{}' not found in JSON", head))?;
592 walk_json_path(next, tail)
593}
594
595fn json_kind(value: &JsonValue) -> &'static str {
597 match value {
598 JsonValue::Null => "null",
599 JsonValue::Bool(_) => "bool",
600 JsonValue::Number(_) => "number",
601 JsonValue::String(_) => "string",
602 JsonValue::Array(_) => "array",
603 JsonValue::Object(_) => "object",
604 }
605}
606
607pub fn collect_column_names(records: &[JsonValue], sample_size: usize) -> Vec<String> {
611 let mut seen: HashSet<String> = HashSet::new();
612 let mut names: Vec<String> = Vec::new();
613 for record in records.iter().take(sample_size) {
614 if let Some(obj) = record.as_object() {
615 for key in obj.keys() {
616 if seen.insert(key.clone()) {
617 names.push(key.clone());
618 }
619 }
620 }
621 }
622 names
623}
624
625pub fn load_json_from_reader<R: Read>(
630 mut reader: R,
631 table_name: &str,
632 source_type: &str,
633 source_path: &str,
634) -> Result<DataTable> {
635 let mut json_str = String::new();
636 reader.read_to_string(&mut json_str)?;
637
638 let json_data: Vec<JsonValue> = parse_json_records(&json_str)?;
639
640 if json_data.is_empty() {
641 return Ok(DataTable::new(table_name));
642 }
643
644 let column_names = collect_column_names(&json_data, 100);
648 if column_names.is_empty() {
649 return Err(anyhow::anyhow!(
650 "JSON data must contain objects (got non-object records)"
651 ));
652 }
653
654 let mut table = DataTable::new(table_name);
655
656 table
658 .metadata
659 .insert("source_type".to_string(), source_type.to_string());
660 table
661 .metadata
662 .insert("source_path".to_string(), source_path.to_string());
663
664 for name in &column_names {
665 table.add_column(DataColumn::new(name));
666 }
667
668 let mut string_rows = Vec::new();
670 for json_obj in &json_data {
671 if let Some(obj) = json_obj.as_object() {
672 let mut row = Vec::new();
673 for col_name in &column_names {
674 let value = obj
675 .get(col_name)
676 .map(|v| json_value_to_string(v))
677 .unwrap_or_default();
678 row.push(value);
679 }
680 string_rows.push(row);
681 }
682 }
683
684 let mut column_types = vec![DataType::Null; column_names.len()];
686 let sample_size = string_rows.len().min(100);
687
688 for row in string_rows.iter().take(sample_size) {
689 for (col_idx, value) in row.iter().enumerate() {
690 if !value.is_empty() && value != "null" {
691 let inferred = DataType::infer_from_string(value);
692 column_types[col_idx] = column_types[col_idx].merge(&inferred);
693 }
694 }
695 }
696
697 for (col_idx, column) in table.columns.iter_mut().enumerate() {
699 column.data_type = column_types[col_idx].clone();
700 }
701
702 for string_row in &string_rows {
704 let mut values = Vec::new();
705 for (col_idx, value) in string_row.iter().enumerate() {
706 let data_value = if value.is_empty() || value == "null" {
707 DataValue::Null
708 } else {
709 DataValue::from_string(value, &column_types[col_idx])
710 };
711 values.push(data_value);
712 }
713 table
714 .add_row(DataRow::new(values))
715 .map_err(|e| anyhow::anyhow!(e))?;
716 }
717
718 table.infer_column_types();
720
721 Ok(table)
722}
723
724fn json_value_to_string(value: &JsonValue) -> String {
726 match value {
727 JsonValue::Null => String::new(),
728 JsonValue::Bool(b) => b.to_string(),
729 JsonValue::Number(n) => n.to_string(),
730 JsonValue::String(s) => s.clone(),
731 JsonValue::Array(arr) => format!("{:?}", arr),
732 JsonValue::Object(obj) => format!("{:?}", obj),
733 }
734}
735
736fn is_null_field(raw_line: &str, field_index: usize, delimiter: char) -> bool {
739 let mut delim_count = 0;
740 let mut in_quotes = false;
741 let mut field_start = 0;
742 let mut prev_char = ' ';
743
744 for (i, ch) in raw_line.char_indices() {
745 if ch == '"' && prev_char != '\\' {
746 in_quotes = !in_quotes;
747 } else if ch == delimiter && !in_quotes {
748 if delim_count == field_index {
749 return i == field_start
751 || (i == field_start + 1
752 && raw_line.chars().nth(field_start) == Some(delimiter));
753 }
754 delim_count += 1;
755 field_start = i + 1;
756 }
757 prev_char = ch;
758 }
759
760 if delim_count == field_index {
762 let remaining = raw_line[field_start..].trim_end();
763 return remaining.is_empty() || remaining.chars().next() == Some(delimiter);
764 }
765
766 false
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772 use std::io::Cursor;
773
774 fn stripped(input: &str, delimiter: u8) -> String {
777 String::from_utf8(strip_field_padding(input.as_bytes(), delimiter).into_owned()).unwrap()
778 }
779
780 #[test]
781 fn test_strip_padding_leaves_rfc4180_untouched() {
782 let input = "Index,Name,Day\n1,\"George Washington\",22\n";
783 assert!(matches!(
785 strip_field_padding(input.as_bytes(), b','),
786 Cow::Borrowed(_)
787 ));
788 assert_eq!(stripped(input, b','), input);
789 }
790
791 #[test]
792 fn test_strip_padding_unwraps_quotes_padded_after_delimiter() {
793 let input = "\"Index\", \"Name\", \"Day\"\n 1, \"George Washington\",\t22\n";
796 assert_eq!(
799 stripped(input, b','),
800 "\"Index\",\"Name\",\"Day\"\n 1,\"George Washington\",\t22\n"
801 );
802 }
803
804 #[test]
805 fn test_strip_padding_preserves_whitespace_inside_quotes() {
806 let input = "a, \" keep \" ,b\n";
808 assert_eq!(stripped(input, b','), "a,\" keep \",b\n");
809 }
810
811 #[test]
812 fn test_strip_padding_leaves_unquoted_fields_alone() {
813 let input = "4, David ,x\n";
816 assert_eq!(stripped(input, b','), input);
817 }
818
819 #[test]
820 fn test_strip_padding_preserves_escaped_quotes() {
821 let input = " \"he said \"\"hi\"\"\" ,1\n";
822 assert_eq!(stripped(input, b','), "\"he said \"\"hi\"\"\",1\n");
823 }
824
825 #[test]
826 fn test_strip_padding_never_eats_a_tab_delimiter() {
827 let input = "a\t \"b\" \tc\n";
829 assert_eq!(stripped(input, b'\t'), "a\t\"b\"\tc\n");
830 }
831
832 #[test]
833 fn test_strip_padding_recovers_delimiter_inside_padded_quotes() {
834 let input = "1, \"Adams, John\", 2\n";
836 let out = stripped(input, b',');
837 assert_eq!(out, "1,\"Adams, John\", 2\n");
838
839 let mut rdr = ReaderBuilder::new()
840 .has_headers(false)
841 .from_reader(out.as_bytes());
842 let rec = rdr.records().next().unwrap().unwrap();
843 assert_eq!(rec.len(), 3);
844 assert_eq!(&rec[1], "Adams, John");
845 }
846
847 #[test]
848 fn test_strip_padding_handles_crlf_and_end_of_line_pad() {
849 let input = " \"a\" , \"b\" \r\n \"c\" , \"d\" \r\n";
850 assert_eq!(stripped(input, b','), "\"a\",\"b\"\r\n\"c\",\"d\"\r\n");
851 }
852
853 #[test]
854 fn test_padded_csv_loads_with_clean_headers_and_numeric_types() {
855 let csv = "\"Index\", \"Name\", \"Year\"\n 1, \"George Washington\", 1732\n 2, \"John Adams\", 1735\n";
856 let table = StreamCsvLoader::new()
857 .load_csv_from_reader(Cursor::new(csv), "pres", "test", "<test>")
858 .unwrap();
859
860 let names: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
861 assert_eq!(names, vec!["Index", "Name", "Year"]);
862 assert_eq!(table.columns[2].data_type, DataType::Integer);
864 assert_eq!(
865 table.rows[0].values[1],
866 DataValue::String("George Washington".to_string())
867 );
868 }
869
870 #[test]
873 fn test_navigate_json_path_descends_object_key() {
874 let doc = serde_json::json!({
876 "count": 2,
877 "project": [{"id": "a"}, {"id": "b"}]
878 });
879 let extracted = navigate_json_path(&doc, "project").unwrap();
880 assert!(extracted.is_array());
881 assert_eq!(extracted.as_array().unwrap().len(), 2);
882 }
883
884 #[test]
885 fn test_navigate_json_path_nested_descent() {
886 let doc = serde_json::json!({
888 "projects": {"project": [{"id": "a"}, {"id": "b"}, {"id": "c"}]}
889 });
890 let extracted = navigate_json_path(&doc, "projects.project").unwrap();
891 assert_eq!(extracted.as_array().unwrap().len(), 3);
892 }
893
894 #[test]
895 fn test_navigate_json_path_array_projection() {
896 let doc = serde_json::json!({
898 "hits": {"hits": [
899 {"_source": {"id": 1}},
900 {"_source": {"id": 2}}
901 ]}
902 });
903 let extracted = navigate_json_path(&doc, "hits.hits[]._source").unwrap();
904 let arr = extracted.as_array().unwrap();
905 assert_eq!(arr.len(), 2);
906 assert_eq!(arr[1]["id"], serde_json::json!(2));
907 }
908
909 #[test]
910 fn test_navigate_json_path_bare_projection_over_root_array() {
911 let doc = serde_json::json!([{"v": {"x": 1}}, {"v": {"x": 2}}]);
912 let extracted = navigate_json_path(&doc, "[].v").unwrap();
913 let arr = extracted.as_array().unwrap();
914 assert_eq!(arr[0]["x"], serde_json::json!(1));
915 }
916
917 #[test]
918 fn test_navigate_json_path_empty_path_is_identity() {
919 let doc = serde_json::json!({"a": 1});
920 let extracted = navigate_json_path(&doc, "").unwrap();
921 assert_eq!(extracted, doc);
922 }
923
924 #[test]
925 fn test_navigate_json_path_missing_key_errors() {
926 let doc = serde_json::json!({"a": 1});
927 let err = navigate_json_path(&doc, "b").unwrap_err();
928 assert!(err.to_string().contains("not found"), "{}", err);
929 }
930
931 #[test]
932 fn test_navigate_json_path_projection_on_non_array_errors() {
933 let doc = serde_json::json!({"a": {"not": "an array"}});
934 let err = navigate_json_path(&doc, "a[]").unwrap_err();
935 let msg = err.to_string();
936 assert!(msg.contains("Expected array"), "{}", msg);
937 assert!(msg.contains("object"), "{}", msg);
938 }
939
940 #[test]
941 fn test_csv_from_reader() {
942 let csv_data = "id,name,value\n1,Alice,100\n2,Bob,200\n3,,300";
943 let reader = Cursor::new(csv_data);
944
945 let table =
946 load_csv_from_reader(reader, "test", "stream", "memory").expect("Failed to load CSV");
947
948 assert_eq!(table.name, "test");
949 assert_eq!(table.column_count(), 3);
950 assert_eq!(table.row_count(), 3);
951
952 let value = table.get_value(2, 1).unwrap();
954 assert!(matches!(value, DataValue::Null));
955 }
956
957 #[test]
958 fn test_json_from_reader() {
959 let json_data = r#"[
960 {"id": 1, "name": "Alice", "value": 100},
961 {"id": 2, "name": "Bob", "value": 200},
962 {"id": 3, "name": null, "value": 300}
963 ]"#;
964 let reader = Cursor::new(json_data);
965
966 let table =
967 load_json_from_reader(reader, "test", "stream", "memory").expect("Failed to load JSON");
968
969 assert_eq!(table.name, "test");
970 assert_eq!(table.column_count(), 3);
971 assert_eq!(table.row_count(), 3);
972
973 let value = table.get_value(2, 1).unwrap();
975 assert!(matches!(value, DataValue::Null));
976 }
977
978 #[test]
979 fn test_jsonl_from_reader() {
980 let jsonl_data = "{\"id\":1,\"name\":\"Alice\"}\n{\"id\":2,\"name\":\"Bob\"}\n";
981 let reader = Cursor::new(jsonl_data);
982
983 let table = load_json_from_reader(reader, "test", "stream", "memory")
984 .expect("Failed to load JSONL");
985
986 assert_eq!(table.column_count(), 2);
987 assert_eq!(table.row_count(), 2);
988 }
989
990 #[test]
991 fn test_jsonl_heterogeneous_schema_unioned() {
992 let jsonl_data = "{\"id\":1}\n{\"id\":2,\"extra\":\"hi\"}\n";
995 let reader = Cursor::new(jsonl_data);
996 let table = load_json_from_reader(reader, "test", "stream", "memory").expect("load");
997 assert_eq!(table.column_count(), 2);
998 assert_eq!(table.row_count(), 2);
999 }
1000
1001 #[test]
1002 fn test_jsonl_skips_blank_lines() {
1003 let jsonl_data = "{\"id\":1}\n\n\n{\"id\":2}\n";
1004 let reader = Cursor::new(jsonl_data);
1005 let table = load_json_from_reader(reader, "test", "stream", "memory").expect("load");
1006 assert_eq!(table.row_count(), 2);
1007 }
1008
1009 #[test]
1010 fn test_parse_json_records_array_form() {
1011 let recs = parse_json_records(r#"[{"a":1},{"a":2}]"#).unwrap();
1012 assert_eq!(recs.len(), 2);
1013 }
1014
1015 #[test]
1016 fn test_parse_json_records_jsonl_form() {
1017 let recs = parse_json_records("{\"a\":1}\n{\"a\":2}\n").unwrap();
1018 assert_eq!(recs.len(), 2);
1019 }
1020
1021 #[test]
1022 fn test_parse_json_records_jsonl_error_cites_line() {
1023 let err = parse_json_records("{\"a\":1}\nnot json\n").unwrap_err();
1024 assert!(err.to_string().contains("line 2"));
1025 }
1026
1027 #[test]
1030 fn test_csv_options_default_is_comma() {
1031 let opts = CsvReadOptions::default();
1032 assert_eq!(opts.delimiter, b',');
1033 assert!(opts.has_headers);
1034 }
1035
1036 #[test]
1037 fn test_detect_delimiter_from_path() {
1038 assert_eq!(detect_delimiter_from_path("data.tsv"), b'\t');
1039 assert_eq!(detect_delimiter_from_path("data.TSV"), b'\t');
1040 assert_eq!(detect_delimiter_from_path("/tmp/foo.psv"), b'|');
1041 assert_eq!(detect_delimiter_from_path("data.PSV"), b'|');
1042 assert_eq!(detect_delimiter_from_path("data.csv"), b',');
1043 assert_eq!(detect_delimiter_from_path("noext"), b',');
1044 assert_eq!(detect_delimiter_from_path("-"), b',');
1045 }
1046
1047 #[test]
1048 fn test_load_csv_with_pipe_delimiter() {
1049 let data = "id|name|score\n1|alice|10\n2|bob|20\n";
1050 let reader = Cursor::new(data);
1051 let opts = CsvReadOptions {
1052 delimiter: b'|',
1053 has_headers: true,
1054 };
1055 let table = load_csv_from_reader_with_opts(reader, "psv", "test", "memory", &opts)
1056 .expect("load failed");
1057 assert_eq!(table.column_count(), 3);
1058 assert_eq!(table.row_count(), 2);
1059 assert_eq!(table.get_value(0, 0).unwrap(), &DataValue::Integer(1));
1060 assert_eq!(
1061 table.get_value(1, 1).unwrap(),
1062 &DataValue::String("bob".to_string())
1063 );
1064 }
1065
1066 #[test]
1067 fn test_load_csv_with_tab_delimiter() {
1068 let data = "id\tname\tscore\n1\talice\t10\n2\tbob\t20\n";
1069 let reader = Cursor::new(data);
1070 let opts = CsvReadOptions {
1071 delimiter: b'\t',
1072 has_headers: true,
1073 };
1074 let table = load_csv_from_reader_with_opts(reader, "tsv", "test", "memory", &opts)
1075 .expect("load failed");
1076 assert_eq!(table.column_count(), 3);
1077 assert_eq!(table.row_count(), 2);
1078 assert_eq!(table.get_value(0, 0).unwrap(), &DataValue::Integer(1));
1079 }
1080
1081 #[test]
1082 fn test_metadata_records_delimiter() {
1083 let table = load_csv_from_reader(Cursor::new("a,b\n1,2\n"), "t", "test", "memory").unwrap();
1085 assert_eq!(
1086 table.metadata.get("delimiter").map(String::as_str),
1087 Some(",")
1088 );
1089
1090 let opts = CsvReadOptions {
1092 delimiter: b'\t',
1093 has_headers: true,
1094 };
1095 let table = load_csv_from_reader_with_opts(
1096 Cursor::new("a\tb\n1\t2\n"),
1097 "t",
1098 "test",
1099 "memory",
1100 &opts,
1101 )
1102 .unwrap();
1103 assert_eq!(
1104 table.metadata.get("delimiter").map(String::as_str),
1105 Some("\\t")
1106 );
1107 }
1108
1109 #[test]
1110 fn test_parse_delimiter_arg_accepts_single_char() {
1111 assert_eq!(parse_delimiter_arg(",").unwrap(), b',');
1112 assert_eq!(parse_delimiter_arg("|").unwrap(), b'|');
1113 assert_eq!(parse_delimiter_arg(";").unwrap(), b';');
1114 }
1115
1116 #[test]
1117 fn test_parse_delimiter_arg_accepts_backslash_escapes() {
1118 assert_eq!(parse_delimiter_arg("\\t").unwrap(), b'\t');
1119 assert_eq!(parse_delimiter_arg("\t").unwrap(), b'\t');
1120 assert_eq!(parse_delimiter_arg("\\n").unwrap(), b'\n');
1121 assert_eq!(parse_delimiter_arg("\\r").unwrap(), b'\r');
1122 }
1123
1124 #[test]
1125 fn test_parse_delimiter_arg_rejects_multi_char() {
1126 let err = parse_delimiter_arg("||").unwrap_err();
1127 assert!(err.to_string().contains("single ASCII character"));
1128 }
1129
1130 #[test]
1131 fn test_parse_delimiter_arg_rejects_non_ascii() {
1132 let err = parse_delimiter_arg("ö").unwrap_err();
1133 assert!(err.to_string().contains("single ASCII character"));
1134 }
1135
1136 #[test]
1137 fn test_resolve_delimiter_explicit_wins() {
1138 assert_eq!(resolve_delimiter("data.psv", Some(b',')), b',');
1139 assert_eq!(resolve_delimiter("data.tsv", Some(b';')), b';');
1140 assert_eq!(resolve_delimiter("data.csv", Some(b'|')), b'|');
1141 }
1142
1143 #[test]
1144 fn test_resolve_delimiter_falls_back_to_extension() {
1145 assert_eq!(resolve_delimiter("data.psv", None), b'|');
1146 assert_eq!(resolve_delimiter("data.tsv", None), b'\t');
1147 assert_eq!(resolve_delimiter("data.csv", None), b',');
1148 assert_eq!(resolve_delimiter("data.dat", None), b',');
1149 }
1150
1151 #[test]
1152 fn test_null_detection_works_with_pipe_delimiter() {
1153 let data = "id|name|score\n1||10\n";
1155 let opts = CsvReadOptions {
1156 delimiter: b'|',
1157 has_headers: true,
1158 };
1159 let table =
1160 load_csv_from_reader_with_opts(Cursor::new(data), "psv", "test", "memory", &opts)
1161 .expect("load failed");
1162 assert!(matches!(table.get_value(0, 1).unwrap(), DataValue::Null));
1163 }
1164}