timeseries_table_format/transaction_log/
segments.rs1use snafu::{Backtrace, prelude::*};
10
11use crate::storage::StorageError;
12
13pub use crate::metadata::segments::{
15 FileFormat, SegmentEntityLayout, SegmentMeta, SegmentMetaError,
16};
17
18#[derive(Debug, Snafu)]
20pub enum SegmentIoError {
21 #[snafu(display("Segment file missing or not a regular file: {path}"))]
23 MissingFile {
24 path: String,
26 backtrace: Backtrace,
28 },
29
30 #[snafu(display("I/O error while validating segment at {path}: {source}"))]
32 Storage {
33 path: String,
35 #[snafu(source, backtrace)]
37 source: StorageError,
38 },
39}
40
41#[derive(Debug, Snafu)]
43pub enum SegmentError {
44 #[snafu(transparent)]
46 Io {
47 source: SegmentIoError,
49 },
50
51 #[snafu(transparent)]
53 Meta {
54 source: SegmentMetaError,
56 },
57}
58
59#[allow(clippy::result_large_err)]
61pub type SegmentResult<T> = Result<T, SegmentError>;
62
63pub fn map_storage_error(err: StorageError) -> SegmentError {
69 let (is_missing, path) = match &err {
70 StorageError::NotFound { path, .. } => (true, path.clone()),
71 StorageError::AlreadyExists { path, .. }
72 | StorageError::OtherIo { path, .. }
73 | StorageError::CleanupFailed { path, .. } => (false, path.clone()),
74 };
75
76 if is_missing {
77 SegmentIoError::MissingFile {
78 path,
79 backtrace: Backtrace::capture(),
80 }
81 .into()
82 } else {
83 SegmentIoError::Storage { path, source: err }.into()
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use chrono::Utc;
91 use chrono::{DateTime, TimeZone};
92
93 fn utc_datetime(
94 year: i32,
95 month: u32,
96 day: u32,
97 hour: u32,
98 minute: u32,
99 second: u32,
100 ) -> DateTime<Utc> {
101 Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
102 .single()
103 .expect("valid UTC timestamp")
104 }
105
106 fn sample_segment_meta() -> SegmentMeta {
107 SegmentMeta {
108 path: "data/seg-001.parquet".to_string(),
109 format: FileFormat::Parquet,
110 entity_layout: SegmentEntityLayout::NotApplicable,
111 index_min: (utc_datetime(2025, 1, 1, 0, 0, 0)).into(),
112 index_max: (utc_datetime(2025, 1, 1, 1, 0, 0)).into(),
113 row_count: 123,
114 file_size: None,
115 coverage_path: None,
116 }
117 }
118
119 #[test]
120 fn segment_meta_json_roundtrip_with_and_without_coverage_path() {
121 let seg = sample_segment_meta();
123 let json = serde_json::to_string(&seg).unwrap();
124 let back: SegmentMeta = serde_json::from_str(&json).unwrap();
125 assert_eq!(back.coverage_path, None);
126 assert_eq!(back.file_size, None);
127
128 let mut seg2 = sample_segment_meta().with_coverage_path("_coverage/segments/a.roar");
130 seg2.file_size = Some(42);
131 let json2 = serde_json::to_string(&seg2).unwrap();
132 let back2: SegmentMeta = serde_json::from_str(&json2).unwrap();
133 assert_eq!(
134 back2.coverage_path.as_deref(),
135 Some("_coverage/segments/a.roar")
136 );
137 assert_eq!(back2.file_size, Some(42));
138 }
139
140 #[test]
141 fn segment_meta_json_requires_entity_layout() {
142 let mut value = serde_json::to_value(sample_segment_meta()).unwrap();
143 value
144 .as_object_mut()
145 .expect("segment metadata is an object")
146 .remove("entity_layout");
147
148 let error = serde_json::from_value::<SegmentMeta>(value)
149 .expect_err("version 6 segment metadata must include entity_layout");
150 assert!(error.to_string().contains("entity_layout"));
151 }
152}