1use rusqlite::Connection;
12
13use crate::error::{DataError, Result};
14use crate::ingest::quote_ident;
15use crate::store::{ColumnDtype, ColumnSummary, NumericDescribe, TableSummary};
16
17pub fn summarize(conn: &Connection, table: &str) -> Result<TableSummary> {
20 let quoted = quote_ident(table);
21
22 let columns: Vec<(String, String)> = {
26 let mut stmt = conn.prepare(&format!("PRAGMA table_info({quoted})"))?;
27 let rows = stmt.query_map([], |row| {
28 Ok((row.get::<_, String>(1)?, row.get::<_, String>(2)?))
29 })?;
30 rows.collect::<rusqlite::Result<Vec<_>>>()?
31 };
32 if columns.is_empty() && !table_exists(conn, table)? {
33 return Err(DataError::NoSuchTable(table.to_string()));
34 }
35
36 let row_count: u64 = conn.query_row(&format!("SELECT COUNT(*) FROM {quoted}"), [], |row| {
37 row.get::<_, i64>(0).map(|n| n.max(0) as u64)
38 })?;
39
40 let mut summaries = Vec::with_capacity(columns.len());
41 for (name, decl) in &columns {
42 let dtype = ColumnDtype::from_decl_type(decl);
43 let qcol = quote_ident(name);
44
45 let non_null: i64 =
46 conn.query_row(&format!("SELECT COUNT({qcol}) FROM {quoted}"), [], |row| {
47 row.get(0)
48 })?;
49 let distinct_count: i64 = conn.query_row(
50 &format!("SELECT COUNT(DISTINCT {qcol}) FROM {quoted}"),
51 [],
52 |row| row.get(0),
53 )?;
54 let null_count = row_count.saturating_sub(non_null.max(0) as u64);
55
56 let numeric = match dtype {
57 ColumnDtype::Integer | ColumnDtype::Real => {
58 let values = collect_numeric(conn, "ed, &qcol)?;
59 Some(describe(&values))
60 }
61 ColumnDtype::Text => None,
62 };
63
64 summaries.push(ColumnSummary {
65 name: name.clone(),
66 dtype,
67 null_count,
68 distinct_count: distinct_count.max(0) as u64,
69 numeric,
70 });
71 }
72
73 Ok(TableSummary {
74 table: table.to_string(),
75 row_count,
76 columns: summaries,
77 })
78}
79
80fn table_exists(conn: &Connection, table: &str) -> Result<bool> {
83 let count: i64 = conn.query_row(
84 "SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table','view') AND name = ?1",
85 [table],
86 |row| row.get(0),
87 )?;
88 Ok(count > 0)
89}
90
91fn collect_numeric(conn: &Connection, quoted_table: &str, quoted_col: &str) -> Result<Vec<f64>> {
94 let mut stmt = conn.prepare(&format!(
95 "SELECT {quoted_col} FROM {quoted_table} WHERE {quoted_col} IS NOT NULL ORDER BY {quoted_col}"
96 ))?;
97 let rows = stmt.query_map([], |row| row.get::<_, f64>(0))?;
98 Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
99}
100
101pub fn describe(sorted: &[f64]) -> NumericDescribe {
107 let count = sorted.len() as u64;
108 if sorted.is_empty() {
109 return NumericDescribe {
110 count: 0,
111 mean: 0.0,
112 std: 0.0,
113 min: 0.0,
114 q25: 0.0,
115 q50: 0.0,
116 q75: 0.0,
117 max: 0.0,
118 };
119 }
120 let n = sorted.len();
121 let sum: f64 = sorted.iter().sum();
122 let mean = sum / n as f64;
123 let std = if n < 2 {
125 0.0
126 } else {
127 let ss: f64 = sorted.iter().map(|v| (v - mean).powi(2)).sum();
128 (ss / (n as f64 - 1.0)).sqrt()
129 };
130 NumericDescribe {
131 count,
132 mean,
133 std,
134 min: sorted[0],
136 q25: quantile_sorted(sorted, 0.25),
137 q50: quantile_sorted(sorted, 0.50),
138 q75: quantile_sorted(sorted, 0.75),
139 max: sorted[n - 1],
140 }
141}
142
143pub fn quantile_sorted(sorted: &[f64], q: f64) -> f64 {
150 if sorted.is_empty() {
151 return 0.0;
152 }
153 let n = sorted.len();
154 if n == 1 {
155 return sorted[0];
156 }
157 let pos = (n as f64 - 1.0) * q;
158 let lower = pos.floor() as usize;
159 let frac = pos - lower as f64;
160 if lower + 1 < n {
161 sorted[lower] + frac * (sorted[lower + 1] - sorted[lower])
162 } else {
163 sorted[n - 1]
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 const EPS: f64 = 1e-9;
173
174 #[test]
175 fn describe_matches_pandas_1_2_3_4() {
176 let d = describe(&[1.0, 2.0, 3.0, 4.0]);
180 assert_eq!(d.count, 4);
181 assert!((d.mean - 2.5).abs() < EPS);
182 assert!((d.std - 1.290_994_448_735_805_6).abs() < EPS);
183 assert!((d.min - 1.0).abs() < EPS);
184 assert!((d.q25 - 1.75).abs() < EPS);
185 assert!((d.q50 - 2.5).abs() < EPS);
186 assert!((d.q75 - 3.25).abs() < EPS);
187 assert!((d.max - 4.0).abs() < EPS);
188 }
189
190 #[test]
191 fn describe_single_value_has_zero_std() {
192 let d = describe(&[7.0]);
193 assert_eq!(d.count, 1);
194 assert!((d.mean - 7.0).abs() < EPS);
195 assert_eq!(d.std, 0.0);
196 assert!((d.q25 - 7.0).abs() < EPS);
197 assert!((d.q50 - 7.0).abs() < EPS);
198 assert!((d.q75 - 7.0).abs() < EPS);
199 }
200
201 #[test]
202 fn describe_empty_is_all_zero() {
203 let d = describe(&[]);
204 assert_eq!(d.count, 0);
205 assert_eq!(d.mean, 0.0);
206 assert_eq!(d.std, 0.0);
207 assert_eq!(d.max, 0.0);
208 }
209
210 #[test]
211 fn quantile_interpolates_linearly() {
212 let v = [10.0, 20.0, 30.0, 40.0, 50.0];
213 assert!((quantile_sorted(&v, 0.0) - 10.0).abs() < EPS);
214 assert!((quantile_sorted(&v, 1.0) - 50.0).abs() < EPS);
215 assert!((quantile_sorted(&v, 0.25) - 20.0).abs() < EPS);
217 assert!((quantile_sorted(&v, 0.1) - 14.0).abs() < EPS);
219 }
220}