#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum TruncationReason {
MaxRows(u64),
MaxBytes(u64),
MemoryPressure,
StopCondition(String),
StreamClosed,
Timeout,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExecutionMetadata {
pub rows_processed: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes_consumed: Option<u64>,
pub columns_detected: usize,
pub scan_time_ms: u128,
#[serde(skip_serializing_if = "Option::is_none")]
pub throughput_rows_sec: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_peak_mb: Option<f64>,
pub error_count: usize,
pub source_exhausted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_reason: Option<TruncationReason>,
pub sampling_applied: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling_ratio: Option<f64>,
}
impl ExecutionMetadata {
pub fn new(rows_processed: usize, columns_detected: usize, scan_time_ms: u128) -> Self {
let throughput_rows_sec = if scan_time_ms > 0 {
Some(rows_processed as f64 / (scan_time_ms as f64 / 1000.0))
} else {
None
};
Self {
rows_processed,
bytes_consumed: None,
columns_detected,
scan_time_ms,
throughput_rows_sec,
memory_peak_mb: None,
error_count: 0,
source_exhausted: true,
truncation_reason: None,
sampling_applied: false,
sampling_ratio: None,
}
}
pub fn with_sampling(mut self, ratio: f64) -> Self {
self.sampling_applied = true;
self.sampling_ratio = Some(ratio);
self
}
pub fn with_source_exhausted(mut self, exhausted: bool) -> Self {
self.source_exhausted = exhausted;
self
}
pub fn with_truncation(mut self, reason: TruncationReason) -> Self {
self.source_exhausted = false;
self.truncation_reason = Some(reason);
self
}
pub fn with_bytes_consumed(mut self, bytes: u64) -> Self {
self.bytes_consumed = Some(bytes);
self
}
pub fn with_error_count(mut self, count: usize) -> Self {
self.error_count = count;
self
}
pub fn with_memory_peak_mb(mut self, mb: f64) -> Self {
self.memory_peak_mb = Some(mb);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_metadata_throughput_calculation() {
let meta = ExecutionMetadata::new(1000, 5, 500);
assert!(meta.throughput_rows_sec.is_some());
assert!((meta.throughput_rows_sec.unwrap() - 2000.0).abs() < 1.0);
assert!(meta.source_exhausted);
assert!(!meta.sampling_applied);
assert!(meta.sampling_ratio.is_none());
}
#[test]
fn test_execution_metadata_zero_time_no_throughput() {
let meta = ExecutionMetadata::new(100, 3, 0);
assert!(meta.throughput_rows_sec.is_none());
}
#[test]
fn test_execution_metadata_with_sampling() {
let meta = ExecutionMetadata::new(500, 3, 100).with_sampling(0.5);
assert!(meta.sampling_applied);
assert_eq!(meta.sampling_ratio, Some(0.5));
}
#[test]
fn test_execution_metadata_with_truncation() {
let meta =
ExecutionMetadata::new(1000, 5, 200).with_truncation(TruncationReason::MaxRows(1000));
assert!(!meta.source_exhausted);
assert!(meta.truncation_reason.is_some());
}
#[test]
fn test_truncation_reason_serde_roundtrip() {
let reasons = vec![
TruncationReason::MaxRows(5000),
TruncationReason::MaxBytes(1_000_000),
TruncationReason::MemoryPressure,
TruncationReason::StopCondition("accuracy > 0.95".to_string()),
TruncationReason::StreamClosed,
TruncationReason::Timeout,
];
for reason in reasons {
let json = serde_json::to_string(&reason).unwrap();
let deserialized: TruncationReason = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&deserialized).unwrap();
assert_eq!(json, json2);
}
}
}