1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::classification::{DataType, TypeHomogeneity};
6use crate::pattern::Pattern;
7
8#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
10pub struct ColumnProfile {
11 pub name: String,
12 pub data_type: DataType,
13 pub null_count: usize,
14 pub total_count: usize,
15 pub unique_count: Option<usize>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub unique_count_is_approximate: Option<bool>,
27 #[serde(skip)]
37 #[schemars(skip)]
38 pub unique_count_lower_bound: Option<usize>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub invalid_count: Option<usize>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub type_homogeneity: Option<TypeHomogeneity>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub locale_number_count: Option<usize>,
86 pub stats: ColumnStats,
87 pub patterns: Option<Vec<Pattern>>,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
98pub struct Quartiles {
99 pub q1: f64,
100 pub q2: f64,
101 pub q3: f64,
102 #[serde(deserialize_with = "crate::serde_helpers::required_nullable")]
106 pub iqr: Option<f64>,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
111pub struct FrequencyItem {
112 pub value: String,
113 pub count: usize,
114 #[serde(serialize_with = "crate::serde_helpers::round_2")]
115 pub percentage: f64,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
126#[serde(rename_all = "snake_case")]
127pub enum ShapeMomentEstimator {
128 AdjustedSample,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
135pub struct NumericStats {
136 #[serde(serialize_with = "crate::serde_helpers::round_4")]
140 pub min: f64,
141 #[serde(serialize_with = "crate::serde_helpers::round_4")]
142 pub max: f64,
143 #[serde(serialize_with = "crate::serde_helpers::round_4")]
144 pub mean: f64,
145 #[serde(
148 serialize_with = "crate::serde_helpers::round_4_opt",
149 deserialize_with = "crate::serde_helpers::required_nullable"
150 )]
151 pub std_dev: Option<f64>,
152 #[serde(
155 serialize_with = "crate::serde_helpers::round_4_opt",
156 deserialize_with = "crate::serde_helpers::required_nullable"
157 )]
158 pub variance: Option<f64>,
159 #[serde(
160 skip_serializing_if = "Option::is_none",
161 serialize_with = "crate::serde_helpers::round_4_opt"
162 )]
163 pub median: Option<f64>,
164 #[serde(
165 skip_serializing_if = "Option::is_none",
166 serialize_with = "crate::serde_helpers::quartiles::serialize"
167 )]
168 pub quartiles: Option<Quartiles>,
169 #[serde(
170 skip_serializing_if = "Option::is_none",
171 serialize_with = "crate::serde_helpers::round_4_opt"
172 )]
173 pub mode: Option<f64>,
174 #[serde(
175 skip_serializing_if = "Option::is_none",
176 serialize_with = "crate::serde_helpers::round_2_opt"
177 )]
178 pub coefficient_of_variation: Option<f64>,
179 #[serde(
180 skip_serializing_if = "Option::is_none",
181 serialize_with = "crate::serde_helpers::round_4_opt"
182 )]
183 pub skewness: Option<f64>,
184 #[serde(
185 skip_serializing_if = "Option::is_none",
186 serialize_with = "crate::serde_helpers::round_4_opt"
187 )]
188 pub kurtosis: Option<f64>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub is_approximate: Option<bool>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub outlier_count: Option<usize>,
199}
200
201impl NumericStats {
202 pub fn empty() -> Self {
203 Self {
204 min: 0.0,
205 max: 0.0,
206 mean: 0.0,
207 std_dev: Some(0.0),
208 variance: Some(0.0),
209 median: None,
210 quartiles: None,
211 mode: None,
212 coefficient_of_variation: None,
213 skewness: None,
214 kurtosis: None,
215 is_approximate: None,
216 outlier_count: None,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
229pub struct TextStats {
230 pub min_length: usize,
232 pub max_length: usize,
234 #[serde(serialize_with = "crate::serde_helpers::round_4")]
237 pub avg_length: f64,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub most_frequent: Option<Vec<FrequencyItem>>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub least_frequent: Option<Vec<FrequencyItem>>,
242}
243
244impl TextStats {
245 pub fn empty() -> Self {
246 Self {
247 min_length: 0,
248 max_length: 0,
249 avg_length: 0.0,
250 most_frequent: None,
251 least_frequent: None,
252 }
253 }
254
255 pub fn from_lengths(min_length: usize, max_length: usize, avg_length: f64) -> Self {
256 Self {
257 min_length: if min_length == usize::MAX {
258 0
259 } else {
260 min_length
261 },
262 max_length,
263 avg_length,
264 most_frequent: None,
265 least_frequent: None,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
272pub struct DateTimeStats {
273 pub min_datetime: String,
274 pub max_datetime: String,
275 #[serde(serialize_with = "crate::serde_helpers::round_2")]
276 pub duration_days: f64,
277 pub year_distribution: HashMap<i32, usize>,
278 pub month_distribution: HashMap<u32, usize>,
279 pub day_of_week_distribution: HashMap<String, usize>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub hour_distribution: Option<HashMap<u32, usize>>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub slash_date_order: Option<SlashDateOrder>,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
293#[serde(rename_all = "snake_case")]
294pub enum SlashDateOrder {
295 DayFirst,
298 MonthFirst,
301 AssumedDayFirst,
304 Mixed,
309}
310
311impl DateTimeStats {
312 pub fn empty() -> Self {
313 Self {
314 min_datetime: String::new(),
315 max_datetime: String::new(),
316 duration_days: 0.0,
317 year_distribution: HashMap::new(),
318 month_distribution: HashMap::new(),
319 day_of_week_distribution: HashMap::new(),
320 hour_distribution: None,
321 slash_date_order: None,
322 }
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
328pub struct BooleanStats {
329 pub true_count: usize,
330 pub false_count: usize,
331 #[serde(serialize_with = "crate::serde_helpers::round_4")]
332 pub true_ratio: f64,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
337pub enum ColumnStats {
338 Numeric(NumericStats),
339 Text(TextStats),
340 DateTime(DateTimeStats),
341 Boolean(BooleanStats),
342 None,
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
353 fn nullable_spread_fields_are_still_required() {
354 let stats = NumericStats {
355 std_dev: None,
356 variance: None,
357 quartiles: Some(Quartiles {
358 q1: 1.0,
359 q2: 2.0,
360 q3: 3.0,
361 iqr: None,
362 }),
363 ..NumericStats::empty()
364 };
365 let document = serde_json::to_value(&stats).unwrap();
366 let restored: NumericStats = serde_json::from_value(document.clone()).unwrap();
367 assert_eq!(serde_json::to_value(restored).unwrap(), document);
368
369 for (parent, key) in [
370 (None, "std_dev"),
371 (None, "variance"),
372 (Some("quartiles"), "iqr"),
373 ] {
374 let mut malformed = document.clone();
375 let object = match parent {
376 Some(parent) => &mut malformed[parent],
377 None => &mut malformed,
378 };
379 object.as_object_mut().unwrap().remove(key).unwrap();
380 let error = serde_json::from_value::<NumericStats>(malformed).unwrap_err();
381 assert!(
382 error
383 .to_string()
384 .contains(&format!("missing field `{key}`")),
385 "{key}: {error}"
386 );
387 }
388 }
389
390 #[test]
391 fn test_column_profile_json_roundtrip() {
392 let profile = ColumnProfile {
393 name: "test_col".to_string(),
394 data_type: DataType::Integer,
395 null_count: 2,
396 total_count: 10,
397 unique_count: Some(8),
398 unique_count_is_approximate: Some(false),
399 unique_count_lower_bound: None,
400 invalid_count: Some(0),
401 type_homogeneity: None,
402 locale_number_count: None,
403 stats: ColumnStats::Numeric(NumericStats {
404 min: 1.0,
405 max: 100.0,
406 mean: 50.5,
407 std_dev: Some(28.87),
408 variance: Some(833.25),
409 median: Some(50.0),
410 quartiles: Some(Quartiles {
411 q1: 25.0,
412 q2: 50.0,
413 q3: 75.0,
414 iqr: Some(50.0),
415 }),
416 mode: Some(42.0),
417 coefficient_of_variation: Some(57.17),
418 skewness: Some(0.0),
419 kurtosis: Some(-1.2),
420 is_approximate: Some(false),
421 outlier_count: Some(0),
422 }),
423 patterns: Some(vec![]),
424 };
425
426 let json = serde_json::to_string(&profile).unwrap();
427 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
428
429 assert_eq!(deserialized.name, "test_col");
430 assert_eq!(deserialized.data_type, DataType::Integer);
431 assert_eq!(deserialized.total_count, 10);
432 assert_eq!(deserialized.null_count, 2);
433 assert_eq!(deserialized.unique_count_is_approximate, Some(false));
434
435 if let ColumnStats::Numeric(n) = &deserialized.stats {
436 assert!((n.min - 1.0).abs() < 0.01);
437 assert!((n.max - 100.0).abs() < 0.01);
438 assert!((n.mean - 50.5).abs() < 0.01);
439 assert!(n.median.is_some());
440 assert!(n.quartiles.is_some());
441 } else {
442 panic!("Expected Numeric stats after roundtrip");
443 }
444 }
445
446 #[test]
447 fn test_text_stats_json_roundtrip() {
448 let profile = ColumnProfile {
449 name: "name".to_string(),
450 data_type: DataType::String,
451 null_count: 0,
452 total_count: 3,
453 unique_count: Some(3),
454 unique_count_is_approximate: Some(false),
455 unique_count_lower_bound: None,
456 invalid_count: None,
457 type_homogeneity: None,
458 locale_number_count: None,
459 stats: ColumnStats::Text(TextStats {
460 min_length: 3,
461 max_length: 7,
462 avg_length: 5.0,
463 most_frequent: None,
464 least_frequent: None,
465 }),
466 patterns: Some(vec![]),
467 };
468
469 let json = serde_json::to_string(&profile).unwrap();
470 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
471
472 assert_eq!(deserialized.data_type, DataType::String);
473 if let ColumnStats::Text(t) = &deserialized.stats {
474 assert_eq!(t.min_length, 3);
475 assert_eq!(t.max_length, 7);
476 } else {
477 panic!("Expected Text stats after roundtrip");
478 }
479 }
480}