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}