Skip to main content

swh_graph/properties/
persons.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;
8
9use super::suffixes::*;
10use super::*;
11use crate::graph::NodeId;
12
13/// Trait implemented by both [`NoPersons`] and all implementors of [`Persons`],
14/// to allow loading person ids only if needed.
15pub trait MaybePersons {}
16impl<P: OptPersons> MaybePersons for P {}
17
18/// Placeholder for when person ids are not loaded
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct NoPersons;
21impl MaybePersons for NoPersons {}
22
23#[diagnostic::on_unimplemented(
24    label = "does not have Person properties loaded",
25    note = "Use `let graph = graph.load_properties(|props| props.load_persons()).unwrap()` to load them",
26    note = "Or replace `graph.init_properties()` with `graph.load_all_properties::<DynMphf>().unwrap()` to load all properties"
27)]
28/// Trait for backend storage of person properties (either in-memory or memory-mapped)
29pub trait OptPersons: MaybePersons + PropertiesBackend {
30    /// Returns `None` if out of bounds, `Some(u32::MAX)` if the node has no author
31    fn author_id(&self, node: NodeId) -> PropertiesResult<'_, Option<u32>, Self>;
32    /// Returns `None` if out of bounds, `Some(u32::MAX)` if the node has no committer
33    fn committer_id(&self, node: NodeId) -> PropertiesResult<'_, Option<u32>, Self>;
34    /// Returns the number of persons having authored/committed objects in the graph
35    fn num_persons(&self) -> PropertiesResult<'_, usize, Self>;
36}
37
38#[diagnostic::on_unimplemented(
39    label = "does not have Person properties loaded",
40    note = "Use `let graph = graph.load_properties(|props| props.load_persons()).unwrap()` to load them",
41    note = "Or replace `graph.init_properties()` with `graph.load_all_properties::<DynMphf>().unwrap()` to load all properties"
42)]
43/// Trait for backend storage of person properties (either in-memory or memory-mapped)
44pub trait Persons: OptPersons<DataFilesAvailability = GuaranteedDataFiles> {}
45impl<P: OptPersons<DataFilesAvailability = GuaranteedDataFiles>> Persons for P {}
46
47pub struct OptMappedPersons {
48    author_id: Result<NumberMmap<BigEndian, u32, Mmap>, UnavailableProperty>,
49    committer_id: Result<NumberMmap<BigEndian, u32, Mmap>, UnavailableProperty>,
50    num_persons: Result<usize, UnavailableProperty>,
51}
52impl PropertiesBackend for OptMappedPersons {
53    type DataFilesAvailability = OptionalDataFiles;
54}
55impl OptPersons for OptMappedPersons {
56    #[inline(always)]
57    fn author_id(&self, node: NodeId) -> PropertiesResult<'_, Option<u32>, Self> {
58        self.author_id
59            .as_ref()
60            .map(|author_ids| author_ids.get_value(node))
61    }
62    #[inline(always)]
63    fn committer_id(&self, node: NodeId) -> PropertiesResult<'_, Option<u32>, Self> {
64        self.committer_id
65            .as_ref()
66            .map(|committer_ids| committer_ids.get_value(node))
67    }
68    #[inline(always)]
69    fn num_persons(&self) -> PropertiesResult<'_, usize, Self> {
70        self.num_persons.as_ref().copied()
71    }
72}
73
74pub struct MappedPersons {
75    author_id: NumberMmap<BigEndian, u32, Mmap>,
76    committer_id: NumberMmap<BigEndian, u32, Mmap>,
77    num_persons: usize,
78}
79impl PropertiesBackend for MappedPersons {
80    type DataFilesAvailability = GuaranteedDataFiles;
81}
82impl OptPersons for MappedPersons {
83    /// Returns `None` if author ids are not loaded, `Some(u32::MAX)` if they are
84    /// loaded and the node has no author, or `Some(Some(_))` if they are loaded
85    /// and the node has an author
86    #[inline(always)]
87    fn author_id(&self, node: NodeId) -> Option<u32> {
88        self.author_id.get_value(node)
89    }
90    /// See [`Self::author_id`]
91    #[inline(always)]
92    fn committer_id(&self, node: NodeId) -> Option<u32> {
93        self.committer_id.get_value(node)
94    }
95    #[inline(always)]
96    fn num_persons(&self) -> usize {
97        self.num_persons
98    }
99}
100
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct VecPersons {
103    author_id: Vec<u32>,
104    committer_id: Vec<u32>,
105    num_persons: usize,
106}
107
108impl VecPersons {
109    /// Returns a [`VecPersons`] from pairs of `(author_id, committer_id)`
110    pub fn new(data: Vec<(Option<u32>, Option<u32>)>) -> Result<Self> {
111        let mut author_id = Vec::with_capacity(data.len());
112        let mut committer_id = Vec::with_capacity(data.len());
113        let mut max_person_id = None;
114        for (a, c) in data.into_iter() {
115            ensure!(a != Some(u32::MAX), "author_id may not be {}", u32::MAX);
116            ensure!(c != Some(u32::MAX), "committer_id may not be {}", u32::MAX);
117            max_person_id = max_person_id.max(a).max(c);
118            author_id.push(a.unwrap_or(u32::MAX));
119            committer_id.push(c.unwrap_or(u32::MAX));
120        }
121        // person ids are dense from 0, so the number of persons is the largest id + 1
122        let num_persons = max_person_id.map_or(0, |id| id as usize + 1);
123        Ok(VecPersons {
124            author_id,
125            committer_id,
126            num_persons,
127        })
128    }
129}
130
131impl PropertiesBackend for VecPersons {
132    type DataFilesAvailability = GuaranteedDataFiles;
133}
134impl OptPersons for VecPersons {
135    #[inline(always)]
136    fn author_id(&self, node: NodeId) -> Option<u32> {
137        self.author_id.get_value(node)
138    }
139    #[inline(always)]
140    fn committer_id(&self, node: NodeId) -> Option<u32> {
141        self.committer_id.get_value(node)
142    }
143    #[inline(always)]
144    fn num_persons(&self) -> usize {
145        self.num_persons
146    }
147}
148
149impl<
150        MAPS: MaybeMaps,
151        TIMESTAMPS: MaybeTimestamps,
152        CONTENTS: MaybeContents,
153        STRINGS: MaybeStrings,
154        LABELNAMES: MaybeLabelNames,
155    > SwhGraphProperties<MAPS, TIMESTAMPS, NoPersons, CONTENTS, STRINGS, LABELNAMES>
156{
157    /// Consumes a [`SwhGraphProperties`] and returns a new one with these methods
158    /// available:
159    ///
160    /// * [`SwhGraphProperties::author_id`]
161    /// * [`SwhGraphProperties::committer_id`]
162    /// * [`SwhGraphProperties::num_persons`]
163    pub fn load_persons(
164        self,
165    ) -> Result<SwhGraphProperties<MAPS, TIMESTAMPS, MappedPersons, CONTENTS, STRINGS, LABELNAMES>>
166    {
167        let OptMappedPersons {
168            author_id,
169            committer_id,
170            num_persons,
171        } = self.get_persons()?;
172        let persons = MappedPersons {
173            author_id: author_id?,
174            committer_id: committer_id?,
175            num_persons: num_persons?,
176        };
177        self.with_persons(persons)
178    }
179
180    /// Equivalent to [`Self::load_persons`] that does not require all files to be present
181    pub fn opt_load_persons(
182        self,
183    ) -> Result<SwhGraphProperties<MAPS, TIMESTAMPS, OptMappedPersons, CONTENTS, STRINGS, LABELNAMES>>
184    {
185        let persons = self.get_persons()?;
186        self.with_persons(persons)
187    }
188
189    fn get_persons(&self) -> Result<OptMappedPersons> {
190        Ok(OptMappedPersons {
191            author_id: load_if_exists(&self.path, AUTHOR_ID, |path| {
192                NumberMmap::new(path, self.num_nodes).context("Could not load author_id")
193            })?,
194            committer_id: load_if_exists(&self.path, COMMITTER_ID, |path| {
195                NumberMmap::new(path, self.num_nodes).context("Could not load committer_id")
196            })?,
197            num_persons: load_if_exists(&self.path, PERSONS_COUNT, |path| {
198                let count = std::fs::read_to_string(path)
199                    .with_context(|| format!("Could not read {}", path.display()))?;
200                count
201                    .trim()
202                    .parse::<usize>()
203                    .with_context(|| format!("Invalid integer in {}: {count}", path.display()))
204            })?,
205        })
206    }
207
208    /// Alternative to [`load_persons`](Self::load_persons) that allows using arbitrary
209    /// persons implementations
210    pub fn with_persons<PERSONS: MaybePersons>(
211        self,
212        persons: PERSONS,
213    ) -> Result<SwhGraphProperties<MAPS, TIMESTAMPS, PERSONS, CONTENTS, STRINGS, LABELNAMES>> {
214        Ok(SwhGraphProperties {
215            maps: self.maps,
216            timestamps: self.timestamps,
217            persons,
218            contents: self.contents,
219            strings: self.strings,
220            label_names: self.label_names,
221            path: self.path,
222            num_nodes: self.num_nodes,
223            label_names_are_in_base64_order: self.label_names_are_in_base64_order,
224        })
225    }
226}
227
228/// Functions to access the id of the author or committer of `revision`/`release` nodes.
229///
230/// Only available after calling [`load_persons`](SwhGraphProperties::load_persons)
231/// or [`load_all_properties`](crate::graph::SwhBidirectionalGraph::load_all_properties)
232impl<
233        MAPS: MaybeMaps,
234        TIMESTAMPS: MaybeTimestamps,
235        PERSONS: OptPersons,
236        CONTENTS: MaybeContents,
237        STRINGS: MaybeStrings,
238        LABELNAMES: MaybeLabelNames,
239    > SwhGraphProperties<MAPS, TIMESTAMPS, PERSONS, CONTENTS, STRINGS, LABELNAMES>
240{
241    /// Returns the number of persons having authored/committed objects in the graph
242    #[inline]
243    pub fn num_persons(&self) -> PropertiesResult<'_, usize, PERSONS> {
244        self.persons.num_persons()
245    }
246
247    /// Returns the id of the author of a revision or release, if any
248    ///
249    /// # Panics
250    ///
251    /// If the node id does not exist
252    #[inline]
253    pub fn author_id(&self, node_id: NodeId) -> PropertiesResult<'_, Option<u32>, PERSONS> {
254        PERSONS::map_if_available(self.try_author_id(node_id), |author_id| {
255            author_id.unwrap_or_else(|e| panic!("Cannot get node author: {e}"))
256        })
257    }
258
259    /// Returns the id of the author of a revision or release, if any
260    ///
261    /// Returns `Err` if the node id does not exist, and `Ok(None)` if the node
262    /// has no author
263    #[inline]
264    pub fn try_author_id(
265        &self,
266        node_id: NodeId,
267    ) -> PropertiesResult<'_, Result<Option<u32>, OutOfBoundError>, PERSONS> {
268        PERSONS::map_if_available(self.persons.author_id(node_id), |author_id| {
269            match author_id {
270                None => Err(OutOfBoundError {
271                    // Invalid node id
272                    index: node_id,
273                    len: self.num_nodes,
274                }),
275                Some(u32::MAX) => Ok(None), // No author
276                Some(id) => Ok(Some(id)),
277            }
278        })
279    }
280
281    /// Returns the id of the committer of a revision, if any
282    ///
283    /// # Panics
284    ///
285    /// If the node id does not exist
286    #[inline]
287    pub fn committer_id(&self, node_id: NodeId) -> PropertiesResult<'_, Option<u32>, PERSONS> {
288        PERSONS::map_if_available(self.try_committer_id(node_id), |committer_id| {
289            committer_id.unwrap_or_else(|e| panic!("Cannot get node committer: {e}"))
290        })
291    }
292
293    /// Returns the id of the committer of a revision, if any
294    ///
295    /// Returns `None` if the node id does not exist, and `Ok(None)` if the node
296    /// has no committer
297    #[inline]
298    pub fn try_committer_id(
299        &self,
300        node_id: NodeId,
301    ) -> PropertiesResult<'_, Result<Option<u32>, OutOfBoundError>, PERSONS> {
302        PERSONS::map_if_available(self.persons.committer_id(node_id), |committer_id| {
303            match committer_id {
304                None => Err(OutOfBoundError {
305                    // Invalid node id
306                    index: node_id,
307                    len: self.num_nodes,
308                }),
309                Some(u32::MAX) => Ok(None), // No committer
310                Some(id) => Ok(Some(id)),
311            }
312        })
313    }
314}