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(default, skip_serializing_if = "Option::is_none")]
39 pub invalid_count: Option<usize>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub type_homogeneity: Option<TypeHomogeneity>,
59 pub stats: ColumnStats,
60 pub patterns: Option<Vec<Pattern>>,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
71pub struct Quartiles {
72 pub q1: f64,
73 pub q2: f64,
74 pub q3: f64,
75 pub iqr: f64,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
80pub struct FrequencyItem {
81 pub value: String,
82 pub count: usize,
83 #[serde(serialize_with = "crate::serde_helpers::round_2")]
84 pub percentage: f64,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
89pub struct NumericStats {
90 #[serde(serialize_with = "crate::serde_helpers::round_4")]
94 pub min: f64,
95 #[serde(serialize_with = "crate::serde_helpers::round_4")]
96 pub max: f64,
97 #[serde(serialize_with = "crate::serde_helpers::round_4")]
98 pub mean: f64,
99 #[serde(serialize_with = "crate::serde_helpers::round_4")]
100 pub std_dev: f64,
101 #[serde(serialize_with = "crate::serde_helpers::round_4")]
102 pub variance: f64,
103 #[serde(
104 skip_serializing_if = "Option::is_none",
105 serialize_with = "crate::serde_helpers::round_4_opt"
106 )]
107 pub median: Option<f64>,
108 #[serde(
109 skip_serializing_if = "Option::is_none",
110 serialize_with = "crate::serde_helpers::quartiles::serialize"
111 )]
112 pub quartiles: Option<Quartiles>,
113 #[serde(
114 skip_serializing_if = "Option::is_none",
115 serialize_with = "crate::serde_helpers::round_4_opt"
116 )]
117 pub mode: Option<f64>,
118 #[serde(
119 skip_serializing_if = "Option::is_none",
120 serialize_with = "crate::serde_helpers::round_2_opt"
121 )]
122 pub coefficient_of_variation: Option<f64>,
123 #[serde(
124 skip_serializing_if = "Option::is_none",
125 serialize_with = "crate::serde_helpers::round_4_opt"
126 )]
127 pub skewness: Option<f64>,
128 #[serde(
129 skip_serializing_if = "Option::is_none",
130 serialize_with = "crate::serde_helpers::round_4_opt"
131 )]
132 pub kurtosis: Option<f64>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub is_approximate: Option<bool>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub outlier_count: Option<usize>,
143}
144
145impl NumericStats {
146 pub fn empty() -> Self {
147 Self {
148 min: 0.0,
149 max: 0.0,
150 mean: 0.0,
151 std_dev: 0.0,
152 variance: 0.0,
153 median: None,
154 quartiles: None,
155 mode: None,
156 coefficient_of_variation: None,
157 skewness: None,
158 kurtosis: None,
159 is_approximate: None,
160 outlier_count: None,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
167pub struct TextStats {
168 pub min_length: usize,
169 pub max_length: usize,
170 #[serde(serialize_with = "crate::serde_helpers::round_4")]
173 pub avg_length: f64,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub most_frequent: Option<Vec<FrequencyItem>>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub least_frequent: Option<Vec<FrequencyItem>>,
178}
179
180impl TextStats {
181 pub fn empty() -> Self {
182 Self {
183 min_length: 0,
184 max_length: 0,
185 avg_length: 0.0,
186 most_frequent: None,
187 least_frequent: None,
188 }
189 }
190
191 pub fn from_lengths(min_length: usize, max_length: usize, avg_length: f64) -> Self {
192 Self {
193 min_length: if min_length == usize::MAX {
194 0
195 } else {
196 min_length
197 },
198 max_length,
199 avg_length,
200 most_frequent: None,
201 least_frequent: None,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
208pub struct DateTimeStats {
209 pub min_datetime: String,
210 pub max_datetime: String,
211 #[serde(serialize_with = "crate::serde_helpers::round_2")]
212 pub duration_days: f64,
213 pub year_distribution: HashMap<i32, usize>,
214 pub month_distribution: HashMap<u32, usize>,
215 pub day_of_week_distribution: HashMap<String, usize>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub hour_distribution: Option<HashMap<u32, usize>>,
218}
219
220impl DateTimeStats {
221 pub fn empty() -> Self {
222 Self {
223 min_datetime: String::new(),
224 max_datetime: String::new(),
225 duration_days: 0.0,
226 year_distribution: HashMap::new(),
227 month_distribution: HashMap::new(),
228 day_of_week_distribution: HashMap::new(),
229 hour_distribution: None,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
236pub struct BooleanStats {
237 pub true_count: usize,
238 pub false_count: usize,
239 #[serde(serialize_with = "crate::serde_helpers::round_4")]
240 pub true_ratio: f64,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
245pub enum ColumnStats {
246 Numeric(NumericStats),
247 Text(TextStats),
248 DateTime(DateTimeStats),
249 Boolean(BooleanStats),
250 None,
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_column_profile_json_roundtrip() {
259 let profile = ColumnProfile {
260 name: "test_col".to_string(),
261 data_type: DataType::Integer,
262 null_count: 2,
263 total_count: 10,
264 unique_count: Some(8),
265 unique_count_is_approximate: Some(false),
266 invalid_count: Some(0),
267 type_homogeneity: None,
268 stats: ColumnStats::Numeric(NumericStats {
269 min: 1.0,
270 max: 100.0,
271 mean: 50.5,
272 std_dev: 28.87,
273 variance: 833.25,
274 median: Some(50.0),
275 quartiles: Some(Quartiles {
276 q1: 25.0,
277 q2: 50.0,
278 q3: 75.0,
279 iqr: 50.0,
280 }),
281 mode: Some(42.0),
282 coefficient_of_variation: Some(57.17),
283 skewness: Some(0.0),
284 kurtosis: Some(-1.2),
285 is_approximate: Some(false),
286 outlier_count: Some(0),
287 }),
288 patterns: Some(vec![]),
289 };
290
291 let json = serde_json::to_string(&profile).unwrap();
292 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
293
294 assert_eq!(deserialized.name, "test_col");
295 assert_eq!(deserialized.data_type, DataType::Integer);
296 assert_eq!(deserialized.total_count, 10);
297 assert_eq!(deserialized.null_count, 2);
298 assert_eq!(deserialized.unique_count_is_approximate, Some(false));
299
300 if let ColumnStats::Numeric(n) = &deserialized.stats {
301 assert!((n.min - 1.0).abs() < 0.01);
302 assert!((n.max - 100.0).abs() < 0.01);
303 assert!((n.mean - 50.5).abs() < 0.01);
304 assert!(n.median.is_some());
305 assert!(n.quartiles.is_some());
306 } else {
307 panic!("Expected Numeric stats after roundtrip");
308 }
309 }
310
311 #[test]
312 fn test_text_stats_json_roundtrip() {
313 let profile = ColumnProfile {
314 name: "name".to_string(),
315 data_type: DataType::String,
316 null_count: 0,
317 total_count: 3,
318 unique_count: Some(3),
319 unique_count_is_approximate: Some(false),
320 invalid_count: None,
321 type_homogeneity: None,
322 stats: ColumnStats::Text(TextStats {
323 min_length: 3,
324 max_length: 7,
325 avg_length: 5.0,
326 most_frequent: None,
327 least_frequent: None,
328 }),
329 patterns: Some(vec![]),
330 };
331
332 let json = serde_json::to_string(&profile).unwrap();
333 let deserialized: ColumnProfile = serde_json::from_str(&json).unwrap();
334
335 assert_eq!(deserialized.data_type, DataType::String);
336 if let ColumnStats::Text(t) = &deserialized.stats {
337 assert_eq!(t.min_length, 3);
338 assert_eq!(t.max_length, 7);
339 } else {
340 panic!("Expected Text stats after roundtrip");
341 }
342 }
343}