Skip to main content

concept_store/
column.rs

1//! A dense column: one record per ordinal, addressed by position.
2//!
3//! An ordinal is a position, so the record at that position is found by
4//! reading two offsets and slicing, with no search and no per-read
5//! transaction. No spec governs the layout: our own design, the same shape
6//! `concept-graph` stores its adjacency in. Little-endian throughout: a
7//! record count, then one offset per record plus a terminator, then the
8//! records themselves end to end.
9//!
10//! A record the build never wrote has an empty range. Every encoding in
11//! [`crate::record`] is longer than zero bytes, so an empty range means
12//! absent and never an empty record.
13
14use std::fmt;
15
16use concept_graph::ordinal::{Ordinal, to_usize};
17
18/// The width of the count and of each offset.
19const WIDTH: usize = 4;
20
21/// A column whose bytes are not the layout this build reads.
22#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23pub enum ColumnError {
24    /// The bytes end before the header or the offsets do.
25    #[error("the column is {found} bytes, shorter than the {needed} its header declares")]
26    Truncated {
27        /// The length found.
28        found: usize,
29        /// The length the header needs.
30        needed: usize,
31    },
32    /// An offset points outside the records, or before the one before it.
33    #[error(
34        "offset {at} is {offset}, outside the {len} bytes of records or behind its predecessor"
35    )]
36    Offset {
37        /// The index of the offending offset.
38        at: usize,
39        /// Its value.
40        offset: usize,
41        /// The length of the records.
42        len: usize,
43    },
44}
45
46/// A read-only dense column.
47#[derive(Clone, Default, PartialEq, Eq)]
48pub struct Column {
49    /// `count + 1` offsets into `records`, ascending.
50    offsets: Vec<u32>,
51    records: Vec<u8>,
52}
53
54impl fmt::Debug for Column {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.debug_struct("Column")
57            .field("len", &self.len())
58            .field("bytes", &self.records.len())
59            .finish_non_exhaustive()
60    }
61}
62
63impl Column {
64    /// The column `records` pack, in ordinal order.
65    ///
66    /// A record is placed at its ordinal, and an ordinal no record names gets
67    /// an empty range.
68    #[must_use]
69    pub fn pack<'a>(count: u32, records: impl IntoIterator<Item = (Ordinal, &'a [u8])>) -> Vec<u8> {
70        let count = to_usize(count);
71        let mut placed: Vec<&[u8]> = vec![&[]; count];
72        for (ordinal, bytes) in records {
73            let at = to_usize(ordinal.index());
74            if let Some(slot) = placed.get_mut(at) {
75                *slot = bytes;
76            }
77        }
78        let total: usize = placed.iter().map(|r| r.len()).sum();
79        let mut out = Vec::with_capacity(WIDTH * (count + 2) + total);
80        out.extend_from_slice(&u32::try_from(count).unwrap_or(u32::MAX).to_le_bytes());
81        let mut at = 0_u32;
82        for record in &placed {
83            out.extend_from_slice(&at.to_le_bytes());
84            at = at.saturating_add(u32::try_from(record.len()).unwrap_or(u32::MAX));
85        }
86        out.extend_from_slice(&at.to_le_bytes());
87        for record in placed {
88            out.extend_from_slice(record);
89        }
90        out
91    }
92
93    /// Reads a packed column, checking its offsets once so a read never can.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`ColumnError`] when the bytes are shorter than the header
98    /// declares or an offset lies outside the records.
99    pub fn read(bytes: &[u8]) -> Result<Self, ColumnError> {
100        let short = |needed: usize| ColumnError::Truncated {
101            found: bytes.len(),
102            needed,
103        };
104        let head = bytes.get(..WIDTH).ok_or_else(|| short(WIDTH))?;
105        let count = to_usize(u32::from_le_bytes(head.try_into().unwrap_or([0, 0, 0, 0])));
106        let end = WIDTH.saturating_mul(count.saturating_add(2));
107        let table = bytes.get(WIDTH..end).ok_or_else(|| short(end))?;
108        let offsets: Vec<u32> = table
109            .as_chunks::<WIDTH>()
110            .0
111            .iter()
112            .map(|c| u32::from_le_bytes(*c))
113            .collect();
114        let records = bytes.get(end..).unwrap_or_default().to_vec();
115        let mut previous = 0_u32;
116        for (at, offset) in offsets.iter().enumerate() {
117            if *offset < previous || to_usize(*offset) > records.len() {
118                return Err(ColumnError::Offset {
119                    at,
120                    offset: to_usize(*offset),
121                    len: records.len(),
122                });
123            }
124            previous = *offset;
125        }
126        Ok(Self { offsets, records })
127    }
128
129    /// How many ordinals the column holds.
130    #[must_use]
131    pub fn len(&self) -> usize {
132        self.offsets.len().saturating_sub(1)
133    }
134
135    /// Whether the column holds no ordinals.
136    #[must_use]
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// The record at `ordinal`, `None` beyond the column or where the build
142    /// wrote none.
143    ///
144    /// The offsets were checked when the column was read, so this slices
145    /// without failing.
146    #[must_use]
147    pub fn get(&self, ordinal: Ordinal) -> Option<&[u8]> {
148        let at = to_usize(ordinal.index());
149        let start = to_usize(*self.offsets.get(at)?);
150        let end = to_usize(*self.offsets.get(at.checked_add(1)?)?);
151        if start == end {
152            return None;
153        }
154        self.records.get(start..end)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use concept_graph::ordinal::Ordinal;
161
162    use super::{Column, ColumnError};
163
164    #[test]
165    fn a_packed_column_reads_back_what_was_placed() {
166        let packed = Column::pack(
167            4,
168            [
169                (Ordinal::new(0), b"first".as_slice()),
170                (Ordinal::new(3), b"fourth".as_slice()),
171            ],
172        );
173        let column = Column::read(&packed).expect("reads");
174        assert_eq!(column.len(), 4);
175        assert!(!column.is_empty());
176        assert_eq!(column.get(Ordinal::new(0)), Some(b"first".as_slice()));
177        assert_eq!(column.get(Ordinal::new(1)), None, "no record was placed");
178        assert_eq!(column.get(Ordinal::new(3)), Some(b"fourth".as_slice()));
179        assert_eq!(column.get(Ordinal::new(4)), None, "beyond the column");
180    }
181
182    #[test]
183    fn an_empty_column_holds_nothing() {
184        let column = Column::read(&Column::pack(0, [])).expect("reads");
185        assert!(column.is_empty());
186        assert_eq!(column.get(Ordinal::new(0)), None);
187    }
188
189    #[test]
190    fn a_short_or_crossed_column_is_refused() {
191        assert!(matches!(
192            Column::read(&[0, 0]),
193            Err(ColumnError::Truncated { .. })
194        ));
195        // The header survives and the records do not, so an offset now points
196        // past the end of them.
197        let packed = Column::pack(2, [(Ordinal::new(0), b"ab".as_slice())]);
198        assert!(matches!(
199            Column::read(packed.get(..packed.len() - 1).expect("shortened")),
200            Err(ColumnError::Offset { .. })
201        ));
202    }
203}