Skip to main content

journal_engine/
query_time_range.rs

1//! Query time range with automatic alignment for histogram bucketing
2
3use crate::EngineError;
4use crate::histogram::calculate_bucket_duration;
5use journal_index::Seconds;
6
7/// A time range for querying journal entries with automatic alignment.
8///
9/// This type encapsulates:
10/// - The original requested time boundaries
11/// - The computed bucket duration based on the range
12/// - The aligned boundaries for consistent indexing and querying
13///
14/// All alignment logic is handled internally, ensuring consistency between
15/// histogram computation, file indexing, and log queries.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct QueryTimeRange {
18    /// Original requested start time (seconds)
19    requested_start: u32,
20    /// Original requested end time (seconds)
21    requested_end: u32,
22    /// Computed bucket duration in seconds
23    bucket_duration: u32,
24    /// Aligned start time (rounds down to bucket boundary)
25    aligned_start: u32,
26    /// Aligned end time (rounds up to bucket boundary)
27    aligned_end: u32,
28}
29
30impl QueryTimeRange {
31    /// Create a new query time range with automatic alignment.
32    ///
33    /// The bucket duration is computed based on the range duration, and
34    /// the boundaries are aligned to bucket boundaries:
35    /// - `aligned_start` rounds down to the nearest bucket boundary
36    /// - `aligned_end` rounds up to the nearest bucket boundary
37    ///
38    /// # Arguments
39    /// * `start` - Start time in seconds (inclusive)
40    /// * `end` - End time in seconds (exclusive)
41    ///
42    /// # Returns
43    /// * `Ok(QueryTimeRange)` if the range is valid
44    /// * `Err(EngineError::InvalidTimeRange)` if start >= end
45    ///
46    /// # Example
47    /// ```
48    /// use journal_engine::QueryTimeRange;
49    ///
50    /// let range = QueryTimeRange::new(100, 500).unwrap();
51    /// assert_eq!(range.requested_start(), 100);
52    /// assert_eq!(range.requested_end(), 500);
53    /// assert!(range.aligned_start() <= 100);
54    /// assert!(range.aligned_end() >= 500);
55    /// ```
56    pub fn new(start: u32, end: u32) -> Result<Self, EngineError> {
57        if start >= end {
58            return Err(EngineError::InvalidTimeRange { start, end });
59        }
60
61        let duration = end - start;
62        let bucket_duration = calculate_bucket_duration(duration);
63        let aligned_start = (start / bucket_duration) * bucket_duration;
64        let aligned_end = end.div_ceil(bucket_duration) * bucket_duration;
65
66        Ok(Self {
67            requested_start: start,
68            requested_end: end,
69            bucket_duration,
70            aligned_start,
71            aligned_end,
72        })
73    }
74
75    /// Get the original requested start time (seconds).
76    pub fn requested_start(&self) -> u32 {
77        self.requested_start
78    }
79
80    /// Get the original requested end time (seconds).
81    pub fn requested_end(&self) -> u32 {
82        self.requested_end
83    }
84
85    /// Get the computed bucket duration (seconds).
86    ///
87    /// This is used for file indexing to ensure all files are indexed
88    /// with the same bucket size.
89    pub fn bucket_duration(&self) -> u32 {
90        self.bucket_duration
91    }
92
93    /// Get the bucket duration as `Seconds`.
94    pub fn bucket_duration_seconds(&self) -> Seconds {
95        Seconds(self.bucket_duration)
96    }
97
98    /// Get the aligned start time (seconds).
99    ///
100    /// This is the start time rounded down to the nearest bucket boundary.
101    pub fn aligned_start(&self) -> u32 {
102        self.aligned_start
103    }
104
105    /// Get the aligned end time (seconds).
106    ///
107    /// This is the end time rounded up to the nearest bucket boundary.
108    pub fn aligned_end(&self) -> u32 {
109        self.aligned_end
110    }
111
112    /// Get the duration of the aligned range (seconds).
113    pub fn aligned_duration(&self) -> u32 {
114        self.aligned_end - self.aligned_start
115    }
116
117    /// Get the duration of the requested range (seconds).
118    pub fn requested_duration(&self) -> u32 {
119        self.requested_end - self.requested_start
120    }
121
122    /// Returns an iterator over the bucket time ranges.
123    ///
124    /// Each bucket is a `(start, end)` tuple in seconds, where:
125    /// - `start` is inclusive
126    /// - `end` is exclusive
127    /// - `end - start == bucket_duration`
128    ///
129    /// # Example
130    /// ```
131    /// use journal_engine::QueryTimeRange;
132    ///
133    /// let range = QueryTimeRange::new(0, 1000).unwrap();
134    /// for (start, end) in range.buckets() {
135    ///     println!("Bucket: [{}, {})", start, end);
136    /// }
137    /// ```
138    pub fn buckets(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
139        let bucket_duration = self.bucket_duration;
140        let num_buckets = (self.aligned_end - self.aligned_start) / bucket_duration;
141
142        (0..num_buckets).map(move |i| {
143            let start = self.aligned_start + (i * bucket_duration);
144            let end = start + bucket_duration;
145            (start, end)
146        })
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_invalid_range() {
156        assert!(QueryTimeRange::new(100, 100).is_err());
157        assert!(QueryTimeRange::new(100, 50).is_err());
158    }
159
160    #[test]
161    fn test_alignment() {
162        let range = QueryTimeRange::new(100, 500).unwrap();
163
164        // Aligned boundaries should encompass requested boundaries
165        assert!(range.aligned_start() <= range.requested_start());
166        assert!(range.aligned_end() >= range.requested_end());
167
168        // Aligned boundaries should be multiples of bucket duration
169        assert_eq!(range.aligned_start() % range.bucket_duration(), 0);
170        assert_eq!(range.aligned_end() % range.bucket_duration(), 0);
171    }
172
173    #[test]
174    fn test_accessors() {
175        let range = QueryTimeRange::new(100, 500).unwrap();
176
177        assert_eq!(range.requested_start(), 100);
178        assert_eq!(range.requested_end(), 500);
179        assert_eq!(range.requested_duration(), 400);
180        assert!(range.bucket_duration() > 0);
181        assert_eq!(
182            range.aligned_duration(),
183            range.aligned_end() - range.aligned_start()
184        );
185    }
186
187    #[test]
188    fn test_buckets_iterator() {
189        let range = QueryTimeRange::new(0, 1000).unwrap();
190        let buckets: Vec<(u32, u32)> = range.buckets().collect();
191
192        // Check that we have at least one bucket
193        assert!(!buckets.is_empty());
194
195        // Check that buckets are contiguous and cover the aligned range
196        let mut expected_start = range.aligned_start();
197        for (start, end) in &buckets {
198            assert_eq!(*start, expected_start);
199            assert_eq!(end - start, range.bucket_duration());
200            expected_start = *end;
201        }
202        assert_eq!(expected_start, range.aligned_end());
203
204        // Check that all buckets have the same duration
205        for (start, end) in &buckets {
206            assert_eq!(end - start, range.bucket_duration());
207        }
208    }
209}