re_chunk/range.rs
1use re_log_types::{AbsoluteTimeRange, TimeInt, TimelineName};
2use re_types_core::ComponentDescriptor;
3
4use crate::Chunk;
5
6// --- Range ---
7
8/// A query over a time range, for a given timeline.
9///
10/// Get all the data within this time interval, plus the latest one before the start of the
11/// interval.
12///
13/// Motivation: all data is considered alive until the next logging to the same component path.
14#[derive(Clone, PartialEq, Eq, Hash)]
15pub struct RangeQuery {
16    pub timeline: TimelineName,
17    pub range: AbsoluteTimeRange,
18    pub options: RangeQueryOptions,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct RangeQueryOptions {
23    /// Should the results contain all extra timeline information available in the [`Chunk`]?
24    ///
25    /// While this information can be useful in some cases, it comes at a performance cost.
26    pub keep_extra_timelines: bool,
27
28    /// Should the results contain all extra component information available in the [`Chunk`]?
29    ///
30    /// While this information can be useful in some cases, it comes at a performance cost.
31    pub keep_extra_components: bool,
32
33    /// If true, the results will include one extra tick on each side of the range.
34    ///
35    /// Note: this is different from simply subtracting/adding one to the time range of the query,
36    /// as this will work even with non-contiguous time values, and even if these non-contiguous
37    /// jumps happen across multiple chunks.
38    ///
39    /// Consider for example this data:
40    /// ```text
41    /// ┌──────────────────────────────────┬───────────────┬──────────────────────┐
42    /// │ RowId                            ┆ frame_nr      ┆ Scalar               │
43    /// ╞══════════════════════════════════╪═══════════════╪══════════════════════╡
44    /// │ 17E9C11C655B21A9006568024DA10857 ┆ 0             ┆ [2]                  │
45    /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
46    /// │ 17E9C11C6560E8A6006568024DA10859 ┆ 2             ┆ [2.04]               │
47    /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
48    /// │ 17E9C11C656504F0006568024DA1085B ┆ 4             ┆ [2.08]               │
49    /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
50    /// │ 17E9C11C65693204006568024DA1085D ┆ 6             ┆ [2.12]               │
51    /// └──────────────────────────────────┴───────────────┴──────────────────────┘
52    /// ```
53    ///
54    /// * A `RangeQuery(#2, #4)` would yield frames #2 and #4.
55    /// * A `RangeQuery(#1, #5)` would still only yield frames #2 and #4.
56    /// * A `RangeQuery(#2, #4, include_extended_bounds=true)`, on the other hand, would yield all of
57    ///   frames #0, #2, #4 and #6.
58    pub include_extended_bounds: bool,
59}
60
61impl RangeQueryOptions {
62    pub const DEFAULT: Self = Self {
63        keep_extra_timelines: false,
64        keep_extra_components: false,
65        include_extended_bounds: false,
66    };
67}
68
69impl Default for RangeQueryOptions {
70    #[inline]
71    fn default() -> Self {
72        Self::DEFAULT
73    }
74}
75
76impl std::fmt::Debug for RangeQuery {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.write_fmt(format_args!(
79            "<ranging {:?}..={:?} on {:?} ([{}]keep_timelines [{}]keep_components [{}]extended_bounds)>",
80            self.range.min(),
81            self.range.max(),
82            self.timeline,
83            if self.options.keep_extra_timelines {
84                "✓"
85            } else {
86                " "
87            },
88            if self.options.keep_extra_components {
89                "✓"
90            } else {
91                " "
92            },
93            if self.options.include_extended_bounds {
94                "✓"
95            } else {
96                " "
97            },
98        ))
99    }
100}
101
102impl RangeQuery {
103    /// The returned query is guaranteed to never include [`TimeInt::STATIC`].
104    #[inline]
105    pub const fn new(timeline: TimelineName, range: AbsoluteTimeRange) -> Self {
106        Self {
107            timeline,
108            range,
109            options: RangeQueryOptions::DEFAULT,
110        }
111    }
112
113    /// The returned query is guaranteed to never include [`TimeInt::STATIC`].
114    ///
115    /// Keeps all extra timelines and components around.
116    #[inline]
117    pub const fn with_extras(timeline: TimelineName, range: AbsoluteTimeRange) -> Self {
118        Self {
119            timeline,
120            range,
121            options: RangeQueryOptions {
122                keep_extra_timelines: true,
123                keep_extra_components: true,
124                include_extended_bounds: false,
125            },
126        }
127    }
128
129    #[inline]
130    pub const fn everything(timeline: TimelineName) -> Self {
131        Self {
132            timeline,
133            range: AbsoluteTimeRange::EVERYTHING,
134            options: RangeQueryOptions::DEFAULT,
135        }
136    }
137
138    /// See [`RangeQueryOptions::keep_extra_timelines`] for more information.
139    #[inline]
140    pub fn keep_extra_timelines(mut self, toggle: bool) -> Self {
141        self.options.keep_extra_timelines = toggle;
142        self
143    }
144
145    /// See [`RangeQueryOptions::keep_extra_components`] for more information.
146    #[inline]
147    pub fn keep_extra_components(mut self, toggle: bool) -> Self {
148        self.options.keep_extra_components = toggle;
149        self
150    }
151
152    /// See [`RangeQueryOptions::include_extended_bounds`] for more information.
153    #[inline]
154    pub fn include_extended_bounds(mut self, toggle: bool) -> Self {
155        self.options.include_extended_bounds = toggle;
156        self
157    }
158
159    #[inline]
160    pub fn timeline(&self) -> &TimelineName {
161        &self.timeline
162    }
163
164    #[inline]
165    pub fn range(&self) -> AbsoluteTimeRange {
166        self.range
167    }
168
169    #[inline]
170    pub fn options(&self) -> RangeQueryOptions {
171        self.options.clone()
172    }
173}
174
175// ---
176
177impl Chunk {
178    /// Runs a [`RangeQuery`] filter on a [`Chunk`].
179    ///
180    /// This behaves as a row-based filter: the result is a new [`Chunk`] that is vertically
181    /// sliced, sorted and filtered in order to only contain the row(s) relevant for the
182    /// specified `query`.
183    ///
184    /// The resulting [`Chunk`] is guaranteed to contain all the same columns has the queried
185    /// chunk: there is no horizontal slicing going on.
186    ///
187    /// An empty [`Chunk`] (i.e. 0 rows, but N columns) is returned if the `query` yields nothing.
188    ///
189    /// Because the resulting chunk doesn't discard any column information, you can find extra relevant
190    /// information by inspecting the data, for examples timestamps on other timelines.
191    /// See [`Self::timeline_sliced`] and [`Self::component_sliced`] if you do want to filter this
192    /// extra data.
193    //
194    // TODO(apache/arrow-rs#5375): Since we don't have access to arrow's ListView yet, we must actually clone the
195    // data if the chunk requires sorting.
196    pub fn range(&self, query: &RangeQuery, component_descr: &ComponentDescriptor) -> Self {
197        if self.is_empty() {
198            return self.clone();
199        }
200
201        re_tracing::profile_function!(format!("{query:?}"));
202
203        let RangeQueryOptions {
204            keep_extra_timelines,
205            keep_extra_components,
206            include_extended_bounds,
207        } = query.options();
208
209        // Pre-slice the data if the caller allowed us: this will make further slicing
210        // (e.g. the range query itself) much cheaper to compute.
211        use std::borrow::Cow;
212        let chunk = if !keep_extra_timelines {
213            Cow::Owned(self.timeline_sliced(*query.timeline()))
214        } else {
215            Cow::Borrowed(self)
216        };
217        let chunk = if !keep_extra_components {
218            Cow::Owned(chunk.component_sliced(component_descr))
219        } else {
220            chunk
221        };
222
223        if chunk.is_static() {
224            // NOTE: A given component for a given entity can only have one static entry associated
225            // with it, and this entry overrides everything else, which means it is functionally
226            // equivalent to just running a latest-at query.
227            chunk.latest_at(
228                &crate::LatestAtQuery::new(*query.timeline(), TimeInt::MAX),
229                component_descr,
230            )
231        } else {
232            let Some(is_sorted_by_time) = chunk
233                .timelines
234                .get(query.timeline())
235                .map(|time_column| time_column.is_sorted())
236            else {
237                return chunk.emptied();
238            };
239
240            let chunk = chunk.densified(component_descr);
241
242            let chunk = if is_sorted_by_time {
243                // Temporal, row-sorted, time-sorted chunk
244                chunk
245            } else {
246                // Temporal, unsorted chunk
247                chunk.sorted_by_timeline_if_unsorted(query.timeline())
248            };
249
250            let Some(times) = chunk
251                .timelines
252                .get(query.timeline())
253                .map(|time_column| time_column.times_raw())
254            else {
255                return chunk.emptied();
256            };
257
258            let mut start_index =
259                times.partition_point(|&time| time < query.range().min().as_i64());
260            let mut end_index = times.partition_point(|&time| time <= query.range().max().as_i64());
261
262            // See `RangeQueryOptions::include_extended_bounds` for more information.
263            if include_extended_bounds {
264                start_index = start_index.saturating_sub(1);
265                end_index = usize::min(self.num_rows(), end_index.saturating_add(1));
266            }
267
268            chunk.row_sliced(start_index, end_index.saturating_sub(start_index))
269        }
270    }
271}