Skip to main content

concept_graph/
members.rs

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