pub mod utils;
pub use utils::*;
use anyhow::Result;
use csv::ReaderBuilder;
use dashmap::DashMap;
use rayon::prelude::*;
use std::io::Cursor;
use crate::config::RuntimeConfig;
use crate::parsers::ParseResult;
use crate::parsers::column_stats;
use crate::parsers::traits::AdaptiveParallel;
use crate::results::{BooleanStats, CsvMetadata, DateStats, NumericStats};
pub fn extract_csv_metadata(
content: &[u8],
stats: &ParseResult,
config: &RuntimeConfig,
) -> Result<CsvMetadata> {
let encoding = if std::str::from_utf8(content).is_ok() {
Some("UTF-8".to_string())
} else {
Some("Non-UTF-8".to_string())
};
let Ok(content_str) = std::str::from_utf8(content) else {
return Ok(CsvMetadata {
encoding,
..Default::default()
});
};
let delim_byte = utils::delimiter_byte_for_reader(content_str, &stats.file_path);
let field_sep = char::from_u32(u32::from(delim_byte)).unwrap_or(',');
let mut reader = ReaderBuilder::new()
.delimiter(delim_byte)
.has_headers(true) .flexible(true) .from_reader(Cursor::new(content_str));
let headers_result = reader.headers();
let (column_names, column_count_from_headers, has_header) = match headers_result {
Ok(headers) => {
let names: Vec<String> = headers.iter().map(|s: &str| s.to_string()).collect();
let count = names.len();
(Some(names), Some(count), Some(true))
}
Err(_) => (None, None, Some(false)),
};
let delimiter_display = utils::format_delimiter_for_metadata(delim_byte);
let quote_character = utils::detect_quote_character(content_str, field_sep);
let escape_character = utils::detect_escape_character(
content_str,
Some(delimiter_display.as_str()),
quote_character.as_deref(),
);
let max_sample_rows = config.max_csv_sample_rows;
let mut row_count = 0;
let mut column_count: usize = column_count_from_headers.unwrap_or(0);
let mut sample_data: Vec<Vec<String>> = Vec::new();
for result in reader.records() {
if let Ok(record) = result {
row_count += 1;
if column_count == 0 {
column_count = record.len();
}
if sample_data.len() < max_sample_rows {
let row: Vec<String> = record.iter().map(|s: &str| s.to_string()).collect();
sample_data.push(row);
}
} else {
}
}
let (column_types, null_percentages, unique_counts, numeric_stats, date_stats, boolean_stats) =
if !sample_data.is_empty() && column_count > 0 {
let types = infer_column_types(&sample_data, column_count, config);
let (null_pcts, unique_cts) =
compute_column_statistics(&sample_data, column_count, config);
let (num_stats, dt_stats, bool_stats) =
compute_type_specific_statistics(&sample_data, &types, column_count, config);
(
Some(types),
Some(null_pcts),
Some(unique_cts),
Some(num_stats),
Some(dt_stats),
Some(bool_stats),
)
} else {
(None, None, None, None, None, None)
};
Ok(CsvMetadata {
row_count,
column_count,
column_names,
encoding,
column_types,
delimiter: Some(delimiter_display),
quote_character,
escape_character,
has_header,
null_percentages,
unique_counts,
numeric_stats,
date_stats,
boolean_stats,
})
}
fn infer_column_types(
sample_data: &[Vec<String>],
column_count: usize,
config: &RuntimeConfig,
) -> Vec<String> {
let type_scores: Vec<DashMap<String, usize>> =
(0..column_count).map(|_| DashMap::new()).collect();
sample_data.par_iter_adaptive(config).for_each(|row| {
for (col_idx, value) in row.iter().enumerate() {
if col_idx >= column_count {
break;
}
let inferred_type = utils::infer_value_type(value);
*type_scores[col_idx].entry(inferred_type).or_insert(0) += 1;
}
});
type_scores
.into_iter()
.map(|scores| {
scores
.into_iter()
.max_by_key(|(_, count)| *count)
.map_or_else(|| "string".to_string(), |(type_name, _)| type_name)
})
.collect()
}
fn compute_column_statistics(
sample_data: &[Vec<String>],
column_count: usize,
config: &RuntimeConfig,
) -> (Vec<f64>, Vec<usize>) {
let total_rows = sample_data.len();
if total_rows == 0 {
return (vec![0.0; column_count], vec![0; column_count]);
}
let mut null_percentages = Vec::with_capacity(column_count);
let mut unique_counts = Vec::with_capacity(column_count);
for col_idx in 0..column_count {
let values = column_stats::extract_column_values(sample_data, col_idx);
let (null_pct, unique_ct) = column_stats::compute_null_and_unique_stats(&values, config);
null_percentages.push(null_pct);
unique_counts.push(unique_ct);
}
(null_percentages, unique_counts)
}
type TypeSpecificStats = (
Vec<Option<NumericStats>>,
Vec<Option<DateStats>>,
Vec<Option<BooleanStats>>,
);
#[allow(clippy::type_complexity)]
fn compute_type_specific_statistics(
sample_data: &[Vec<String>],
column_types: &[String],
column_count: usize,
config: &RuntimeConfig,
) -> TypeSpecificStats {
let mut numeric_stats: Vec<Option<NumericStats>> = vec![None; column_count];
let mut date_stats: Vec<Option<DateStats>> = vec![None; column_count];
let mut boolean_stats: Vec<Option<BooleanStats>> = vec![None; column_count];
for col_idx in 0..column_count {
if col_idx >= column_types.len() {
break;
}
let col_type = &column_types[col_idx];
match col_type.as_str() {
"number" => {
numeric_stats[col_idx] = compute_numeric_stats(sample_data, col_idx, config);
}
"timestamp" | "date" => {
date_stats[col_idx] = compute_date_stats(sample_data, col_idx, config);
}
"boolean" => {
boolean_stats[col_idx] = compute_boolean_stats(sample_data, col_idx, config);
}
_ => {
}
}
}
(numeric_stats, date_stats, boolean_stats)
}
fn compute_numeric_stats(
sample_data: &[Vec<String>],
col_idx: usize,
config: &RuntimeConfig,
) -> Option<NumericStats> {
let values = column_stats::extract_column_values(sample_data, col_idx);
column_stats::compute_numeric_stats_from_strings(&values, config)
}
fn compute_date_stats(
sample_data: &[Vec<String>],
col_idx: usize,
_config: &RuntimeConfig,
) -> Option<DateStats> {
let values = column_stats::extract_column_values(sample_data, col_idx);
column_stats::compute_date_stats_from_strings(&values)
}
fn compute_boolean_stats(
sample_data: &[Vec<String>],
col_idx: usize,
config: &RuntimeConfig,
) -> Option<BooleanStats> {
let values = column_stats::extract_column_values(sample_data, col_idx);
column_stats::compute_boolean_stats_from_strings(&values, config)
}
crate::no_template_mining!(
extract_csv_templates,
"CSV files don't have templates, return empty result."
);