timeseries_table_format/transaction_log/
segments.rs1use snafu::prelude::*;
10
11use crate::storage::StorageError;
12
13pub use crate::metadata::segments::{
15 FileFormat, SegmentEntityLayout, SegmentMeta, SegmentMetaError,
16};
17
18#[derive(Debug, Snafu)]
20#[non_exhaustive]
21pub enum SegmentError {
22 #[snafu(display("Segment file not found: {path}"))]
24 MissingFile {
25 path: String,
27 #[snafu(source, backtrace)]
29 source: StorageError,
30 },
31
32 #[snafu(display("Storage error while accessing segment at {path}: {source}"))]
34 Storage {
35 path: String,
37 #[snafu(source, backtrace)]
39 source: StorageError,
40 },
41
42 #[snafu(context(false), display("{source}"))]
44 Metadata {
45 #[snafu(source, backtrace)]
47 source: SegmentMetaError,
48 },
49}
50
51#[allow(clippy::result_large_err)]
53pub type SegmentResult<T> = Result<T, SegmentError>;
54
55impl From<StorageError> for SegmentError {
56 fn from(source: StorageError) -> Self {
57 let is_missing = matches!(&source, StorageError::NotFound { .. });
58 let path = match &source {
59 StorageError::InvalidRelativePath { path, .. }
60 | StorageError::NotFound { path, .. }
61 | StorageError::AlreadyExists { path, .. }
62 | StorageError::OtherIo { path, .. }
63 | StorageError::CleanupFailed { path, .. } => path.clone(),
64 };
65
66 if is_missing {
67 Self::MissingFile { path, source }
68 } else {
69 Self::Storage { path, source }
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use std::{error::Error as _, io};
77
78 use super::*;
79 use chrono::Utc;
80 use chrono::{DateTime, TimeZone};
81 use snafu::{Backtrace, ErrorCompat};
82
83 use crate::storage::StorageBackendError;
84
85 fn utc_datetime(
86 year: i32,
87 month: u32,
88 day: u32,
89 hour: u32,
90 minute: u32,
91 second: u32,
92 ) -> DateTime<Utc> {
93 Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
94 .single()
95 .expect("valid UTC timestamp")
96 }
97
98 fn sample_segment_meta() -> SegmentMeta {
99 SegmentMeta {
100 path: "data/seg-001.parquet".to_string(),
101 format: FileFormat::Parquet,
102 entity_layout: SegmentEntityLayout::NotApplicable,
103 index_min: (utc_datetime(2025, 1, 1, 0, 0, 0)).into(),
104 index_max: (utc_datetime(2025, 1, 1, 1, 0, 0)).into(),
105 row_count: 123,
106 file_size: None,
107 coverage_path: None,
108 }
109 }
110
111 #[test]
112 fn segment_meta_json_roundtrip_with_and_without_coverage_path() {
113 let seg = sample_segment_meta();
115 let json = serde_json::to_string(&seg).unwrap();
116 let back: SegmentMeta = serde_json::from_str(&json).unwrap();
117 assert_eq!(back.coverage_path, None);
118 assert_eq!(back.file_size, None);
119
120 let mut seg2 = sample_segment_meta().with_coverage_path("_coverage/segments/a.roar");
122 seg2.file_size = Some(42);
123 let json2 = serde_json::to_string(&seg2).unwrap();
124 let back2: SegmentMeta = serde_json::from_str(&json2).unwrap();
125 assert_eq!(
126 back2.coverage_path.as_deref(),
127 Some("_coverage/segments/a.roar")
128 );
129 assert_eq!(back2.file_size, Some(42));
130 }
131
132 #[test]
133 fn segment_meta_json_requires_entity_layout() {
134 let mut value = serde_json::to_value(sample_segment_meta()).unwrap();
135 value
136 .as_object_mut()
137 .expect("segment metadata is an object")
138 .remove("entity_layout");
139
140 let error = serde_json::from_value::<SegmentMeta>(value)
141 .expect_err("segment metadata must include entity_layout");
142 assert!(error.to_string().contains("entity_layout"));
143 }
144
145 #[test]
146 fn missing_segment_preserves_storage_source_and_backtrace() {
147 let storage = StorageError::NotFound {
148 path: "data/missing.parquet".to_string(),
149 source: io::Error::new(io::ErrorKind::NotFound, "missing").into(),
150 backtrace: Backtrace::capture(),
151 };
152 let error = SegmentError::from(storage);
153
154 let segment_backtrace = ErrorCompat::backtrace(&error).expect("segment backtrace");
155 let storage = error
156 .source()
157 .and_then(|source| source.downcast_ref::<StorageError>())
158 .expect("storage source");
159 let storage_backtrace = ErrorCompat::backtrace(storage).expect("storage backtrace");
160 let backend = storage
161 .source()
162 .and_then(|source| source.downcast_ref::<StorageBackendError>())
163 .expect("storage backend source");
164 let io_source = backend
165 .source()
166 .and_then(|source| source.downcast_ref::<io::Error>())
167 .expect("io source");
168
169 assert!(matches!(&error, SegmentError::MissingFile { .. }));
170 assert_eq!(io_source.kind(), io::ErrorKind::NotFound);
171 assert!(std::ptr::eq(segment_backtrace, storage_backtrace));
172 }
173
174 #[test]
175 fn non_missing_storage_failure_remains_a_storage_error() {
176 let storage = StorageError::OtherIo {
177 path: "data/unreadable.parquet".to_string(),
178 source: io::Error::from(io::ErrorKind::PermissionDenied).into(),
179 backtrace: Backtrace::capture(),
180 };
181
182 assert!(matches!(
183 SegmentError::from(storage),
184 SegmentError::Storage {
185 source: StorageError::OtherIo { .. },
186 ..
187 }
188 ));
189 }
190}