oxirs_tsdb/query/
range.rs

1//! Range query implementation
2//!
3//! Provides efficient time-range queries over time-series data.
4
5use crate::error::TsdbResult;
6use crate::series::DataPoint;
7use crate::storage::TimeChunk;
8use chrono::{DateTime, Utc};
9
10/// Time range specification
11#[derive(Debug, Clone, Copy)]
12pub struct TimeRange {
13    /// Start of time range (inclusive)
14    pub start: DateTime<Utc>,
15    /// End of time range (exclusive)
16    pub end: DateTime<Utc>,
17}
18
19impl TimeRange {
20    /// Create a new time range
21    pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
22        Self { start, end }
23    }
24
25    /// Check if a timestamp falls within this range
26    pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
27        timestamp >= self.start && timestamp < self.end
28    }
29
30    /// Check if this range overlaps with a chunk's time range
31    pub fn overlaps_chunk(&self, chunk: &TimeChunk) -> bool {
32        // Ranges overlap if one doesn't end before the other starts
33        self.start < chunk.end_time && self.end > chunk.start_time
34    }
35
36    /// Duration of the time range
37    pub fn duration(&self) -> chrono::Duration {
38        self.end - self.start
39    }
40}
41
42/// Range query over time-series data
43#[derive(Debug)]
44pub struct RangeQuery {
45    /// Series to query
46    pub series_id: u64,
47    /// Time range
48    pub time_range: TimeRange,
49    /// Maximum number of results (None = unlimited)
50    pub limit: Option<usize>,
51    /// Order by timestamp (true = ascending, false = descending)
52    pub ascending: bool,
53}
54
55impl RangeQuery {
56    /// Create a new range query
57    pub fn new(series_id: u64, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
58        Self {
59            series_id,
60            time_range: TimeRange::new(start, end),
61            limit: None,
62            ascending: true,
63        }
64    }
65
66    /// Set result limit
67    pub fn with_limit(mut self, limit: usize) -> Self {
68        self.limit = Some(limit);
69        self
70    }
71
72    /// Set order (ascending or descending)
73    pub fn with_order(mut self, ascending: bool) -> Self {
74        self.ascending = ascending;
75        self
76    }
77
78    /// Execute query against a list of chunks
79    pub fn execute(&self, chunks: &[TimeChunk]) -> TsdbResult<Vec<DataPoint>> {
80        let mut results = Vec::new();
81
82        // Filter chunks that overlap with time range
83        let relevant_chunks: Vec<&TimeChunk> = chunks
84            .iter()
85            .filter(|c| c.series_id == self.series_id && self.time_range.overlaps_chunk(c))
86            .collect();
87
88        // Query each relevant chunk
89        for chunk in relevant_chunks {
90            let chunk_results = chunk.query_range(self.time_range.start, self.time_range.end)?;
91            results.extend(chunk_results);
92
93            // Early exit if limit reached
94            if let Some(limit) = self.limit {
95                if results.len() >= limit {
96                    results.truncate(limit);
97                    break;
98                }
99            }
100        }
101
102        // Sort by timestamp
103        if self.ascending {
104            results.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
105        } else {
106            results.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
107        }
108
109        // Apply limit after sorting
110        if let Some(limit) = self.limit {
111            results.truncate(limit);
112        }
113
114        Ok(results)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use chrono::Duration;
122
123    fn create_test_chunk(series_id: u64, start: DateTime<Utc>, count: usize) -> TimeChunk {
124        let mut points = Vec::new();
125        for i in 0..count {
126            points.push(DataPoint {
127                timestamp: start + Duration::seconds(i as i64),
128                value: i as f64,
129            });
130        }
131        TimeChunk::new(series_id, start, Duration::hours(2), points).unwrap()
132    }
133
134    #[test]
135    fn test_time_range_contains() {
136        let now = Utc::now();
137        let range = TimeRange::new(now, now + Duration::hours(1));
138
139        assert!(range.contains(now));
140        assert!(range.contains(now + Duration::minutes(30)));
141        assert!(!range.contains(now - Duration::minutes(1)));
142        assert!(!range.contains(now + Duration::hours(1))); // End is exclusive
143    }
144
145    #[test]
146    fn test_range_query_basic() {
147        let now = Utc::now();
148        let chunk = create_test_chunk(1, now, 100);
149
150        let query = RangeQuery::new(1, now + Duration::seconds(10), now + Duration::seconds(20));
151        let results = query.execute(&[chunk]).unwrap();
152
153        assert_eq!(results.len(), 10);
154        assert!(results[0].timestamp >= now + Duration::seconds(10));
155    }
156
157    #[test]
158    fn test_range_query_with_limit() {
159        let now = Utc::now();
160        let chunk = create_test_chunk(1, now, 100);
161
162        let query = RangeQuery::new(1, now, now + Duration::seconds(100)).with_limit(5);
163        let results = query.execute(&[chunk]).unwrap();
164
165        assert_eq!(results.len(), 5);
166    }
167
168    #[test]
169    fn test_range_query_descending() {
170        let now = Utc::now();
171        let chunk = create_test_chunk(1, now, 100);
172
173        let query = RangeQuery::new(1, now, now + Duration::seconds(100)).with_order(false);
174        let results = query.execute(&[chunk]).unwrap();
175
176        // Should be descending
177        for window in results.windows(2) {
178            assert!(window[0].timestamp >= window[1].timestamp);
179        }
180    }
181
182    #[test]
183    fn test_range_overlaps_chunk() {
184        let now = Utc::now();
185        let chunk = create_test_chunk(1, now, 100);
186
187        // Range fully contains chunk
188        let range1 = TimeRange::new(now - Duration::hours(1), now + Duration::hours(3));
189        assert!(range1.overlaps_chunk(&chunk));
190
191        // Range partially overlaps
192        let range2 = TimeRange::new(now + Duration::seconds(50), now + Duration::hours(3));
193        assert!(range2.overlaps_chunk(&chunk));
194
195        // Range doesn't overlap (before chunk)
196        let range3 = TimeRange::new(now - Duration::hours(2), now - Duration::hours(1));
197        assert!(!range3.overlaps_chunk(&chunk));
198    }
199}