1use std::collections::{HashMap, HashSet};
2
3use anyhow::Result;
4
5use crate::input;
6use crate::value::display_value;
7use crate::{
8 ColumnInfo, ColumnStats, ExtractionOptions, InferredType, QueryValue, TableSummary,
9 TypeInferenceOptions, WorkbookInput,
10};
11
12pub fn load_table_summaries(
16 workbook_inputs: &[WorkbookInput<'_>],
17 sample: usize,
18 include_stats: bool,
19) -> Result<Vec<TableSummary>> {
20 let mut summaries = Vec::with_capacity(workbook_inputs.len());
21 let inference_options = TypeInferenceOptions::default();
22
23 for (index, workbook_input) in workbook_inputs.iter().enumerate() {
24 let sheet = input::load_input(
25 workbook_input.path,
26 workbook_input.sheet_name,
27 &inference_options,
28 &ExtractionOptions::default(),
29 true,
30 workbook_input.explicit_format,
31 )?;
32
33 let table_name = workbook_input
34 .table_name
35 .map(str::to_owned)
36 .unwrap_or_else(|| {
37 if index == 0 {
38 "table".to_owned()
39 } else {
40 format!("table{}", index + 1)
41 }
42 });
43
44 let columns = infer_column_infos(&table_name, &sheet);
45 let mut warnings = collect_warnings(&table_name, &sheet, &columns);
46
47 let sample_rows = sheet.rows.iter().take(sample).cloned().collect();
48
49 let column_stats = if include_stats {
50 Some(compute_column_stats(&sheet))
51 } else {
52 None
53 };
54
55 if sheet.rows.is_empty() {
57 warnings.push(format!(
58 "table '{}' loaded from '{}' contains no data rows",
59 table_name,
60 workbook_input.path.display()
61 ));
62 }
63
64 summaries.push(TableSummary {
65 table_name,
66 source_path: workbook_input.path.display().to_string(),
67 source_selector: workbook_input.sheet_name.map(str::to_owned),
68 row_count: sheet.rows.len(),
69 columns,
70 sample_rows,
71 warnings,
72 column_stats,
73 });
74 }
75
76 Ok(summaries)
77}
78
79fn infer_column_infos(table_name: &str, sheet: &input::SheetData) -> Vec<ColumnInfo> {
81 sheet
82 .columns
83 .iter()
84 .enumerate()
85 .map(|(col_idx, col_name)| {
86 let mut has_integer = false;
87 let mut has_real = false;
88 let mut has_text = false;
89 let mut has_null = false;
90
91 for row in &sheet.rows {
92 match row.get(col_idx).unwrap_or(&QueryValue::Null) {
93 QueryValue::Null => has_null = true,
94 QueryValue::Integer(_) => has_integer = true,
95 QueryValue::Real(_) => has_real = true,
96 QueryValue::Text(_) => has_text = true,
97 }
98 }
99
100 let inferred_type = match (has_integer, has_real, has_text) {
101 (true, false, false) => InferredType::Integer,
102 (false, false, true) => InferredType::Text,
103 (false, false, false) => InferredType::Text,
104 (_, true, false) => InferredType::Real,
107 _ => InferredType::Mixed,
108 };
109
110 ColumnInfo {
111 table_name: table_name.to_owned(),
112 column_name: col_name.clone(),
113 inferred_type,
114 nullable: has_null,
115 }
116 })
117 .collect()
118}
119
120fn collect_warnings(table_name: &str, sheet: &input::SheetData, columns: &[ColumnInfo]) -> Vec<String> {
122 let mut warnings = Vec::new();
123
124 for col_info in columns {
125 if col_info.inferred_type == InferredType::Mixed {
126 warnings.push(format!(
127 "column '{}' in table '{}' has mixed types (INTEGER/REAL and TEXT values)",
128 col_info.column_name, table_name
129 ));
130 }
131 }
132
133 let mut seen_names: HashMap<String, usize> = HashMap::new();
135 for col in &sheet.columns {
136 *seen_names.entry(col.clone()).or_insert(0) += 1;
137 }
138 for (name, count) in &seen_names {
139 if *count > 1 {
140 warnings.push(format!(
141 "column name '{}' appears {} times in table '{}'; duplicates were renamed",
142 name, count, table_name
143 ));
144 }
145 }
146
147 warnings
148}
149
150fn compute_column_stats(sheet: &input::SheetData) -> Vec<ColumnStats> {
152 sheet
153 .columns
154 .iter()
155 .enumerate()
156 .map(|(col_idx, _)| {
157 let mut distinct: HashSet<String> = HashSet::new();
158 let mut min_num: Option<f64> = None;
161 let mut max_num: Option<f64> = None;
162 let mut min_as_int: Option<i64> = None;
165 let mut max_as_int: Option<i64> = None;
166
167 for row in &sheet.rows {
168 let value = row.get(col_idx).unwrap_or(&QueryValue::Null);
169 if matches!(value, QueryValue::Null) {
170 continue;
171 }
172 distinct.insert(display_value(value));
173
174 let (as_f64, as_int) = match value {
175 QueryValue::Integer(n) => (*n as f64, Some(*n)),
176 QueryValue::Real(n) => (*n, None),
177 _ => continue,
178 };
179
180 if min_num.map_or(true, |m| as_f64 < m) {
181 min_num = Some(as_f64);
182 min_as_int = as_int;
183 }
184 if max_num.map_or(true, |m| as_f64 > m) {
185 max_num = Some(as_f64);
186 max_as_int = as_int;
187 }
188 }
189
190 let min_value =
191 min_num.map(|v| min_as_int.map_or_else(|| v.to_string(), |i| i.to_string()));
192 let max_value =
193 max_num.map(|v| max_as_int.map_or_else(|| v.to_string(), |i| i.to_string()));
194
195 ColumnStats {
196 distinct_count: distinct.len(),
197 min_value,
198 max_value,
199 }
200 })
201 .collect()
202}