Skip to main content

concept_store/
store.rs

1//! Read-only access to a built artifact.
2//!
3//! Every method is a point read: one position or one key, one record, decoded
4//! with a typed error. Ordinal-keyed data is answered from a dense column read
5//! at open; the designation text is point-read from the database, one row per
6//! concept. Scans belong to the offline build, never to a request path.
7//!
8//! Opening takes one read snapshot and keeps the two tables a request path
9//! reads open on it, so a lookup costs one descent rather than a transaction,
10//! a table-name lookup, and a descent.
11
12use std::path::{Path, PathBuf};
13
14use concept_graph::ordinal::Ordinal;
15use redb::{ReadOnlyDatabase, ReadOnlyTable, ReadableDatabase, ReadableTable, TableHandle};
16
17use crate::column::{Column, ColumnError};
18use crate::record::{self, Concept, Designation, PropertyValue, RecordError};
19use crate::tables;
20
21/// A failure while opening or reading an artifact.
22#[derive(Debug, thiserror::Error)]
23pub enum StoreError {
24    /// The database could not be opened or read.
25    #[error("cannot open the artifact at {path}")]
26    Open {
27        /// The artifact.
28        path: PathBuf,
29        /// The underlying error.
30        #[source]
31        source: redb::DatabaseError,
32    },
33    /// A transaction failed.
34    #[error("cannot begin a read transaction")]
35    Transaction(#[from] redb::TransactionError),
36    /// A table is missing or has another type.
37    #[error("cannot open table {table}")]
38    Table {
39        /// The table name.
40        table: String,
41        /// The underlying error.
42        #[source]
43        source: redb::TableError,
44    },
45    /// A read failed inside the database.
46    #[error("storage read failed")]
47    Storage(#[from] redb::StorageError),
48    /// The artifact was written by another layout version.
49    #[error("artifact layout {found:?}, expected {expected:?}")]
50    Layout {
51        /// The layout found, if any.
52        found: Option<String>,
53        /// The layout this build reads.
54        expected: &'static str,
55    },
56    /// A packed column is not the layout this build reads.
57    #[error("the {column} column does not read")]
58    Column {
59        /// The column name.
60        column: String,
61        /// The underlying error.
62        #[source]
63        source: ColumnError,
64    },
65    /// A stored record is damaged.
66    #[error("damaged record in table {table} at key {key}")]
67    Record {
68        /// The table name.
69        table: String,
70        /// The key, rendered.
71        key: String,
72        /// The underlying error.
73        #[source]
74        source: RecordError,
75    },
76}
77
78/// An opened artifact.
79pub struct Store {
80    path: PathBuf,
81    db: ReadOnlyDatabase,
82    /// The concepts, read once at open. An ordinal is a position, so this
83    /// column answers by slicing where a b-tree would search.
84    concepts: Column,
85    /// The preferred designations the build chose, per concept, so choosing a
86    /// display reads no table at all.
87    displays: Column,
88    /// The properties of every concept. The whole hierarchy is read through
89    /// these on an ECL refinement, so they are answered by position.
90    properties: Column,
91    /// The acceptability of every designation, per language reference set.
92    acceptability: Column,
93    /// The native code to ordinal index, held open on the snapshot the store
94    /// was opened on. A string key is what a b-tree is for; paying for a
95    /// transaction and a table-name lookup on top of it is not.
96    code_index: ReadOnlyTable<&'static str, u32>,
97    /// The designations of every concept, one row per concept, held open on
98    /// the same snapshot. The text is the largest thing an artifact holds, so
99    /// it stays on disk rather than becoming a resident column (#338).
100    designation_rows: ReadOnlyTable<u32, &'static [u8]>,
101    /// The acceptability ordinal that marks a designation preferred.
102    preferred: Option<u32>,
103}
104
105impl std::fmt::Debug for Store {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("Store")
108            .field("path", &self.path)
109            .finish_non_exhaustive()
110    }
111}
112
113macro_rules! open_table {
114    ($txn:expr, $def:expr) => {
115        $txn.open_table($def).map_err(|source| StoreError::Table {
116            table: $def.name().to_owned(),
117            source,
118        })
119    };
120}
121
122impl Store {
123    /// Opens the artifact at `path` read-only and checks its layout version.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`StoreError`] when the file cannot be opened, is not an
128    /// artifact of this layout, or a table is missing.
129    pub fn open(path: &Path) -> Result<Self, StoreError> {
130        let db = ReadOnlyDatabase::open(path).map_err(|source| StoreError::Open {
131            path: path.to_path_buf(),
132            source,
133        })?;
134        let txn = db.begin_read()?;
135        let meta = open_table!(txn, tables::META)?;
136        let layout = meta
137            .get(tables::META_LAYOUT)?
138            .map(|value| value.value().to_owned());
139        if layout.as_deref() != Some(tables::LAYOUT_VERSION) {
140            return Err(StoreError::Layout {
141                found: layout,
142                expected: tables::LAYOUT_VERSION,
143            });
144        }
145        let columns = open_table!(txn, tables::COLUMNS)?;
146        let column = |name: &str| -> Result<Column, StoreError> {
147            let Some(bytes) = columns.get(name)? else {
148                return Ok(Column::default());
149            };
150            Column::read(bytes.value()).map_err(|source| StoreError::Column {
151                column: name.to_owned(),
152                source,
153            })
154        };
155        let concepts = column(tables::COLUMN_CONCEPTS)?;
156        let displays = column(tables::COLUMN_DISPLAYS)?;
157        let properties = column(tables::COLUMN_PROPERTIES)?;
158        let acceptability = column(tables::COLUMN_ACCEPTABILITY)?;
159        let preferred = meta
160            .get(tables::META_PREFERRED)?
161            .and_then(|value| value.value().parse().ok());
162        let code_index = open_table!(txn, tables::CODES)?;
163        let designation_rows = open_table!(txn, tables::DESIGNATIONS)?;
164        Ok(Self {
165            path: path.to_path_buf(),
166            db,
167            concepts,
168            displays,
169            properties,
170            acceptability,
171            code_index,
172            designation_rows,
173            preferred,
174        })
175    }
176
177    /// The artifact's path.
178    #[must_use]
179    pub fn path(&self) -> &Path {
180        &self.path
181    }
182
183    /// An artifact-level fact by key.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`StoreError`] when the database cannot be read.
188    pub fn meta(&self, key: &str) -> Result<Option<String>, StoreError> {
189        let txn = self.db.begin_read()?;
190        let table = open_table!(txn, tables::META)?;
191        Ok(table.get(key)?.map(|v| v.value().to_owned()))
192    }
193
194    /// The ordinal of a native code, if the version has it.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`StoreError`] when the database cannot be read.
199    pub fn ordinal(&self, code: &str) -> Result<Option<Ordinal>, StoreError> {
200        Ok(self
201            .code_index
202            .get(code)?
203            .map(|value| Ordinal::new(value.value())))
204    }
205
206    /// The concept at `ordinal`, if any.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`StoreError`] when the database cannot be read or the record is damaged.
211    pub fn concept(&self, ordinal: Ordinal) -> Result<Option<Concept>, StoreError> {
212        self.concepts
213            .get(ordinal)
214            .map(|bytes| Self::decode_concept(ordinal, bytes))
215            .transpose()
216    }
217
218    /// The native code at `ordinal`, borrowed from the column.
219    ///
220    /// The code is the first field of the record, so this reads one length
221    /// and one string and copies nothing.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`StoreError`] when the code is damaged.
226    pub fn code(&self, ordinal: Ordinal) -> Result<Option<&str>, StoreError> {
227        self.concepts
228            .get(ordinal)
229            .map(|bytes| {
230                record::code(bytes).map_err(|source| StoreError::Record {
231                    table: tables::COLUMN_CONCEPTS.to_owned(),
232                    key: ordinal.to_string(),
233                    source,
234                })
235            })
236            .transpose()
237    }
238
239    /// Decodes one concept record, naming the ordinal it came from.
240    fn decode_concept(ordinal: Ordinal, bytes: &[u8]) -> Result<Concept, StoreError> {
241        Concept::decode(bytes).map_err(|source| StoreError::Record {
242            table: tables::COLUMN_CONCEPTS.to_owned(),
243            key: ordinal.to_string(),
244            source,
245        })
246    }
247
248    /// The concepts at `ordinals`, in the order given, `None` where the store
249    /// has no such concept.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`StoreError`] when a record is damaged.
254    pub fn concepts(
255        &self,
256        ordinals: impl IntoIterator<Item = Ordinal>,
257    ) -> Result<Vec<Option<Concept>>, StoreError> {
258        ordinals
259            .into_iter()
260            .map(|ordinal| self.concept(ordinal))
261            .collect()
262    }
263
264    /// The native codes at `ordinals`, in the order given, `None` where the
265    /// store has no such concept.
266    ///
267    /// Each code is sliced out of the concept column and decoded only as far
268    /// as its code, so the batch costs one string per code and nothing else.
269    /// A caller that can borrow calls [`Self::code`] and pays not even that.
270    ///
271    /// # Errors
272    ///
273    /// Returns [`StoreError`] when a code is damaged.
274    pub fn codes(
275        &self,
276        ordinals: impl IntoIterator<Item = Ordinal>,
277    ) -> Result<Vec<Option<String>>, StoreError> {
278        ordinals
279            .into_iter()
280            .map(|ordinal| Ok(self.code(ordinal)?.map(ToOwned::to_owned)))
281            .collect()
282    }
283
284    /// Every designation of `ordinal`, in index order.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`StoreError`] when the database cannot be read or a record is damaged.
289    pub fn designations(&self, ordinal: Ordinal) -> Result<Vec<Designation>, StoreError> {
290        let Some(packed) = self.designation_rows.get(ordinal.index())? else {
291            return Ok(Vec::new());
292        };
293        record::Designations::decode(packed.value()).map_err(|source| StoreError::Record {
294            table: tables::DESIGNATIONS.name().to_owned(),
295            key: ordinal.to_string(),
296            source,
297        })
298    }
299
300    /// The acceptability ordinal of a designation in a language reference set.
301    ///
302    /// # Errors
303    ///
304    /// Returns [`StoreError`] when the column is damaged.
305    pub fn acceptability(
306        &self,
307        ordinal: Ordinal,
308        designation: u32,
309        language_refset: u32,
310    ) -> Result<Option<u32>, StoreError> {
311        let Some(bytes) = self.acceptability.get(ordinal) else {
312            return Ok(None);
313        };
314        record::Acceptability::find(bytes, designation, language_refset).map_err(|source| {
315            StoreError::Record {
316                table: tables::COLUMN_ACCEPTABILITY.to_owned(),
317                key: ordinal.to_string(),
318                source,
319            }
320        })
321    }
322
323    /// The preferred designation of `ordinal` for a language reference set and
324    /// designation use: the lowest-indexed designation of that use the
325    /// reference set marks preferred.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`StoreError`] when the database cannot be read or a record is
330    /// damaged.
331    pub fn preferred(
332        &self,
333        ordinal: Ordinal,
334        language_refset: u32,
335        use_ordinal: u32,
336    ) -> Result<Option<Designation>, StoreError> {
337        let (Some(preferred), Some(bytes)) = (self.preferred, self.acceptability.get(ordinal))
338        else {
339            return Ok(None);
340        };
341        let entries =
342            record::Acceptability::decode(bytes).map_err(|source| StoreError::Record {
343                table: tables::COLUMN_ACCEPTABILITY.to_owned(),
344                key: ordinal.to_string(),
345                source,
346            })?;
347        let Some(packed) = self.designation_rows.get(ordinal.index())? else {
348            return Ok(None);
349        };
350        let damaged = |source| StoreError::Record {
351            table: tables::DESIGNATIONS.name().to_owned(),
352            key: ordinal.to_string(),
353            source,
354        };
355        let mut chosen: Option<(u32, Designation)> = None;
356        for (index, refset, acceptability) in entries {
357            if refset != language_refset || acceptability != preferred {
358                continue;
359            }
360            if chosen.as_ref().is_some_and(|(at, _)| *at <= index) {
361                continue;
362            }
363            let Some(designation) =
364                record::Designations::at(packed.value(), index).map_err(damaged)?
365            else {
366                continue;
367            };
368            if designation.use_ordinal == use_ordinal {
369                chosen = Some((index, designation));
370            }
371        }
372        Ok(chosen.map(|(_, designation)| designation))
373    }
374
375    /// The display of `ordinal` from the first of `language_refsets` whose
376    /// language `accept` admits.
377    ///
378    /// The build chose one display per reference set, so this reads the column
379    /// and stops at the first language the caller wants; no table is touched.
380    /// The reference sets are asked in the order given, which is what decides
381    /// the answer when several carry the same language.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`StoreError`] when the column is damaged.
386    pub fn display(
387        &self,
388        ordinal: Ordinal,
389        language_refsets: impl IntoIterator<Item = u32>,
390        accept: impl Fn(&str) -> bool,
391    ) -> Result<Option<String>, StoreError> {
392        let Some(bytes) = self.displays.get(ordinal) else {
393            return Ok(None);
394        };
395        let damaged = |source| StoreError::Record {
396            table: tables::COLUMN_DISPLAYS.to_owned(),
397            key: ordinal.to_string(),
398            source,
399        };
400        for refset in language_refsets {
401            let Some((language, term)) = record::Displays::find(bytes, refset).map_err(damaged)?
402            else {
403                continue;
404            };
405            if accept(&language) {
406                return Ok(Some(term));
407            }
408        }
409        Ok(None)
410    }
411
412    /// Every property of `ordinal`, as `(property key ordinal, values)`, in key order.
413    ///
414    /// # Errors
415    ///
416    /// Returns [`StoreError`] when the column is damaged.
417    pub fn properties(
418        &self,
419        ordinal: Ordinal,
420    ) -> Result<Vec<(u32, Vec<PropertyValue>)>, StoreError> {
421        let Some(bytes) = self.properties.get(ordinal) else {
422            return Ok(Vec::new());
423        };
424        record::Properties::decode(bytes).map_err(|source| StoreError::Record {
425            table: tables::COLUMN_PROPERTIES.to_owned(),
426            key: ordinal.to_string(),
427            source,
428        })
429    }
430
431    /// A vocabulary entry: the name of a property key, designation use,
432    /// language reference set, or acceptability ordinal.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`StoreError`] when the database cannot be read.
437    pub fn vocabulary(&self, kind: Vocabulary, ordinal: u32) -> Result<Option<String>, StoreError> {
438        let txn = self.db.begin_read()?;
439        let table = open_table!(txn, kind.table())?;
440        Ok(table.get(ordinal)?.map(|v| v.value().to_owned()))
441    }
442
443    /// The ordinal of a vocabulary name, by scanning the (small) vocabulary table.
444    ///
445    /// # Errors
446    ///
447    /// Returns [`StoreError`] when the database cannot be read.
448    pub fn vocabulary_ordinal(
449        &self,
450        kind: Vocabulary,
451        name: &str,
452    ) -> Result<Option<u32>, StoreError> {
453        let txn = self.db.begin_read()?;
454        let table = open_table!(txn, kind.table())?;
455        for entry in table.iter()? {
456            let (key, value) = entry?;
457            if value.value() == name {
458                return Ok(Some(key.value()));
459            }
460        }
461        Ok(None)
462    }
463}
464
465/// The small name tables of an artifact.
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum Vocabulary {
468    /// Property key names.
469    PropertyKeys,
470    /// Designation use codes.
471    DesignationUses,
472    /// Language reference set codes.
473    LanguageRefsets,
474    /// Acceptability codes.
475    Acceptabilities,
476}
477
478impl Vocabulary {
479    fn table(self) -> redb::TableDefinition<'static, u32, &'static str> {
480        match self {
481            Self::PropertyKeys => tables::PROPERTY_KEYS,
482            Self::DesignationUses => tables::DESIGNATION_USES,
483            Self::LanguageRefsets => tables::LANGUAGE_REFSETS,
484            Self::Acceptabilities => tables::ACCEPTABILITIES,
485        }
486    }
487}