Skip to main content

swh_graph/properties/
timestamps.rs

1// Copyright (C) 2023-2026  The Software Heritage developers
2// See the AUTHORS file at the top-level directory of this distribution
3// License: GNU General Public License version 3, or any later version
4// See top-level LICENSE file for more information
5
6use anyhow::{ensure, Context, Result};
7use mmap_rs::Mmap;
8use value_traits::slices::SliceByValue;
9
10use super::suffixes::*;
11use super::*;
12use crate::graph::NodeId;
13
14/// Trait implemented by both [`NoTimestamps`] and all implementors of [`Timestamps`],
15/// to allow loading timestamp properties only if needed.
16pub trait MaybeTimestamps {}
17impl<T: OptTimestamps> MaybeTimestamps for T {}
18
19/// Placeholder for when timestamp properties are not loaded
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct NoTimestamps;
22impl MaybeTimestamps for NoTimestamps {}
23
24#[diagnostic::on_unimplemented(
25    label = "does not have Timestamp properties loaded",
26    note = "Use `let graph = graph.load_properties(|props| props.load_timestamps()).unwrap()` to load them",
27    note = "Or replace `graph.init_properties()` with `graph.load_all_properties::<DynMphf>().unwrap()` to load all properties"
28)]
29/// Trait for backend storage of timestamp properties (either in-memory or memory-mapped)
30pub trait OptTimestamps: MaybeTimestamps + PropertiesBackend {
31    /// Returns `None` if out of bound, `Some(i64::MIN)` if the node has no author timestamp
32    fn author_timestamp(&self, node: NodeId) -> PropertiesResult<'_, Option<i64>, Self>;
33    /// Returns `None` if out of bound, `Some(i16::MIN)` if the node has no author timestamp offset
34    fn author_timestamp_offset(&self, node: NodeId) -> PropertiesResult<'_, Option<i16>, Self>;
35    /// Returns `None` if out of bound, `Some(i64::MIN)` if the node has no committer timestamp
36    fn committer_timestamp(&self, node: NodeId) -> PropertiesResult<'_, Option<i64>, Self>;
37    /// Returns `None` if out of bound, `Some(i16::MIN)` if the node has no committer timestamp offset
38    fn committer_timestamp_offset(&self, node: NodeId) -> PropertiesResult<'_, Option<i16>, Self>;
39}
40
41#[diagnostic::on_unimplemented(
42    label = "does not have Timestamp properties loaded",
43    note = "Use `let graph = graph.load_properties(|props| props.load_timestamps()).unwrap()` to load them",
44    note = "Or replace `graph.init_properties()` with `graph.load_all_properties::<DynMphf>().unwrap()` to load all properties"
45)]
46/// Trait for backend storage of timestamp properties (either in-memory or memory-mapped)
47pub trait Timestamps: OptTimestamps<DataFilesAvailability = GuaranteedDataFiles> {}
48impl<T: OptTimestamps<DataFilesAvailability = GuaranteedDataFiles>> Timestamps for T {}
49
50pub struct OptMappedTimestamps {
51    author_timestamp: Result<NumberMmap<BigEndian, i64, Mmap>, UnavailableProperty>,
52    author_timestamp_offset: Result<NumberMmap<BigEndian, i16, Mmap>, UnavailableProperty>,
53    committer_timestamp: Result<NumberMmap<BigEndian, i64, Mmap>, UnavailableProperty>,
54    committer_timestamp_offset: Result<NumberMmap<BigEndian, i16, Mmap>, UnavailableProperty>,
55}
56impl PropertiesBackend for OptMappedTimestamps {
57    type DataFilesAvailability = OptionalDataFiles;
58}
59impl OptTimestamps for OptMappedTimestamps {
60    #[inline(always)]
61    fn author_timestamp(&self, node: NodeId) -> PropertiesResult<'_, Option<i64>, Self> {
62        self.author_timestamp
63            .as_ref()
64            .map(|author_timestamps| author_timestamps.get_value(node))
65    }
66    #[inline(always)]
67    fn author_timestamp_offset(&self, node: NodeId) -> PropertiesResult<'_, Option<i16>, Self> {
68        self.author_timestamp_offset
69            .as_ref()
70            .map(|author_timestamp_offsets| author_timestamp_offsets.get_value(node))
71    }
72    #[inline(always)]
73    fn committer_timestamp(&self, node: NodeId) -> PropertiesResult<'_, Option<i64>, Self> {
74        self.committer_timestamp
75            .as_ref()
76            .map(|committer_timestamps| committer_timestamps.get_value(node))
77    }
78    #[inline(always)]
79    fn committer_timestamp_offset(&self, node: NodeId) -> PropertiesResult<'_, Option<i16>, Self> {
80        self.committer_timestamp_offset
81            .as_ref()
82            .map(|committer_timestamp_offsets| committer_timestamp_offsets.get_value(node))
83    }
84}
85
86pub struct MappedTimestamps {
87    author_timestamp: NumberMmap<BigEndian, i64, Mmap>,
88    author_timestamp_offset: NumberMmap<BigEndian, i16, Mmap>,
89    committer_timestamp: NumberMmap<BigEndian, i64, Mmap>,
90    committer_timestamp_offset: NumberMmap<BigEndian, i16, Mmap>,
91}
92impl PropertiesBackend for MappedTimestamps {
93    type DataFilesAvailability = GuaranteedDataFiles;
94}
95
96impl OptTimestamps for MappedTimestamps {
97    #[inline(always)]
98    fn author_timestamp(&self, node: NodeId) -> Option<i64> {
99        self.author_timestamp.get_value(node)
100    }
101    #[inline(always)]
102    fn author_timestamp_offset(&self, node: NodeId) -> Option<i16> {
103        self.author_timestamp_offset.get_value(node)
104    }
105    #[inline(always)]
106    fn committer_timestamp(&self, node: NodeId) -> Option<i64> {
107        self.committer_timestamp.get_value(node)
108    }
109    #[inline(always)]
110    fn committer_timestamp_offset(&self, node: NodeId) -> Option<i16> {
111        self.committer_timestamp_offset.get_value(node)
112    }
113}
114
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116pub struct VecTimestamps {
117    author_timestamp: Vec<i64>,
118    author_timestamp_offset: Vec<i16>,
119    committer_timestamp: Vec<i64>,
120    committer_timestamp_offset: Vec<i16>,
121}
122
123impl VecTimestamps {
124    /// Builds [`VecTimestamps`] from 4-tuples of `(author_timestamp, author_timestamp_offset,
125    /// committer_timestamp, committer_timestamp_offset)`
126    #[allow(clippy::type_complexity)]
127    pub fn new(
128        timestamps: Vec<(Option<i64>, Option<i16>, Option<i64>, Option<i16>)>,
129    ) -> Result<Self> {
130        let mut author_timestamp = Vec::with_capacity(timestamps.len());
131        let mut author_timestamp_offset = Vec::with_capacity(timestamps.len());
132        let mut committer_timestamp = Vec::with_capacity(timestamps.len());
133        let mut committer_timestamp_offset = Vec::with_capacity(timestamps.len());
134        for (a_ts, a_ts_o, c_ts, c_ts_o) in timestamps {
135            ensure!(
136                a_ts != Some(i64::MIN),
137                "author timestamp may not be {}",
138                i64::MIN
139            );
140            ensure!(
141                a_ts_o != Some(i16::MIN),
142                "author timestamp offset may not be {}",
143                i16::MIN
144            );
145            ensure!(
146                c_ts != Some(i64::MIN),
147                "committer timestamp may not be {}",
148                i64::MIN
149            );
150            ensure!(
151                c_ts_o != Some(i16::MIN),
152                "committer timestamp offset may not be {}",
153                i16::MIN
154            );
155            author_timestamp.push(a_ts.unwrap_or(i64::MIN));
156            author_timestamp_offset.push(a_ts_o.unwrap_or(i16::MIN));
157            committer_timestamp.push(c_ts.unwrap_or(i64::MIN));
158            committer_timestamp_offset.push(c_ts_o.unwrap_or(i16::MIN));
159        }
160        Ok(VecTimestamps {
161            author_timestamp,
162            author_timestamp_offset,
163            committer_timestamp,
164            committer_timestamp_offset,
165        })
166    }
167}
168
169impl PropertiesBackend for VecTimestamps {
170    type DataFilesAvailability = GuaranteedDataFiles;
171}
172impl OptTimestamps for VecTimestamps {
173    #[inline(always)]
174    fn author_timestamp(&self, node: NodeId) -> Option<i64> {
175        self.author_timestamp.get_value(node)
176    }
177    #[inline(always)]
178    fn author_timestamp_offset(&self, node: NodeId) -> Option<i16> {
179        self.author_timestamp_offset.get_value(node)
180    }
181    #[inline(always)]
182    fn committer_timestamp(&self, node: NodeId) -> Option<i64> {
183        self.committer_timestamp.get_value(node)
184    }
185    #[inline(always)]
186    fn committer_timestamp_offset(&self, node: NodeId) -> Option<i16> {
187        self.committer_timestamp_offset.get_value(node)
188    }
189}
190
191impl<
192        MAPS: MaybeMaps,
193        PERSONS: MaybePersons,
194        CONTENTS: MaybeContents,
195        STRINGS: MaybeStrings,
196        LABELNAMES: MaybeLabelNames,
197    > SwhGraphProperties<MAPS, NoTimestamps, PERSONS, CONTENTS, STRINGS, LABELNAMES>
198{
199    /// Consumes a [`SwhGraphProperties`] and returns a new one with these methods
200    /// available:
201    ///
202    /// * [`SwhGraphProperties::author_timestamp`]
203    /// * [`SwhGraphProperties::author_timestamp_offset`]
204    /// * [`SwhGraphProperties::committer_timestamp`]
205    /// * [`SwhGraphProperties::committer_timestamp_offset`]
206    pub fn load_timestamps(
207        self,
208    ) -> Result<SwhGraphProperties<MAPS, MappedTimestamps, PERSONS, CONTENTS, STRINGS, LABELNAMES>>
209    {
210        let OptMappedTimestamps {
211            author_timestamp,
212            author_timestamp_offset,
213            committer_timestamp,
214            committer_timestamp_offset,
215        } = self.get_timestamps()?;
216        let timestamps = MappedTimestamps {
217            author_timestamp: author_timestamp?,
218            author_timestamp_offset: author_timestamp_offset?,
219            committer_timestamp: committer_timestamp?,
220            committer_timestamp_offset: committer_timestamp_offset?,
221        };
222        self.with_timestamps(timestamps)
223    }
224
225    /// Equivalent to [`Self::load_timestamps`] that does not require all files to be present
226    pub fn opt_load_timestamps(
227        self,
228    ) -> Result<SwhGraphProperties<MAPS, OptMappedTimestamps, PERSONS, CONTENTS, STRINGS, LABELNAMES>>
229    {
230        let timestamps = self.get_timestamps()?;
231        self.with_timestamps(timestamps)
232    }
233
234    fn get_timestamps(&self) -> Result<OptMappedTimestamps> {
235        Ok(OptMappedTimestamps {
236            author_timestamp: load_if_exists(&self.path, AUTHOR_TIMESTAMP, |path| {
237                NumberMmap::new(path, self.num_nodes).context("Could not load author_timestamp")
238            })?,
239            author_timestamp_offset: load_if_exists(&self.path, AUTHOR_TIMESTAMP_OFFSET, |path| {
240                NumberMmap::new(path, self.num_nodes)
241                    .context("Could not load author_timestamp_offset")
242            })?,
243            committer_timestamp: load_if_exists(&self.path, COMMITTER_TIMESTAMP, |path| {
244                NumberMmap::new(path, self.num_nodes).context("Could not load committer_timestamp")
245            })?,
246            committer_timestamp_offset: load_if_exists(
247                &self.path,
248                COMMITTER_TIMESTAMP_OFFSET,
249                |path| {
250                    NumberMmap::new(path, self.num_nodes)
251                        .context("Could not load committer_timestamp_offset")
252                },
253            )?,
254        })
255    }
256
257    /// Alternative to [`load_timestamps`](Self::load_timestamps) that allows using arbitrary
258    /// timestamps implementations
259    pub fn with_timestamps<TIMESTAMPS: MaybeTimestamps>(
260        self,
261        timestamps: TIMESTAMPS,
262    ) -> Result<SwhGraphProperties<MAPS, TIMESTAMPS, PERSONS, CONTENTS, STRINGS, LABELNAMES>> {
263        Ok(SwhGraphProperties {
264            maps: self.maps,
265            timestamps,
266            persons: self.persons,
267            contents: self.contents,
268            strings: self.strings,
269            label_names: self.label_names,
270            path: self.path,
271            num_nodes: self.num_nodes,
272            label_names_are_in_base64_order: self.label_names_are_in_base64_order,
273        })
274    }
275}
276
277/// Functions to access timestamps of `revision` and `release` nodes
278///
279/// Only available after calling [`load_timestamps`](SwhGraphProperties::load_timestamps)
280/// or [`load_all_properties`](crate::graph::SwhBidirectionalGraph::load_all_properties)
281impl<
282        MAPS: MaybeMaps,
283        TIMESTAMPS: OptTimestamps,
284        PERSONS: MaybePersons,
285        CONTENTS: MaybeContents,
286        STRINGS: MaybeStrings,
287        LABELNAMES: MaybeLabelNames,
288    > SwhGraphProperties<MAPS, TIMESTAMPS, PERSONS, CONTENTS, STRINGS, LABELNAMES>
289{
290    /// Returns the number of seconds since Epoch that a release or revision was
291    /// authored at
292    ///
293    /// # Panics
294    ///
295    /// If the node id does not exist
296    #[inline]
297    pub fn author_timestamp(
298        &self,
299        node_id: NodeId,
300    ) -> PropertiesResult<'_, Option<i64>, TIMESTAMPS> {
301        TIMESTAMPS::map_if_available(self.try_author_timestamp(node_id), |author_timestamp| {
302            author_timestamp.unwrap_or_else(|e| panic!("Cannot get author timestamp: {e}"))
303        })
304    }
305
306    /// Returns the number of seconds since Epoch that a release or revision was
307    /// authored at
308    ///
309    /// Returns `Err` if the node id is unknown, and `Ok(None)` if the node has
310    /// no author timestamp
311    #[inline]
312    pub fn try_author_timestamp(
313        &self,
314        node_id: NodeId,
315    ) -> PropertiesResult<'_, Result<Option<i64>, OutOfBoundError>, TIMESTAMPS> {
316        TIMESTAMPS::map_if_available(
317            self.timestamps.author_timestamp(node_id),
318            |author_timestamp| match author_timestamp {
319                None => Err(OutOfBoundError {
320                    index: node_id,
321                    len: self.num_nodes,
322                }),
323                Some(i64::MIN) => Ok(None),
324                Some(ts) => Ok(Some(ts)),
325            },
326        )
327    }
328
329    /// Returns the UTC offset in minutes of a release or revision's authorship date
330    ///
331    /// # Panics
332    ///
333    /// If the node id does not exist
334    #[inline]
335    pub fn author_timestamp_offset(
336        &self,
337        node_id: NodeId,
338    ) -> PropertiesResult<'_, Option<i16>, TIMESTAMPS> {
339        TIMESTAMPS::map_if_available(
340            self.try_author_timestamp_offset(node_id),
341            |author_timestamp_offset| {
342                author_timestamp_offset
343                    .unwrap_or_else(|e| panic!("Cannot get author timestamp offset: {e}"))
344            },
345        )
346    }
347
348    /// Returns the UTC offset in minutes of a release or revision's authorship date
349    ///
350    /// Returns `Err` if the node id is unknown, and `Ok(None)` if the node has
351    /// no author timestamp
352    #[inline]
353    pub fn try_author_timestamp_offset(
354        &self,
355        node_id: NodeId,
356    ) -> PropertiesResult<'_, Result<Option<i16>, OutOfBoundError>, TIMESTAMPS> {
357        TIMESTAMPS::map_if_available(
358            self.timestamps.author_timestamp_offset(node_id),
359            |author_timestamp_offset| match author_timestamp_offset {
360                None => Err(OutOfBoundError {
361                    index: node_id,
362                    len: self.num_nodes,
363                }),
364                Some(i16::MIN) => Ok(None),
365                Some(offset) => Ok(Some(offset)),
366            },
367        )
368    }
369
370    /// Returns the number of seconds since Epoch that a revision was committed at
371    ///
372    /// # Panics
373    ///
374    /// If the node id does not exist
375    #[inline]
376    pub fn committer_timestamp(
377        &self,
378        node_id: NodeId,
379    ) -> PropertiesResult<'_, Option<i64>, TIMESTAMPS> {
380        TIMESTAMPS::map_if_available(
381            self.try_committer_timestamp(node_id),
382            |committer_timestamp| {
383                committer_timestamp
384                    .unwrap_or_else(|e| panic!("Cannot get committer timestamp: {e}"))
385            },
386        )
387    }
388
389    /// Returns the number of seconds since Epoch that a revision was committed at
390    ///
391    /// Returns `Err` if the node id is unknown, and `Ok(None)` if the node has
392    /// no committer timestamp
393    #[inline]
394    pub fn try_committer_timestamp(
395        &self,
396        node_id: NodeId,
397    ) -> PropertiesResult<'_, Result<Option<i64>, OutOfBoundError>, TIMESTAMPS> {
398        TIMESTAMPS::map_if_available(
399            self.timestamps.committer_timestamp(node_id),
400            |committer_timestamp| match committer_timestamp {
401                None => Err(OutOfBoundError {
402                    index: node_id,
403                    len: self.num_nodes,
404                }),
405                Some(i64::MIN) => Ok(None),
406                Some(ts) => Ok(Some(ts)),
407            },
408        )
409    }
410
411    /// Returns the UTC offset in minutes of a revision's committer date
412    ///
413    /// # Panics
414    ///
415    /// If the node id does not exist
416    #[inline]
417    pub fn committer_timestamp_offset(
418        &self,
419        node_id: NodeId,
420    ) -> PropertiesResult<'_, Option<i16>, TIMESTAMPS> {
421        TIMESTAMPS::map_if_available(
422            self.try_committer_timestamp_offset(node_id),
423            |committer_timestamp_offset| {
424                committer_timestamp_offset
425                    .unwrap_or_else(|e| panic!("Cannot get committer timestamp: {e}"))
426            },
427        )
428    }
429
430    /// Returns the UTC offset in minutes of a revision's committer date
431    ///
432    /// Returns `Err` if the node id is unknown, and `Ok(None)` if the node has
433    /// no committer timestamp
434    #[inline]
435    pub fn try_committer_timestamp_offset(
436        &self,
437        node_id: NodeId,
438    ) -> PropertiesResult<'_, Result<Option<i16>, OutOfBoundError>, TIMESTAMPS> {
439        TIMESTAMPS::map_if_available(
440            self.timestamps.committer_timestamp_offset(node_id),
441            |committer_timestamp_offset| match committer_timestamp_offset {
442                None => Err(OutOfBoundError {
443                    index: node_id,
444                    len: self.num_nodes,
445                }),
446                Some(i16::MIN) => Ok(None),
447                Some(offset) => Ok(Some(offset)),
448            },
449        )
450    }
451}