Skip to main content

concept_store/
store.rs

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