Skip to main content

timeseries_table_format/transaction_log/
segments.rs

1//! Segment IO error types.
2//!
3//! The canonical segment metadata model lives in [`crate::metadata::segments`]
4//! and contains **no storage IO**.
5//!
6//! This module maps storage failures into the segment errors returned by
7//! format-specific readers.
8
9use snafu::{Backtrace, prelude::*};
10
11use crate::storage::StorageError;
12
13// Expose the pure segment types alongside their IO-layer errors.
14pub use crate::metadata::segments::{
15    FileFormat, SegmentEntityLayout, SegmentMeta, SegmentMetaError,
16};
17
18/// IO-layer errors when constructing/validating segments.
19#[derive(Debug, Snafu)]
20pub enum SegmentIoError {
21    /// The file is missing or not a regular file.
22    #[snafu(display("Segment file missing or not a regular file: {path}"))]
23    MissingFile {
24        /// The path to the missing or invalid file.
25        path: String,
26        /// Backtrace for debugging.
27        backtrace: Backtrace,
28    },
29
30    /// Generic I/O error while validating the segment.
31    #[snafu(display("I/O error while validating segment at {path}: {source}"))]
32    Storage {
33        /// The path to the file that caused the I/O error.
34        path: String,
35        /// Underlying storage error that caused this I/O failure.
36        #[snafu(source, backtrace)]
37        source: StorageError,
38    },
39}
40
41/// Segment error at the IO boundary: either a storage failure or a pure metadata failure.
42#[derive(Debug, Snafu)]
43pub enum SegmentError {
44    /// Storage / backend error while accessing a segment.
45    #[snafu(transparent)]
46    Io {
47        /// The underlying IO-layer error.
48        source: SegmentIoError,
49    },
50
51    /// Pure metadata/decoding/validation error.
52    #[snafu(transparent)]
53    Meta {
54        /// The underlying pure metadata error.
55        source: SegmentMetaError,
56    },
57}
58
59/// Convenience alias for results returned by IO-layer segment operations.
60#[allow(clippy::result_large_err)]
61pub type SegmentResult<T> = Result<T, SegmentError>;
62
63/// Convert a lower-level `StorageError` into the corresponding `SegmentError`.
64///
65/// - `StorageError::NotFound` is mapped to `SegmentIoError::MissingFile`.
66/// - All other storage errors are wrapped in `SegmentIoError::Storage`,
67///   preserving the original `StorageError` as the source for diagnostics.
68pub 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        // Without coverage_path
122        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        // With coverage_path
129        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}