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