re_types_core/
id.rs

1use arrow::array::Array as _;
2
3use crate::Loggable as _;
4
5/// A unique ID for a `Chunk`.
6///
7/// `Chunk`s are the atomic unit of ingestion, transport, storage, events and GC in Rerun.
8///
9/// Internally, a `Chunk` is made up of rows, which are themselves uniquely identified by
10/// their [`RowId`].
11///
12/// There is no relationship whatsoever between a [`ChunkId`] and the [`RowId`]s within that chunk.
13///
14/// ### String format
15/// Example: `chunk_182342300C5F8C327a7b4a6e5a379ac4`.
16/// The "chunk_" prefix is optional when parsing.
17/// See [`re_tuid`] docs for explanations of TUID namespaces.
18///
19/// ### Uniqueness
20///
21/// [`ChunkId`] are assumed unique within a single Recording.
22///
23/// The chunk store will treat two chunks with the same [`ChunkId`] as the same, and only keep one
24/// of them (which one is kept is an arbitrary and unstable implementation detail).
25///
26/// This makes it easy to build and maintain secondary indices around [`RowId`]s with few to no
27/// extraneous state tracking.
28///
29/// ### Garbage collection
30///
31/// Garbage collection is handled at the chunk level by first ordering the chunks based on the minimum
32/// [`RowId`] present in each of them.
33/// Garbage collection therefore happens (roughly) in the logger's wall-clock order.
34///
35/// This has very important implications when inserting data far into the past or into the future:
36/// think carefully about your `RowId`s in these cases.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
39pub struct ChunkId(pub(crate) re_tuid::Tuid);
40
41impl std::fmt::Display for ChunkId {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "chunk_{}", self.0)
44    }
45}
46
47#[derive(thiserror::Error, Debug)]
48#[error("Invalid ChunkId: {0}")]
49pub struct InvalidChunkIdError(String);
50
51impl std::str::FromStr for ChunkId {
52    type Err = InvalidChunkIdError;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        let tuid_str = if let Some((namespace, tuid_str)) = s.split_once('_') {
56            if namespace == "chunk" {
57                tuid_str
58            } else {
59                return Err(InvalidChunkIdError(format!(
60                    "Expected chunk_ prefix, got {s:?}"
61                )));
62            }
63        } else {
64            s
65        };
66
67        re_tuid::Tuid::from_str(tuid_str)
68            .map(Self)
69            .map_err(|err| InvalidChunkIdError(format!("Invalid TUID: {err}")))
70    }
71}
72
73impl ChunkId {
74    pub const ZERO: Self = Self(re_tuid::Tuid::ZERO);
75    pub const MAX: Self = Self(re_tuid::Tuid::MAX);
76
77    /// Create a new unique [`ChunkId`] based on the current time.
78    #[allow(clippy::new_without_default)]
79    #[inline]
80    pub fn new() -> Self {
81        Self(re_tuid::Tuid::new())
82    }
83
84    #[inline]
85    pub fn from_tuid(tuid: re_tuid::Tuid) -> Self {
86        Self(tuid)
87    }
88
89    #[inline]
90    pub fn as_tuid(&self) -> re_tuid::Tuid {
91        self.0
92    }
93
94    /// Returns the next logical [`ChunkId`].
95    ///
96    /// Beware: wrong usage can easily lead to conflicts.
97    /// Prefer [`ChunkId::new`] when unsure.
98    #[must_use]
99    #[inline]
100    pub fn next(&self) -> Self {
101        Self(self.0.next())
102    }
103
104    /// Returns the `n`-next logical [`ChunkId`].
105    ///
106    /// This is equivalent to calling [`ChunkId::next`] `n` times.
107    /// Wraps the monotonically increasing back to zero on overflow.
108    ///
109    /// Beware: wrong usage can easily lead to conflicts.
110    /// Prefer [`ChunkId::new`] when unsure.
111    #[must_use]
112    #[inline]
113    pub fn incremented_by(&self, n: u64) -> Self {
114        Self(self.0.incremented_by(n))
115    }
116
117    #[inline]
118    pub fn from_u128(id: u128) -> Self {
119        Self(re_tuid::Tuid::from_u128(id))
120    }
121}
122
123impl re_byte_size::SizeBytes for ChunkId {
124    #[inline]
125    fn heap_size_bytes(&self) -> u64 {
126        0
127    }
128
129    #[inline]
130    fn is_pod() -> bool {
131        true
132    }
133}
134
135impl std::ops::Deref for ChunkId {
136    type Target = re_tuid::Tuid;
137
138    #[inline]
139    fn deref(&self) -> &Self::Target {
140        &self.0
141    }
142}
143
144impl std::ops::DerefMut for ChunkId {
145    #[inline]
146    fn deref_mut(&mut self) -> &mut Self::Target {
147        &mut self.0
148    }
149}
150
151crate::delegate_arrow_tuid!(ChunkId as "rerun.controls.ChunkId"); // Used in the dataplatform
152
153// ---
154
155/// A unique ID for a row's worth of data within a chunk.
156///
157/// There is no relationship whatsoever between a [`ChunkId`] and the [`RowId`]s within that chunk.
158///
159/// ### String format
160/// Example: `row_182342300C5F8C327a7b4a6e5a379ac4`.
161/// The "row_" prefix is optional when parsing.
162/// See [`re_tuid`] docs for explanations of TUID namespaces.
163///
164/// ### Uniqueness
165///
166/// Duplicated [`RowId`]s within a single recording is considered undefined behavior.
167///
168/// While it is benign in most cases, care has to be taken when manually crafting [`RowId`]s.
169/// Ideally: don't do so and stick to [`RowId::new`] instead to avoid bad surprises.
170///
171/// This makes it easy to build and maintain secondary indices around [`RowId`]s with few to no
172/// extraneous state tracking.
173///
174/// ### Query
175///
176/// Queries (both latest-at & range semantics) will defer to `RowId` order as a tie-breaker when
177/// looking at several rows worth of data that rest at the exact same timestamp.
178///
179/// In pseudo-code:
180/// ```text
181/// rr.set_time("frame", sequence=10)
182///
183/// rr.log("my_entity", point1, row_id=#1)
184/// rr.log("my_entity", point2, row_id=#0)
185///
186/// rr.query("my_entity", at=("frame", 10))  # returns `point1`
187/// ```
188///
189/// Think carefully about your `RowId`s when logging a lot of data at the same timestamp.
190///
191/// ### Garbage collection
192///
193/// Garbage collection is handled at the chunk level by first ordering the chunks based on the minimum
194/// [`RowId`] present in each of them.
195/// Garbage collection therefore happens (roughly) in the logger's wall-clock order.
196///
197/// This has very important implications when inserting data far into the past or into the future:
198/// think carefully about your `RowId`s in these cases.
199#[repr(C, align(1))]
200#[derive(
201    Debug,
202    Clone,
203    Copy,
204    PartialEq,
205    Eq,
206    PartialOrd,
207    Ord,
208    Hash,
209    bytemuck::AnyBitPattern,
210    bytemuck::NoUninit,
211)]
212#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
213pub struct RowId(pub(crate) re_tuid::Tuid);
214
215impl std::fmt::Display for RowId {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        write!(f, "row_{}", self.0)
218    }
219}
220
221#[derive(thiserror::Error, Debug)]
222#[error("Invalid RowId: {0}")]
223pub struct InvalidRowIdError(String);
224
225impl std::str::FromStr for RowId {
226    type Err = InvalidRowIdError;
227
228    fn from_str(s: &str) -> Result<Self, Self::Err> {
229        let tuid_str = if let Some((namespace, tuid_str)) = s.split_once('_') {
230            if namespace == "row" {
231                tuid_str
232            } else {
233                return Err(InvalidRowIdError(format!(
234                    "Expected row_ prefix, got {s:?}"
235                )));
236            }
237        } else {
238            s
239        };
240
241        re_tuid::Tuid::from_str(tuid_str)
242            .map(Self)
243            .map_err(|err| InvalidRowIdError(format!("Invalid TUID: {err}")))
244    }
245}
246
247impl RowId {
248    pub const ZERO: Self = Self(re_tuid::Tuid::ZERO);
249    pub const MAX: Self = Self(re_tuid::Tuid::MAX);
250
251    /// Create a new unique [`RowId`] based on the current time.
252    #[allow(clippy::new_without_default)]
253    #[inline]
254    pub fn new() -> Self {
255        Self(re_tuid::Tuid::new())
256    }
257
258    #[inline]
259    pub fn from_tuid(tuid: re_tuid::Tuid) -> Self {
260        Self(tuid)
261    }
262
263    #[inline]
264    pub fn as_tuid(&self) -> re_tuid::Tuid {
265        self.0
266    }
267
268    /// Returns the next logical [`RowId`].
269    ///
270    /// Beware: wrong usage can easily lead to conflicts.
271    /// Prefer [`RowId::new`] when unsure.
272    #[must_use]
273    #[inline]
274    pub fn next(&self) -> Self {
275        Self(self.0.next())
276    }
277
278    /// Returns the `n`-next logical [`RowId`].
279    ///
280    /// This is equivalent to calling [`RowId::next`] `n` times.
281    /// Wraps the monotonically increasing back to zero on overflow.
282    ///
283    /// Beware: wrong usage can easily lead to conflicts.
284    /// Prefer [`RowId::new`] when unsure.
285    #[must_use]
286    #[inline]
287    pub fn incremented_by(&self, n: u64) -> Self {
288        Self(self.0.incremented_by(n))
289    }
290
291    #[inline]
292    pub fn from_u128(id: u128) -> Self {
293        Self(re_tuid::Tuid::from_u128(id))
294    }
295
296    pub fn arrow_from_slice(slice: &[Self]) -> arrow::array::FixedSizeBinaryArray {
297        crate::tuids_to_arrow(bytemuck::cast_slice(slice))
298    }
299
300    /// Panics if the array is of the wrong width
301    pub fn slice_from_arrow(array: &arrow::array::FixedSizeBinaryArray) -> &[Self] {
302        debug_assert_eq!(array.data_type(), &Self::arrow_datatype());
303        bytemuck::cast_slice(array.value_data())
304    }
305}
306
307impl re_byte_size::SizeBytes for RowId {
308    #[inline]
309    fn heap_size_bytes(&self) -> u64 {
310        0
311    }
312
313    #[inline]
314    fn is_pod() -> bool {
315        true
316    }
317}
318
319impl std::ops::Deref for RowId {
320    type Target = re_tuid::Tuid;
321
322    #[inline]
323    fn deref(&self) -> &Self::Target {
324        &self.0
325    }
326}
327
328impl std::ops::DerefMut for RowId {
329    #[inline]
330    fn deref_mut(&mut self) -> &mut Self::Target {
331        &mut self.0
332    }
333}
334
335crate::delegate_arrow_tuid!(RowId as "rerun.controls.RowId");
336
337#[test]
338fn test_row_id_parse() {
339    let tuid: re_tuid::Tuid = "182342300C5F8C327a7b4a6e5a379ac4".parse().unwrap();
340
341    assert_eq!(
342        RowId(tuid).to_string(),
343        "row_182342300C5F8C327a7b4a6e5a379ac4"
344    );
345
346    assert_eq!(
347        "182342300C5F8C327a7b4a6e5a379ac4"
348            .parse::<RowId>()
349            .unwrap()
350            .0,
351        tuid
352    );
353    assert_eq!(
354        "row_182342300C5F8C327a7b4a6e5a379ac4"
355            .parse::<RowId>()
356            .unwrap()
357            .0,
358        tuid
359    );
360    assert!("chunk_182342300C5F8C327a7b4a6e5a379ac4"
361        .parse::<RowId>()
362        .is_err());
363}