Skip to main content

pgdrift_core/
analyzer.rs

1use crate::stats::FieldStats;
2use serde_json::Value;
3use std::collections::HashMap;
4
5pub struct JsonAnalyzer {
6    stats: HashMap<String, FieldStats>,
7    total_samples: u64,
8}
9
10impl Default for JsonAnalyzer {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl JsonAnalyzer {
17    pub fn new() -> Self {
18        Self {
19            stats: HashMap::new(),
20            total_samples: 0,
21        }
22    }
23
24    /// Analyze a sing json document
25    pub fn analyze(&mut self, value: &Value) {
26        self.total_samples += 1;
27        self.walk("", value, 0);
28    }
29
30    /// Recursive walk
31    fn walk(&mut self, path: &str, value: &Value, depth: usize) {
32        match value {
33            Value::Object(map) => {
34                for (key, val) in map {
35                    let field_path = if path.is_empty() {
36                        key.clone()
37                    } else {
38                        format!("{}.{}", path, key)
39                    };
40
41                    self.record_field(&field_path, val, depth + 1);
42
43                    self.walk(&field_path, val, depth + 1);
44                }
45            }
46            Value::Array(arr) => {
47                let array_path = format!("{}[]", path);
48
49                for item in arr {
50                    self.walk(&array_path, item, depth + 1);
51                }
52            }
53            _ => {
54                // Leaf node recorded by parent
55            }
56        }
57    }
58
59    fn record_field(&mut self, path: &str, value: &Value, depth: usize) {
60        self.stats
61            .entry(path.to_string())
62            .or_insert_with(|| FieldStats::new(path.to_string(), depth))
63            .record(value);
64    }
65
66    pub fn finalize(mut self) -> HashMap<String, FieldStats> {
67        for stats in self.stats.values_mut() {
68            stats.finalize(self.total_samples);
69        }
70        self.stats
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::types::JsonType;
78    use serde_json::json;
79
80    #[test]
81    fn test_flat_object() {
82        let mut analyzer = JsonAnalyzer::new();
83
84        analyzer.analyze(&json!({
85            "name": "Alice",
86            "age": 30
87        }));
88
89        let stats = analyzer.finalize();
90
91        assert_eq!(stats.len(), 2);
92        assert!(stats.contains_key("name"));
93        assert!(stats.contains_key("age"));
94
95        let name_stats = &stats["name"];
96        assert_eq!(name_stats.occurrences, 1);
97        assert_eq!(name_stats.density, 1.0);
98        assert_eq!(name_stats.depth, 1);
99    }
100
101    #[test]
102    fn test_nested_object() {
103        let mut analyzer = JsonAnalyzer::new();
104
105        analyzer.analyze(&json!({
106            "user": {
107                "profile": {
108                    "email": "test@example.com"
109                }
110            }
111        }));
112
113        let stats = analyzer.finalize();
114
115        assert!(stats.contains_key("user"));
116        assert!(stats.contains_key("user.profile"));
117        assert!(stats.contains_key("user.profile.email"));
118
119        assert_eq!(stats["user.profile.email"].depth, 3);
120    }
121
122    #[test]
123    fn test_array_with_objects() {
124        let mut analyzer = JsonAnalyzer::new();
125
126        analyzer.analyze(&json!({
127            "addresses": [
128                {"city": "Brisbane", "zip": "4000"},
129                {"city": "Sydney", "zip": "2000"}
130            ]
131        }));
132
133        let stats = analyzer.finalize();
134
135        // Should have: addresses, addresses[], addresses[].city, addresses[].zip
136        assert!(stats.contains_key("addresses"));
137        assert!(stats.contains_key("addresses[].city"));
138        assert!(stats.contains_key("addresses[].zip"));
139
140        // Each item in array was seen once, but we analyzed 1 document
141        let city_stats = &stats["addresses[].city"];
142        assert_eq!(city_stats.occurrences, 2); // Appears in both array items
143    }
144
145    #[test]
146    fn test_multiple_documents_density() {
147        let mut analyzer = JsonAnalyzer::new();
148
149        // 3 documents, "nickname" only in 1
150        analyzer.analyze(&json!({"name": "Alice", "nickname": "Al"}));
151        analyzer.analyze(&json!({"name": "Bob"}));
152        analyzer.analyze(&json!({"name": "Carol"}));
153
154        let stats = analyzer.finalize();
155
156        let name_stats = &stats["name"];
157        assert_eq!(name_stats.occurrences, 3);
158        assert_eq!(name_stats.density, 1.0); // 100%
159
160        let nickname_stats = &stats["nickname"];
161        assert_eq!(nickname_stats.occurrences, 1);
162        assert!((nickname_stats.density - 0.333).abs() < 0.01); // ~33%
163    }
164
165    #[test]
166    fn test_type_inconsistency() {
167        let mut analyzer = JsonAnalyzer::new();
168
169        analyzer.analyze(&json!({"age": "25"})); // String
170        analyzer.analyze(&json!({"age": 30})); // Number
171        analyzer.analyze(&json!({"age": "35"})); // String
172
173        let stats = analyzer.finalize();
174        let age_stats = &stats["age"];
175
176        // Should track both types
177        assert_eq!(age_stats.types.get(&JsonType::String), Some(&2));
178        assert_eq!(age_stats.types.get(&JsonType::Number), Some(&1));
179    }
180
181    #[test]
182    fn test_null_tracking() {
183        let mut analyzer = JsonAnalyzer::new();
184
185        analyzer.analyze(&json!({"optional": null}));
186        analyzer.analyze(&json!({"optional": "value"}));
187        analyzer.analyze(&json!({"optional": null}));
188
189        let stats = analyzer.finalize();
190        let optional_stats = &stats["optional"];
191
192        assert_eq!(optional_stats.null_count, 2);
193        assert_eq!(optional_stats.occurrences, 3);
194    }
195
196    #[test]
197    fn test_empty_array() {
198        let mut analyzer = JsonAnalyzer::new();
199
200        analyzer.analyze(&json!({"items": []}));
201
202        let stats = analyzer.finalize();
203
204        // Should record the array field itself
205        assert!(stats.contains_key("items"));
206
207        // But no items in the array
208        assert!(!stats.contains_key("items[]"));
209    }
210
211    #[test]
212    fn test_deep_nesting() {
213        let mut analyzer = JsonAnalyzer::new();
214
215        analyzer.analyze(&json!({
216            "level1": {
217                "level2": {
218                    "level3": {
219                        "level4": {
220                            "value": "deep"
221                        }
222                    }
223                }
224            }
225        }));
226
227        let stats = analyzer.finalize();
228
229        assert_eq!(stats["level1"].depth, 1);
230        assert_eq!(stats["level1.level2"].depth, 2);
231        assert_eq!(stats["level1.level2.level3"].depth, 3);
232        assert_eq!(stats["level1.level2.level3.level4"].depth, 4);
233        assert_eq!(stats["level1.level2.level3.level4.value"].depth, 5);
234    }
235
236    #[test]
237    fn test_examples_collection() {
238        let mut analyzer = JsonAnalyzer::new();
239
240        for i in 0..15 {
241            analyzer.analyze(&json!({"value": i}));
242        }
243
244        let stats = analyzer.finalize();
245        let value_stats = &stats["value"];
246
247        // Should limit to 10 examples
248        assert_eq!(value_stats.examples.len(), 10);
249    }
250}