use chrono::DateTime;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanceMetrics {
pub version: u64,
pub metadata: TableMetadata,
pub table_properties: HashMap<String, String>,
pub file_stats: FileStatistics,
pub fragment_info: FragmentMetrics,
pub index_info: IndexMetrics,
#[serde(default)]
pub operation_metrics: Option<OperationMetrics>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableMetadata {
pub uuid: String,
pub schema_string: String,
pub field_count: usize,
pub created_time: Option<i64>,
pub last_modified_time: Option<i64>,
pub num_rows: Option<u64>,
pub num_deleted_rows: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileStatistics {
pub num_data_files: usize,
pub num_deletion_files: usize,
pub total_data_size_bytes: u64,
pub total_deletion_size_bytes: u64,
pub avg_data_file_size_bytes: f64,
pub min_data_file_size_bytes: u64,
pub max_data_file_size_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentMetrics {
pub num_fragments: usize,
pub num_fragments_with_deletions: usize,
pub avg_rows_per_fragment: f64,
pub min_rows_per_fragment: u64,
pub max_rows_per_fragment: u64,
pub total_physical_rows: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexMetrics {
pub num_indices: usize,
pub indexed_columns: Vec<String>,
pub index_types: Vec<String>,
pub total_index_size_bytes: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OperationMetrics {
pub total_operations: usize,
pub append_count: usize,
pub delete_count: usize,
pub overwrite_count: usize,
pub compaction_count: usize,
pub index_operation_count: usize,
pub total_bytes_written: u64,
pub operations_per_day: f64,
pub operation_distribution: HashMap<String, f64>,
}
impl Default for LanceMetrics {
fn default() -> Self {
Self {
version: 0,
metadata: TableMetadata {
uuid: String::new(),
schema_string: String::new(),
field_count: 0,
created_time: None,
last_modified_time: None,
num_rows: None,
num_deleted_rows: None,
},
table_properties: HashMap::new(),
file_stats: FileStatistics {
num_data_files: 0,
num_deletion_files: 0,
total_data_size_bytes: 0,
total_deletion_size_bytes: 0,
avg_data_file_size_bytes: 0.0,
min_data_file_size_bytes: 0,
max_data_file_size_bytes: 0,
},
fragment_info: FragmentMetrics {
num_fragments: 0,
num_fragments_with_deletions: 0,
avg_rows_per_fragment: 0.0,
min_rows_per_fragment: 0,
max_rows_per_fragment: 0,
total_physical_rows: 0,
},
index_info: IndexMetrics {
num_indices: 0,
indexed_columns: Vec::new(),
index_types: Vec::new(),
total_index_size_bytes: 0,
},
operation_metrics: None,
}
}
}
impl Display for LanceMetrics {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
writeln!(f)?;
writeln!(f, " Lance Specific Metrics")?;
writeln!(f, "{}", "━".repeat(80))?;
writeln!(f, " {:<40} {:>32}", "Version", self.version)?;
if !self.metadata.uuid.is_empty() {
writeln!(f, " {:<31} {:>40}", "Table UUID", &self.metadata.uuid)?;
}
writeln!(
f,
" {:<40} {:>32}",
"Field Count", self.metadata.field_count
)?;
if let Some(rows) = self.metadata.num_rows {
writeln!(f, " {:<40} {:>32}", "Total Rows", rows)?;
}
if let Some(deleted) = self.metadata.num_deleted_rows {
if deleted > 0 {
writeln!(f, " {:<40} {:>32}", "Deleted Rows", deleted)?;
}
}
if let Some(created) = self.metadata.created_time {
if let Some(dt) = DateTime::from_timestamp_millis(created) {
writeln!(f, " {:<40} {:>32}", "Created Time", dt.to_rfc3339())?;
}
}
if let Some(modified) = self.metadata.last_modified_time {
if let Some(dt) = DateTime::from_timestamp_millis(modified) {
writeln!(f, " {:<40} {:>32}", "Last Modified", dt.to_rfc3339())?;
}
}
if !self.table_properties.is_empty() {
self.table_properties
.iter()
.enumerate()
.try_for_each(|(i, (k, v))| {
if i == 0 {
writeln!(
f,
" {:<20} {:>52}",
"Table Properties",
format!("{}: {}", k, v)
)
} else {
writeln!(f, " {:<20} {:>52}", "", format!("{}: {}", k, v))
}
})?;
}
let file_stats = &self.file_stats;
let total_size_mb = file_stats.total_data_size_bytes as f64 / (1024.0 * 1024.0);
let avg_size_mb = file_stats.avg_data_file_size_bytes / (1024.0 * 1024.0);
let min_size_kb = file_stats.min_data_file_size_bytes as f64 / 1024.0;
let max_size_mb = file_stats.max_data_file_size_bytes as f64 / (1024.0 * 1024.0);
writeln!(f, " File Statistics")?;
writeln!(
f,
" {:<38} {:>32}",
"Data Files", file_stats.num_data_files
)?;
writeln!(
f,
" {:<38} {:>32}",
"Total Data Size",
format!("{:.2} MB", total_size_mb)
)?;
writeln!(
f,
" {:<38} {:>32}",
"Avg Data File Size",
format!("{:.2} MB", avg_size_mb)
)?;
writeln!(
f,
" {:<38} {:>32}",
"Min Data File Size",
format!("{:.2} KB", min_size_kb)
)?;
writeln!(
f,
" {:<38} {:>32}",
"Max Data File Size",
format!("{:.2} MB", max_size_mb)
)?;
if file_stats.num_deletion_files > 0 {
let del_size_mb = file_stats.total_deletion_size_bytes as f64 / (1024.0 * 1024.0);
writeln!(
f,
" {:<38} {:>32}",
"Deletion Files", file_stats.num_deletion_files
)?;
writeln!(
f,
" {:<38} {:>32}",
"Total Deletion Size",
format!("{:.2} MB", del_size_mb)
)?;
}
let fragment_info = &self.fragment_info;
writeln!(f, " Fragment Info")?;
writeln!(
f,
" {:<38} {:>32}",
"Total Fragments", fragment_info.num_fragments
)?;
if fragment_info.num_fragments_with_deletions > 0 {
writeln!(
f,
" {:<38} {:>32}",
"Fragments with Deletions", fragment_info.num_fragments_with_deletions
)?;
}
writeln!(
f,
" {:<38} {:>32}",
"Avg Rows per Fragment",
format!("{:.0}", fragment_info.avg_rows_per_fragment)
)?;
writeln!(
f,
" {:<38} {:>32}",
"Min Rows per Fragment", fragment_info.min_rows_per_fragment
)?;
writeln!(
f,
" {:<38} {:>32}",
"Max Rows per Fragment", fragment_info.max_rows_per_fragment
)?;
writeln!(
f,
" {:<38} {:>32}",
"Total Physical Rows", fragment_info.total_physical_rows
)?;
let index_info = &self.index_info;
if index_info.num_indices > 0 {
let index_size_mb = index_info.total_index_size_bytes as f64 / (1024.0 * 1024.0);
writeln!(f, " Index Info")?;
writeln!(
f,
" {:<38} {:>32}",
"Number of Indices", index_info.num_indices
)?;
if !index_info.indexed_columns.is_empty() {
writeln!(
f,
" {:<38} {:>32}",
"Indexed Columns",
index_info.indexed_columns.join(", ")
)?;
}
if !index_info.index_types.is_empty() {
writeln!(
f,
" {:<38} {:>32}",
"Index Types",
index_info.index_types.join(", ")
)?;
}
writeln!(
f,
" {:<38} {:>32}",
"Total Index Size",
format!("{:.2} MB", index_size_mb)
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lance_metrics_default() {
let metrics = LanceMetrics::default();
assert_eq!(metrics.version, 0);
assert_eq!(metrics.metadata.uuid, "");
assert!(metrics.metadata.num_rows.is_none());
assert!(metrics.metadata.num_deleted_rows.is_none());
assert!(metrics.metadata.created_time.is_none());
assert!(metrics.metadata.last_modified_time.is_none());
assert!(metrics.table_properties.is_empty());
assert_eq!(metrics.file_stats.num_data_files, 0);
assert_eq!(metrics.fragment_info.num_fragments, 0);
assert_eq!(metrics.index_info.num_indices, 0);
}
#[test]
fn test_lance_metrics_display() {
let mut table_properties = HashMap::new();
table_properties.insert("lance.version".to_string(), "0.10.0".to_string());
let metrics = LanceMetrics {
version: 5,
metadata: TableMetadata {
uuid: "test-lance-uuid".to_string(),
schema_string: String::new(),
field_count: 0,
num_rows: Some(1000000),
num_deleted_rows: Some(100),
created_time: Some(1700000000000),
last_modified_time: Some(1700001000000),
},
table_properties,
file_stats: FileStatistics {
num_data_files: 100,
num_deletion_files: 5,
total_data_size_bytes: 1024 * 1024 * 500,
total_deletion_size_bytes: 1024 * 100,
avg_data_file_size_bytes: 1024.0 * 1024.0 * 5.0,
min_data_file_size_bytes: 1024,
max_data_file_size_bytes: 1024 * 1024 * 10,
},
fragment_info: FragmentMetrics {
num_fragments: 50,
num_fragments_with_deletions: 5,
avg_rows_per_fragment: 20000.0,
min_rows_per_fragment: 1000,
max_rows_per_fragment: 50000,
total_physical_rows: 1000100,
},
index_info: IndexMetrics {
num_indices: 2,
indexed_columns: vec!["id".to_string(), "embedding".to_string()],
index_types: vec!["IVF_PQ".to_string()],
total_index_size_bytes: 1024 * 1024 * 50,
},
operation_metrics: None,
};
let display = format!("{}", metrics);
assert!(display.contains("Lance Specific Metrics"));
assert!(display.contains("Version"));
assert!(display.contains("5"));
assert!(display.contains("test-lance-uuid"));
assert!(display.contains("Total Rows"));
assert!(display.contains("1000000"));
assert!(display.contains("Deleted Rows"));
assert!(display.contains("File Statistics"));
assert!(display.contains("Fragment Info"));
assert!(display.contains("Index Info"));
assert!(display.contains("Table Properties"));
}
#[test]
fn test_lance_metrics_display_minimal() {
let metrics = LanceMetrics::default();
let display = format!("{}", metrics);
assert!(display.contains("Lance Specific Metrics"));
assert!(display.contains("Version"));
assert!(display.contains("File Statistics"));
assert!(display.contains("Fragment Info"));
}
#[test]
fn test_lance_metrics_display_no_indices() {
let metrics = LanceMetrics::default();
let display = format!("{}", metrics);
assert!(!display.contains("Index Info"));
}
#[test]
fn test_lance_metrics_display_with_deletions() {
let mut metrics = LanceMetrics::default();
metrics.metadata.num_deleted_rows = Some(500);
metrics.fragment_info.num_fragments_with_deletions = 10;
let display = format!("{}", metrics);
assert!(display.contains("Deleted Rows"));
assert!(display.contains("500"));
assert!(display.contains("Fragments with Deletions"));
assert!(display.contains("10"));
}
#[test]
fn test_lance_metrics_default_has_empty_table_properties() {
let metrics = LanceMetrics::default();
assert!(metrics.table_properties.is_empty());
}
}