#![allow(
clippy::cast_precision_loss,
reason = "diagnostic rate/throughput calculations displayed to user; >2^53 bytes is unreachable"
)]
use serde::Serialize;
use std::time::Instant;
#[derive(Debug)]
pub struct StatsTimer {
start: Instant,
}
impl StatsTimer {
#[must_use]
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}
#[must_use]
pub fn elapsed_ms(&self) -> u64 {
u64::try_from(self.start.elapsed().as_millis()).unwrap_or(u64::MAX)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IngestStats {
pub operation: String,
pub rows: u64,
pub elapsed_ms: u64,
pub bytes_read: u64,
pub bytes_stored: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_inference_ms: Option<u64>,
pub table: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
#[serde(skip_serializing_if = "is_false")]
#[serde(default)]
pub schema_changed: bool,
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde skip_serializing_if requires `&T -> bool`"
)]
fn is_false(b: &bool) -> bool {
!*b
}
impl IngestStats {
#[must_use]
pub fn rows_per_sec(&self) -> u64 {
if self.elapsed_ms == 0 {
return self.rows;
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
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"
)]
let rate = (self.rows as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
rate
}
#[must_use]
pub fn ingest_throughput_mb_sec(&self) -> f64 {
if self.elapsed_ms == 0 {
return 0.0;
}
(self.bytes_read as f64 / 1_000_000.0) / (self.elapsed_ms as f64 / 1000.0)
}
#[must_use]
pub fn compression_ratio(&self) -> f64 {
if self.bytes_read == 0 {
return 1.0;
}
self.bytes_stored as f64 / self.bytes_read as f64
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
let mut v = serde_json::to_value(self).unwrap_or_default();
if let Some(obj) = v.as_object_mut() {
obj.insert("rows_per_sec".into(), self.rows_per_sec().into());
obj.insert(
"ingest_throughput_mb_sec".into(),
serde_json::json!(self.ingest_throughput_mb_sec()),
);
obj.insert(
"compression_ratio".into(),
serde_json::json!(self.compression_ratio()),
);
}
v
}
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryStats {
pub operation: String,
pub rows_returned: u64,
pub rows_scanned: u64,
pub elapsed_ms: u64,
pub result_size_bytes: u64,
pub tables_touched: Vec<String>,
}
impl QueryStats {
#[must_use]
pub fn scan_rate_rows_sec(&self) -> u64 {
if self.elapsed_ms == 0 {
return self.rows_scanned;
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
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"
)]
let rate = (self.rows_scanned as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
rate
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
let mut v = serde_json::to_value(self).unwrap_or_default();
if let Some(obj) = v.as_object_mut() {
obj.insert(
"scan_rate_rows_sec".into(),
self.scan_rate_rows_sec().into(),
);
}
v
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ExportStats {
pub operation: String,
pub rows: u64,
pub elapsed_ms: u64,
pub file_size_bytes: u64,
pub format: String,
pub output_path: String,
}
impl ExportStats {
#[must_use]
pub fn rows_per_sec(&self) -> u64 {
if self.elapsed_ms == 0 {
return self.rows;
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
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"
)]
let rate = (self.rows as f64 / (self.elapsed_ms as f64 / 1000.0)) as u64;
rate
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
let mut v = serde_json::to_value(self).unwrap_or_default();
if let Some(obj) = v.as_object_mut() {
obj.insert("rows_per_sec".into(), self.rows_per_sec().into());
}
v
}
}