use std::collections::{HashMap, HashSet};
use anyhow::Result;
use crate::input;
use crate::value::display_value;
use crate::{
ColumnInfo, ColumnStats, ExtractionOptions, InferredType, QueryValue, TableSummary,
TypeInferenceOptions, WorkbookInput,
};
pub fn load_table_summaries(
workbook_inputs: &[WorkbookInput<'_>],
sample: usize,
include_stats: bool,
) -> Result<Vec<TableSummary>> {
let mut summaries = Vec::with_capacity(workbook_inputs.len());
let inference_options = TypeInferenceOptions::default();
for (index, workbook_input) in workbook_inputs.iter().enumerate() {
let sheet = input::load_input(
workbook_input.path,
workbook_input.sheet_name,
&inference_options,
&ExtractionOptions::default(),
true,
workbook_input.explicit_format,
)?;
let table_name = workbook_input
.table_name
.map(str::to_owned)
.unwrap_or_else(|| {
if index == 0 {
"table".to_owned()
} else {
format!("table{}", index + 1)
}
});
let columns = infer_column_infos(&table_name, &sheet);
let mut warnings = collect_warnings(&table_name, &sheet, &columns);
let sample_rows = sheet.rows.iter().take(sample).cloned().collect();
let column_stats = if include_stats {
Some(compute_column_stats(&sheet))
} else {
None
};
if sheet.rows.is_empty() {
warnings.push(format!(
"table '{}' loaded from '{}' contains no data rows",
table_name,
workbook_input.path.display()
));
}
summaries.push(TableSummary {
table_name,
source_path: workbook_input.path.display().to_string(),
source_selector: workbook_input.sheet_name.map(str::to_owned),
row_count: sheet.rows.len(),
columns,
sample_rows,
warnings,
column_stats,
});
}
Ok(summaries)
}
fn infer_column_infos(table_name: &str, sheet: &input::SheetData) -> Vec<ColumnInfo> {
sheet
.columns
.iter()
.enumerate()
.map(|(col_idx, col_name)| {
let mut has_integer = false;
let mut has_real = false;
let mut has_text = false;
let mut has_null = false;
for row in &sheet.rows {
match row.get(col_idx).unwrap_or(&QueryValue::Null) {
QueryValue::Null => has_null = true,
QueryValue::Integer(_) => has_integer = true,
QueryValue::Real(_) => has_real = true,
QueryValue::Text(_) => has_text = true,
}
}
let inferred_type = match (has_integer, has_real, has_text) {
(true, false, false) => InferredType::Integer,
(false, false, true) => InferredType::Text,
(false, false, false) => InferredType::Text,
(_, true, false) => InferredType::Real,
_ => InferredType::Mixed,
};
ColumnInfo {
table_name: table_name.to_owned(),
column_name: col_name.clone(),
inferred_type,
nullable: has_null,
}
})
.collect()
}
fn collect_warnings(table_name: &str, sheet: &input::SheetData, columns: &[ColumnInfo]) -> Vec<String> {
let mut warnings = Vec::new();
for col_info in columns {
if col_info.inferred_type == InferredType::Mixed {
warnings.push(format!(
"column '{}' in table '{}' has mixed types (INTEGER/REAL and TEXT values)",
col_info.column_name, table_name
));
}
}
let mut seen_names: HashMap<String, usize> = HashMap::new();
for col in &sheet.columns {
*seen_names.entry(col.clone()).or_insert(0) += 1;
}
for (name, count) in &seen_names {
if *count > 1 {
warnings.push(format!(
"column name '{}' appears {} times in table '{}'; duplicates were renamed",
name, count, table_name
));
}
}
warnings
}
fn compute_column_stats(sheet: &input::SheetData) -> Vec<ColumnStats> {
sheet
.columns
.iter()
.enumerate()
.map(|(col_idx, _)| {
let mut distinct: HashSet<String> = HashSet::new();
let mut min_num: Option<f64> = None;
let mut max_num: Option<f64> = None;
let mut min_as_int: Option<i64> = None;
let mut max_as_int: Option<i64> = None;
for row in &sheet.rows {
let value = row.get(col_idx).unwrap_or(&QueryValue::Null);
if matches!(value, QueryValue::Null) {
continue;
}
distinct.insert(display_value(value));
let (as_f64, as_int) = match value {
QueryValue::Integer(n) => (*n as f64, Some(*n)),
QueryValue::Real(n) => (*n, None),
_ => continue,
};
if min_num.map_or(true, |m| as_f64 < m) {
min_num = Some(as_f64);
min_as_int = as_int;
}
if max_num.map_or(true, |m| as_f64 > m) {
max_num = Some(as_f64);
max_as_int = as_int;
}
}
let min_value =
min_num.map(|v| min_as_int.map_or_else(|| v.to_string(), |i| i.to_string()));
let max_value =
max_num.map(|v| max_as_int.map_or_else(|| v.to_string(), |i| i.to_string()));
ColumnStats {
distinct_count: distinct.len(),
min_value,
max_value,
}
})
.collect()
}