use snafu::{Backtrace, prelude::*};
use crate::storage::StorageError;
pub use crate::metadata::segments::{
FileFormat, SegmentEntityLayout, SegmentMeta, SegmentMetaError,
};
#[derive(Debug, Snafu)]
pub enum SegmentIoError {
#[snafu(display("Segment file missing or not a regular file: {path}"))]
MissingFile {
path: String,
backtrace: Backtrace,
},
#[snafu(display("I/O error while validating segment at {path}: {source}"))]
Storage {
path: String,
#[snafu(source, backtrace)]
source: StorageError,
},
}
#[derive(Debug, Snafu)]
pub enum SegmentError {
#[snafu(transparent)]
Io {
source: SegmentIoError,
},
#[snafu(transparent)]
Meta {
source: SegmentMetaError,
},
}
#[allow(clippy::result_large_err)]
pub type SegmentResult<T> = Result<T, SegmentError>;
pub fn map_storage_error(err: StorageError) -> SegmentError {
let (is_missing, path) = match &err {
StorageError::NotFound { path, .. } => (true, path.clone()),
StorageError::AlreadyExists { path, .. }
| StorageError::OtherIo { path, .. }
| StorageError::CleanupFailed { path, .. } => (false, path.clone()),
};
if is_missing {
SegmentIoError::MissingFile {
path,
backtrace: Backtrace::capture(),
}
.into()
} else {
SegmentIoError::Storage { path, source: err }.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use chrono::{DateTime, TimeZone};
fn utc_datetime(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> DateTime<Utc> {
Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.expect("valid UTC timestamp")
}
fn sample_segment_meta() -> SegmentMeta {
SegmentMeta {
path: "data/seg-001.parquet".to_string(),
format: FileFormat::Parquet,
entity_layout: SegmentEntityLayout::NotApplicable,
index_min: (utc_datetime(2025, 1, 1, 0, 0, 0)).into(),
index_max: (utc_datetime(2025, 1, 1, 1, 0, 0)).into(),
row_count: 123,
file_size: None,
coverage_path: None,
}
}
#[test]
fn segment_meta_json_roundtrip_with_and_without_coverage_path() {
let seg = sample_segment_meta();
let json = serde_json::to_string(&seg).unwrap();
let back: SegmentMeta = serde_json::from_str(&json).unwrap();
assert_eq!(back.coverage_path, None);
assert_eq!(back.file_size, None);
let mut seg2 = sample_segment_meta().with_coverage_path("_coverage/segments/a.roar");
seg2.file_size = Some(42);
let json2 = serde_json::to_string(&seg2).unwrap();
let back2: SegmentMeta = serde_json::from_str(&json2).unwrap();
assert_eq!(
back2.coverage_path.as_deref(),
Some("_coverage/segments/a.roar")
);
assert_eq!(back2.file_size, Some(42));
}
#[test]
fn segment_meta_json_requires_entity_layout() {
let mut value = serde_json::to_value(sample_segment_meta()).unwrap();
value
.as_object_mut()
.expect("segment metadata is an object")
.remove("entity_layout");
let error = serde_json::from_value::<SegmentMeta>(value)
.expect_err("version 6 segment metadata must include entity_layout");
assert!(error.to_string().contains("entity_layout"));
}
}