dataprof_core/
execution.rs1#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
3pub enum TruncationReason {
4 MaxRows(u64),
6 MaxBytes(u64),
8 MemoryPressure,
10 StopCondition(String),
12 StreamClosed,
14 Timeout,
16}
17
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
20pub struct ExecutionMetadata {
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub engine: Option<String>,
24 pub rows_processed: usize,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub bytes_consumed: Option<u64>,
29 pub columns_detected: usize,
31 pub scan_time_ms: u128,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub throughput_rows_sec: Option<f64>,
36 #[serde(skip_serializing_if = "Option::is_none")]
40 pub memory_peak_mb: Option<f64>,
41 pub error_count: usize,
43 #[serde(default)]
51 #[schemars(default)]
52 pub ragged_row_count: usize,
53 pub source_exhausted: bool,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub truncation_reason: Option<TruncationReason>,
58 pub sampling_applied: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub sampling_ratio: Option<f64>,
63}
64
65impl ExecutionMetadata {
66 pub fn new(rows_processed: usize, columns_detected: usize, scan_time_ms: u128) -> Self {
68 let throughput_rows_sec = if scan_time_ms > 0 {
69 Some(rows_processed as f64 / (scan_time_ms as f64 / 1000.0))
70 } else {
71 None
72 };
73
74 Self {
75 engine: None,
76 rows_processed,
77 bytes_consumed: None,
78 columns_detected,
79 scan_time_ms,
80 throughput_rows_sec,
81 memory_peak_mb: None,
82 error_count: 0,
83 ragged_row_count: 0,
84 source_exhausted: true,
85 truncation_reason: None,
86 sampling_applied: false,
87 sampling_ratio: None,
88 }
89 }
90
91 pub fn with_engine(mut self, engine: impl Into<String>) -> Self {
93 self.engine = Some(engine.into());
94 self
95 }
96
97 pub fn with_sampling(mut self, ratio: f64) -> Self {
99 self.sampling_applied = true;
100 self.sampling_ratio = Some(ratio);
101 self
102 }
103
104 pub fn with_source_exhausted(mut self, exhausted: bool) -> Self {
106 self.source_exhausted = exhausted;
107 self
108 }
109
110 pub fn with_truncation(mut self, reason: TruncationReason) -> Self {
112 self.source_exhausted = false;
113 self.truncation_reason = Some(reason);
114 self
115 }
116
117 pub fn with_bytes_consumed(mut self, bytes: u64) -> Self {
119 self.bytes_consumed = Some(bytes);
120 self
121 }
122
123 pub fn with_error_count(mut self, count: usize) -> Self {
125 self.error_count = count;
126 self
127 }
128
129 pub fn with_ragged_row_count(mut self, count: usize) -> Self {
131 self.ragged_row_count = count;
132 self
133 }
134
135 pub fn with_memory_peak_mb(mut self, mb: f64) -> Self {
137 self.memory_peak_mb = Some(mb);
138 self
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_execution_metadata_throughput_calculation() {
148 let meta = ExecutionMetadata::new(1000, 5, 500);
149 assert!(meta.throughput_rows_sec.is_some());
150 assert!((meta.throughput_rows_sec.unwrap() - 2000.0).abs() < 1.0);
151 assert!(meta.source_exhausted);
152 assert!(!meta.sampling_applied);
153 assert!(meta.sampling_ratio.is_none());
154 }
155
156 #[test]
157 fn test_execution_metadata_zero_time_no_throughput() {
158 let meta = ExecutionMetadata::new(100, 3, 0);
159 assert!(meta.throughput_rows_sec.is_none());
160 }
161
162 #[test]
163 fn test_execution_metadata_with_engine() {
164 let meta = ExecutionMetadata::new(100, 3, 25).with_engine("incremental");
165 assert_eq!(meta.engine.as_deref(), Some("incremental"));
166 }
167
168 #[test]
169 fn test_execution_metadata_with_sampling() {
170 let meta = ExecutionMetadata::new(500, 3, 100).with_sampling(0.5);
171 assert!(meta.sampling_applied);
172 assert_eq!(meta.sampling_ratio, Some(0.5));
173 }
174
175 #[test]
176 fn test_execution_metadata_ragged_row_count() {
177 let meta = ExecutionMetadata::new(100, 3, 25);
178 assert_eq!(meta.ragged_row_count, 0);
179 let meta = meta.with_ragged_row_count(2);
180 assert_eq!(meta.ragged_row_count, 2);
181 }
182
183 #[test]
184 fn test_execution_metadata_ragged_defaults_when_absent() {
185 let json = r#"{
188 "rows_processed": 10,
189 "columns_detected": 2,
190 "scan_time_ms": 5,
191 "error_count": 0,
192 "source_exhausted": true,
193 "sampling_applied": false
194 }"#;
195 let meta: ExecutionMetadata = serde_json::from_str(json).unwrap();
196 assert_eq!(meta.ragged_row_count, 0);
197 }
198
199 #[test]
200 fn test_execution_metadata_with_truncation() {
201 let meta =
202 ExecutionMetadata::new(1000, 5, 200).with_truncation(TruncationReason::MaxRows(1000));
203 assert!(!meta.source_exhausted);
204 assert!(meta.truncation_reason.is_some());
205 }
206
207 #[test]
208 fn test_truncation_reason_serde_roundtrip() {
209 let reasons = vec![
210 TruncationReason::MaxRows(5000),
211 TruncationReason::MaxBytes(1_000_000),
212 TruncationReason::MemoryPressure,
213 TruncationReason::StopCondition("accuracy > 0.95".to_string()),
214 TruncationReason::StreamClosed,
215 TruncationReason::Timeout,
216 ];
217
218 for reason in reasons {
219 let json = serde_json::to_string(&reason).unwrap();
220 let deserialized: TruncationReason = serde_json::from_str(&json).unwrap();
221 let json2 = serde_json::to_string(&deserialized).unwrap();
222 assert_eq!(json, json2);
223 }
224 }
225}