1#![allow(
12 clippy::cast_precision_loss,
13 reason = "diagnostic rate/throughput calculations displayed to user; >2^53 bytes is unreachable"
14)]
15
16use serde::Serialize;
17use std::time::Instant;
18
19#[derive(Debug)]
21pub struct StatsTimer {
22 start: Instant,
23}
24
25impl StatsTimer {
26 #[must_use]
27 pub fn start() -> Self {
28 Self {
29 start: Instant::now(),
30 }
31 }
32
33 #[must_use]
34 pub fn elapsed_ms(&self) -> u64 {
35 u64::try_from(self.start.elapsed().as_millis()).unwrap_or(u64::MAX)
42 }
43}
44
45#[derive(Debug, Clone, Serialize)]
50pub struct IngestStats {
51 pub operation: String,
52 pub rows: u64,
58 pub elapsed_ms: u64,
59 pub bytes_read: u64,
61 pub bytes_stored: u64,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub schema_inference_ms: Option<u64>,
65 pub table: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub file_format: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub warning: Option<String>,
71 #[serde(skip_serializing_if = "is_false")]
77 #[serde(default)]
78 pub schema_changed: bool,
79}
80
81#[allow(
82 clippy::trivially_copy_pass_by_ref,
83 reason = "serde skip_serializing_if requires `&T -> bool`"
84)]
85fn is_false(b: &bool) -> bool {
86 !*b
87}
88
89impl IngestStats {
90 #[must_use]
93 pub fn rows_per_sec(&self) -> u64 {
94 if self.elapsed_ms == 0 {
95 return self.rows;
96 }
97 #[expect(
98 clippy::cast_precision_loss,
99 clippy::cast_possible_truncation,
100 clippy::cast_sign_loss,
101 reason = "diagnostic rate: operands are non-negative u64 throughput counters; Rust's f64→u64 cast saturates at u64::MAX, which is strictly better than overflow for a reported rate"
102 )]
103 let rate = (self.rows as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
104 rate
105 }
106
107 #[must_use]
109 pub fn ingest_throughput_mb_sec(&self) -> f64 {
110 if self.elapsed_ms == 0 {
111 return 0.0;
112 }
113 (self.bytes_read as f64 / 1_000_000.0) / (self.elapsed_ms as f64 / 1000.0)
114 }
115
116 #[must_use]
119 pub fn compression_ratio(&self) -> f64 {
120 if self.bytes_read == 0 {
121 return 1.0;
122 }
123 self.bytes_stored as f64 / self.bytes_read as f64
124 }
125
126 #[must_use]
128 pub fn to_json(&self) -> serde_json::Value {
129 let mut v = serde_json::to_value(self).unwrap_or_default();
130 if let Some(obj) = v.as_object_mut() {
131 obj.insert("rows_per_sec".into(), self.rows_per_sec().into());
132 obj.insert(
133 "ingest_throughput_mb_sec".into(),
134 serde_json::json!(self.ingest_throughput_mb_sec()),
135 );
136 obj.insert(
137 "compression_ratio".into(),
138 serde_json::json!(self.compression_ratio()),
139 );
140 }
141 v
142 }
143}
144
145#[derive(Debug, Clone, Serialize)]
147pub struct QueryStats {
148 pub operation: String,
149 pub rows_returned: u64,
150 pub rows_scanned: u64,
152 pub elapsed_ms: u64,
153 pub result_size_bytes: u64,
155 pub tables_touched: Vec<String>,
156}
157
158impl QueryStats {
159 #[must_use]
161 pub fn scan_rate_rows_sec(&self) -> u64 {
162 if self.elapsed_ms == 0 {
163 return self.rows_scanned;
164 }
165 #[expect(
166 clippy::cast_precision_loss,
167 clippy::cast_possible_truncation,
168 clippy::cast_sign_loss,
169 reason = "diagnostic rate: operands are non-negative u64 counters; Rust's f64→u64 cast saturates at u64::MAX, which is strictly better than overflow for a reported rate"
170 )]
171 let rate = (self.rows_scanned as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
172 rate
173 }
174
175 #[must_use]
177 pub fn to_json(&self) -> serde_json::Value {
178 let mut v = serde_json::to_value(self).unwrap_or_default();
179 if let Some(obj) = v.as_object_mut() {
180 obj.insert(
181 "scan_rate_rows_sec".into(),
182 self.scan_rate_rows_sec().into(),
183 );
184 }
185 v
186 }
187}
188
189#[derive(Debug, Clone, Serialize)]
191pub struct ExportStats {
192 pub operation: String,
193 pub rows: u64,
194 pub elapsed_ms: u64,
195 pub file_size_bytes: u64,
196 pub format: String,
197 pub output_path: String,
198}
199
200impl ExportStats {
201 #[must_use]
203 pub fn rows_per_sec(&self) -> u64 {
204 if self.elapsed_ms == 0 {
205 return self.rows;
206 }
207 #[expect(
208 clippy::cast_precision_loss,
209 clippy::cast_possible_truncation,
210 clippy::cast_sign_loss,
211 reason = "diagnostic rate: operands are non-negative u64 counters; Rust's f64→u64 cast saturates at u64::MAX, which is strictly better than overflow for a reported rate"
212 )]
213 let rate = (self.rows as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
214 rate
215 }
216
217 #[must_use]
219 pub fn to_json(&self) -> serde_json::Value {
220 let mut v = serde_json::to_value(self).unwrap_or_default();
221 if let Some(obj) = v.as_object_mut() {
222 obj.insert("rows_per_sec".into(), self.rows_per_sec().into());
223 }
224 v
225 }
226}