pub struct RawCsv {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub nulls: Vec<Vec<bool>>,
pub row_ids: Vec<usize>,
}
pub(super) fn push_row<'a>(
fields: impl Iterator<Item = &'a str>,
n_cols: usize,
rows: &mut Vec<Vec<String>>,
nulls: &mut Vec<Vec<bool>>,
) {
let mut row = Vec::with_capacity(n_cols);
let mut nrow = Vec::with_capacity(n_cols);
for cell in fields.take(n_cols) {
nrow.push(cell.is_empty());
row.push(cell.to_string());
}
while row.len() < n_cols {
row.push(String::new());
nrow.push(true);
}
rows.push(row);
nulls.push(nrow);
}
impl RawCsv {
pub fn col_index(&self, name: &str) -> Option<usize> {
self.headers.iter().position(|h| h == name)
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn row_id(&self, r: usize) -> usize {
debug_assert_eq!(
self.row_ids.len(),
self.rows.len(),
"row_ids must carry one source row number per row"
);
self.row_ids.get(r).copied().unwrap_or(r + 1)
}
}
pub(super) fn looks_like_a_missed_list(cell: &str) -> bool {
if serde_json::from_str::<serde_json::Value>(cell)
.is_ok_and(|v| matches!(v, serde_json::Value::Array(_)))
{
return false;
}
cell.contains(['|', ';', ','])
}
#[derive(Default)]
pub struct ListMisparseTally {
hits: Vec<(String, usize, usize, String)>,
}
impl ListMisparseTally {
pub(super) fn record(&mut self, column: &str, row_id: usize, cell: &str) {
if let Some(hit) = self.hits.iter_mut().find(|(c, _, _, _)| c == column) {
hit.1 += 1;
return;
}
self.hits
.push((column.to_string(), 1, row_id, cell.to_string()));
}
pub fn into_warnings(self, where_: &str) -> Vec<String> {
self.hits
.into_iter()
.map(|(column, count, row_id, cell)| {
let cell = if cell.chars().count() > 80 {
let head: String = cell.chars().take(80).collect();
format!("{head}…")
} else {
cell
};
format!(
"{where_}: column '{column}' is declared list but {count} cell(s) are not a \
JSON array and contain a separator ('|', ';' or ','); each was kept whole \
as a one-element list. First at row {row_id}: '{cell}'. Write list cells \
as JSON arrays, e.g. [\"a\",\"b\"]."
)
})
.collect()
}
}
#[cfg(test)]
mod row_id_tests {
use super::RawCsv;
fn two_rows_one_id() -> RawCsv {
RawCsv {
headers: vec!["a".to_string()],
rows: vec![vec!["x".to_string()], vec!["y".to_string()]],
nulls: vec![vec![false], vec![false]],
row_ids: vec![7],
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "row_ids")]
fn a_table_with_short_row_ids_panics_in_debug() {
let raw = two_rows_one_id();
let _ = raw.row_id(1);
}
#[test]
#[cfg(not(debug_assertions))]
fn a_table_with_short_row_ids_falls_back_in_release() {
assert_eq!(two_rows_one_id().row_id(1), 2);
}
}
#[cfg(test)]
mod chunk_tests {
use super::super::input::csv::CsvFile;
use super::super::input::Source;
use super::RawCsv;
use std::io::Write;
fn csv_source(f: &tempfile::NamedTempFile) -> CsvFile {
CsvFile::new(f.path().to_path_buf(), "sample.csv".to_string())
}
fn write_csv(content: &str) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f
}
#[test]
fn small_file_yields_single_chunk() {
let f = write_csv("a,b\n1,2\n3,4\n");
let chunks: Vec<RawCsv> = csv_source(&f)
.chunks(100)
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].rows.len(), 2);
assert_eq!(chunks[0].headers, vec!["a", "b"]);
}
#[test]
fn large_file_yields_multiple_chunks() {
let mut content = String::from("a,b\n");
for i in 0..2500 {
content.push_str(&format!("{i},{i}\n"));
}
let f = write_csv(&content);
let chunks: Vec<RawCsv> = csv_source(&f)
.chunks(1000)
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].rows.len(), 1000);
assert_eq!(chunks[1].rows.len(), 1000);
assert_eq!(chunks[2].rows.len(), 500);
for c in &chunks {
assert_eq!(c.headers, vec!["a", "b"]);
}
}
#[test]
fn empty_chunk_at_end_is_dropped() {
let f = write_csv("a,b\n1,2\n3,4\n5,6\n");
let chunks: Vec<RawCsv> = csv_source(&f)
.chunks(3)
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].rows.len(), 3);
}
#[test]
fn header_only_yields_zero_chunks() {
let f = write_csv("only,header\n");
let chunks: Vec<RawCsv> = csv_source(&f)
.chunks(10)
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(chunks.len(), 0);
}
#[test]
fn chunks_carry_nulls_correctly() {
let f = write_csv("a,b,c\n1,,3\n,,\n");
let chunks: Vec<RawCsv> = csv_source(&f)
.chunks(100)
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(chunks.len(), 1);
let c = &chunks[0];
assert_eq!(c.nulls[0], vec![false, true, false]);
assert_eq!(c.nulls[1], vec![true, true, true]);
}
}