Skip to main content

concept_graph/
members.rs

1//! Reference set memberships: for every reference set of an edition, the
2//! bitmap of the concepts that are its active members.
3//!
4//! `?fhir_vs=refset/[sctid]` and the `concept in [sctid]` filter of the FHIR
5//! SNOMED CT page read one bitmap; `?fhir_vs=refset` reads the keys. No spec
6//! governs the layout: our own design. Little-endian, a magic and version
7//! prefix, a count, then per reference set its SCTID as `u64`, the bitmap's
8//! serialized length, and the bitmap in roaring's portable serialization.
9
10use std::collections::BTreeMap;
11use std::io::{self, Read, Write};
12
13use roaring::RoaringBitmap;
14
15use crate::ordinal::{Ordinal, to_usize};
16
17const MAGIC: &[u8; 8] = b"FTRSETS\0";
18const VERSION: u32 = 1;
19
20/// A failure while reading or writing the memberships.
21#[derive(Debug, thiserror::Error)]
22pub enum MembersError {
23    /// An I/O failure.
24    #[error("memberships I/O failed")]
25    Io(#[from] io::Error),
26    /// The bytes do not start with the memberships magic.
27    #[error("not a memberships artifact")]
28    Magic,
29    /// The layout version is not the one this build reads.
30    #[error("memberships layout version {found}, expected {expected}")]
31    Version {
32        /// The version found.
33        found: u32,
34        /// The version this build reads.
35        expected: u32,
36    },
37}
38
39/// The members of every reference set, by reference set SCTID.
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct Memberships {
42    sets: BTreeMap<u64, RoaringBitmap>,
43}
44
45impl Memberships {
46    /// No reference sets.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Adds `concept` to the reference set `refset`.
53    pub fn insert(&mut self, refset: u64, concept: Ordinal) {
54        self.sets.entry(refset).or_default().insert(concept.index());
55    }
56
57    /// The heap bytes the membership bitmaps hold ([`crate::footprint`]).
58    #[must_use]
59    pub fn size_in_bytes(&self) -> usize {
60        self.sets.values().fold(
61            crate::footprint::entries::<u64, RoaringBitmap>(self.sets.len()),
62            |total, set| total.saturating_add(crate::footprint::bitmap(set)),
63        )
64    }
65
66    /// The members of `refset`, when the edition has it.
67    #[must_use]
68    pub fn members(&self, refset: u64) -> Option<&RoaringBitmap> {
69        self.sets.get(&refset)
70    }
71
72    /// The reference set SCTIDs, ascending.
73    pub fn refsets(&self) -> impl Iterator<Item = u64> + '_ {
74        self.sets.keys().copied()
75    }
76
77    /// The number of reference sets.
78    #[must_use]
79    pub fn len(&self) -> usize {
80        self.sets.len()
81    }
82
83    /// Whether there are no reference sets.
84    #[must_use]
85    pub fn is_empty(&self) -> bool {
86        self.sets.is_empty()
87    }
88
89    /// The number of memberships over every reference set.
90    #[must_use]
91    pub fn total(&self) -> u64 {
92        self.sets.values().map(RoaringBitmap::len).sum()
93    }
94
95    /// Writes the layout.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`MembersError::Io`] when writing fails.
100    pub fn write_to(&self, out: &mut impl Write) -> Result<(), MembersError> {
101        out.write_all(MAGIC)?;
102        out.write_all(&VERSION.to_le_bytes())?;
103        let count = crate::persist::u32_len(self.sets.len(), "the refset count")?;
104        out.write_all(&count.to_le_bytes())?;
105        for (refset, set) in &self.sets {
106            out.write_all(&refset.to_le_bytes())?;
107            let size = crate::persist::u32_len(set.serialized_size(), "a set size")?;
108            out.write_all(&size.to_le_bytes())?;
109            set.serialize_into(&mut *out)?;
110        }
111        Ok(())
112    }
113
114    /// Reads the layout.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`MembersError`] for a truncated or foreign artifact.
119    pub fn read_from(input: &mut impl Read) -> Result<Self, MembersError> {
120        let mut magic = [0_u8; 8];
121        input.read_exact(&mut magic)?;
122        if &magic != MAGIC {
123            return Err(MembersError::Magic);
124        }
125        let version = read_u32(input)?;
126        if version != VERSION {
127            return Err(MembersError::Version {
128                found: version,
129                expected: VERSION,
130            });
131        }
132        let count = read_u32(input)?;
133        let mut sets = BTreeMap::new();
134        for _ in 0..count {
135            let mut long = [0_u8; 8];
136            input.read_exact(&mut long)?;
137            let size = read_u32(input)?;
138            let mut bytes = vec![0_u8; to_usize(size)];
139            input.read_exact(&mut bytes)?;
140            sets.insert(
141                u64::from_le_bytes(long),
142                RoaringBitmap::deserialize_from(bytes.as_slice())?,
143            );
144        }
145        Ok(Self { sets })
146    }
147}
148
149fn read_u32(input: &mut impl Read) -> io::Result<u32> {
150    let mut buffer = [0_u8; 4];
151    input.read_exact(&mut buffer)?;
152    Ok(u32::from_le_bytes(buffer))
153}
154
155#[cfg(test)]
156mod tests {
157    use roaring::RoaringBitmap;
158
159    use super::{MembersError, Memberships};
160    use crate::ordinal::Ordinal;
161
162    #[test]
163    fn memberships_round_trip_and_reject_foreign_bytes() {
164        let mut members = Memberships::new();
165        members.insert(31_000_147_101, Ordinal::new(2));
166        members.insert(31_000_147_101, Ordinal::new(3));
167        members.insert(900_000_000_000_497_000, Ordinal::new(3));
168        assert_eq!(members.len(), 2);
169        assert_eq!(members.total(), 3);
170        assert_eq!(
171            members.members(31_000_147_101).map(RoaringBitmap::len),
172            Some(2)
173        );
174        assert!(members.members(1).is_none());
175        assert_eq!(members.refsets().next(), Some(31_000_147_101));
176        let mut bytes = Vec::new();
177        members.write_to(&mut bytes).expect("writes");
178        assert_eq!(
179            Memberships::read_from(&mut bytes.as_slice()).expect("reads"),
180            members
181        );
182        assert!(matches!(
183            Memberships::read_from(&mut b"XXXXXXXX\0\0\0\0".as_slice()),
184            Err(MembersError::Magic)
185        ));
186    }
187}