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 key, one record, decoded with a typed
4//! error. Scans belong to the offline build, never to a request path.
5
6use std::path::{Path, PathBuf};
7
8use concept_graph::ordinal::Ordinal;
9use redb::{ReadOnlyDatabase, ReadableDatabase, ReadableTable, TableHandle};
10
11use crate::record::{Concept, Designation, PropertyValue, RecordError};
12use crate::tables;
13
14/// A failure while opening or reading an artifact.
15#[derive(Debug, thiserror::Error)]
16pub enum StoreError {
17    /// The database could not be opened or read.
18    #[error("cannot open the artifact at {path}")]
19    Open {
20        /// The artifact.
21        path: PathBuf,
22        /// The underlying error.
23        #[source]
24        source: redb::DatabaseError,
25    },
26    /// A transaction failed.
27    #[error("cannot begin a read transaction")]
28    Transaction(#[from] redb::TransactionError),
29    /// A table is missing or has another type.
30    #[error("cannot open table {table}")]
31    Table {
32        /// The table name.
33        table: String,
34        /// The underlying error.
35        #[source]
36        source: redb::TableError,
37    },
38    /// A read failed inside the database.
39    #[error("storage read failed")]
40    Storage(#[from] redb::StorageError),
41    /// The artifact was written by another layout version.
42    #[error("artifact layout {found:?}, expected {expected:?}")]
43    Layout {
44        /// The layout found, if any.
45        found: Option<String>,
46        /// The layout this build reads.
47        expected: &'static str,
48    },
49    /// A stored record is damaged.
50    #[error("damaged record in table {table} at key {key}")]
51    Record {
52        /// The table name.
53        table: String,
54        /// The key, rendered.
55        key: String,
56        /// The underlying error.
57        #[source]
58        source: RecordError,
59    },
60}
61
62/// An opened artifact.
63pub struct Store {
64    path: PathBuf,
65    db: ReadOnlyDatabase,
66}
67
68impl std::fmt::Debug for Store {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("Store")
71            .field("path", &self.path)
72            .finish_non_exhaustive()
73    }
74}
75
76macro_rules! open_table {
77    ($txn:expr, $def:expr) => {
78        $txn.open_table($def).map_err(|source| StoreError::Table {
79            table: $def.name().to_owned(),
80            source,
81        })
82    };
83}
84
85impl Store {
86    /// Opens the artifact at `path` read-only and checks its layout version.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`StoreError`] when the file cannot be opened, is not an
91    /// artifact of this layout, or a table is missing.
92    pub fn open(path: &Path) -> Result<Self, StoreError> {
93        let db = ReadOnlyDatabase::open(path).map_err(|source| StoreError::Open {
94            path: path.to_path_buf(),
95            source,
96        })?;
97        let store = Self {
98            path: path.to_path_buf(),
99            db,
100        };
101        let layout = store.meta(tables::META_LAYOUT)?;
102        if layout.as_deref() != Some(tables::LAYOUT_VERSION) {
103            return Err(StoreError::Layout {
104                found: layout,
105                expected: tables::LAYOUT_VERSION,
106            });
107        }
108        Ok(store)
109    }
110
111    /// The artifact's path.
112    #[must_use]
113    pub fn path(&self) -> &Path {
114        &self.path
115    }
116
117    /// An artifact-level fact by key.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`StoreError`] when the database cannot be read.
122    pub fn meta(&self, key: &str) -> Result<Option<String>, StoreError> {
123        let txn = self.db.begin_read()?;
124        let table = open_table!(txn, tables::META)?;
125        Ok(table.get(key)?.map(|v| v.value().to_owned()))
126    }
127
128    /// The ordinal of a native code, if the version has it.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`StoreError`] when the database cannot be read.
133    pub fn ordinal(&self, code: &str) -> Result<Option<Ordinal>, StoreError> {
134        let txn = self.db.begin_read()?;
135        let table = open_table!(txn, tables::CODES)?;
136        Ok(table.get(code)?.map(|v| Ordinal::new(v.value())))
137    }
138
139    /// The concept at `ordinal`, if any.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`StoreError`] when the database cannot be read or the record is damaged.
144    pub fn concept(&self, ordinal: Ordinal) -> Result<Option<Concept>, StoreError> {
145        let txn = self.db.begin_read()?;
146        let table = open_table!(txn, tables::CONCEPTS)?;
147        table
148            .get(ordinal.index())?
149            .map(|v| {
150                Concept::decode(v.value()).map_err(|source| StoreError::Record {
151                    table: tables::CONCEPTS.name().to_owned(),
152                    key: ordinal.to_string(),
153                    source,
154                })
155            })
156            .transpose()
157    }
158
159    /// Every designation of `ordinal`, in index order.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`StoreError`] when the database cannot be read or a record is damaged.
164    pub fn designations(&self, ordinal: Ordinal) -> Result<Vec<Designation>, StoreError> {
165        let txn = self.db.begin_read()?;
166        let table = open_table!(txn, tables::DESIGNATIONS)?;
167        let mut out = Vec::new();
168        for entry in table.range((ordinal.index(), 0)..(ordinal.index(), u32::MAX))? {
169            let (key, value) = entry?;
170            out.push(
171                Designation::decode(value.value()).map_err(|source| StoreError::Record {
172                    table: tables::DESIGNATIONS.name().to_owned(),
173                    key: format!("{:?}", key.value()),
174                    source,
175                })?,
176            );
177        }
178        Ok(out)
179    }
180
181    /// The acceptability ordinal of a designation in a language reference set.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`StoreError`] when the database cannot be read.
186    pub fn acceptability(
187        &self,
188        ordinal: Ordinal,
189        designation: u32,
190        language_refset: u32,
191    ) -> Result<Option<u32>, StoreError> {
192        let txn = self.db.begin_read()?;
193        let table = open_table!(txn, tables::ACCEPTABILITY)?;
194        Ok(table
195            .get((ordinal.index(), designation, language_refset))?
196            .map(|v| v.value()))
197    }
198
199    /// The preferred designation of `ordinal` for a language reference set and
200    /// designation use, as the build precomputed it.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`StoreError`] when the database cannot be read or the record is damaged.
205    pub fn preferred(
206        &self,
207        ordinal: Ordinal,
208        language_refset: u32,
209        use_ordinal: u32,
210    ) -> Result<Option<Designation>, StoreError> {
211        let txn = self.db.begin_read()?;
212        let preferred = open_table!(txn, tables::PREFERRED)?;
213        let Some(index) = preferred.get((ordinal.index(), language_refset, use_ordinal))? else {
214            return Ok(None);
215        };
216        let designations = open_table!(txn, tables::DESIGNATIONS)?;
217        designations
218            .get((ordinal.index(), index.value()))?
219            .map(|v| {
220                Designation::decode(v.value()).map_err(|source| StoreError::Record {
221                    table: tables::DESIGNATIONS.name().to_owned(),
222                    key: format!("({ordinal}, {})", index.value()),
223                    source,
224                })
225            })
226            .transpose()
227    }
228
229    /// Every property of `ordinal`, as `(property key ordinal, values)`, in key order.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`StoreError`] when the database cannot be read or a record is damaged.
234    pub fn properties(
235        &self,
236        ordinal: Ordinal,
237    ) -> Result<Vec<(u32, Vec<PropertyValue>)>, StoreError> {
238        let txn = self.db.begin_read()?;
239        let table = open_table!(txn, tables::PROPERTIES)?;
240        let mut out = Vec::new();
241        for entry in table.range((ordinal.index(), 0)..(ordinal.index(), u32::MAX))? {
242            let (key, value) = entry?;
243            let values =
244                PropertyValue::decode_list(value.value()).map_err(|source| StoreError::Record {
245                    table: tables::PROPERTIES.name().to_owned(),
246                    key: format!("{:?}", key.value()),
247                    source,
248                })?;
249            out.push((key.value().1, values));
250        }
251        Ok(out)
252    }
253
254    /// A vocabulary entry: the name of a property key, designation use,
255    /// language reference set, or acceptability ordinal.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`StoreError`] when the database cannot be read.
260    pub fn vocabulary(&self, kind: Vocabulary, ordinal: u32) -> Result<Option<String>, StoreError> {
261        let txn = self.db.begin_read()?;
262        let table = open_table!(txn, kind.table())?;
263        Ok(table.get(ordinal)?.map(|v| v.value().to_owned()))
264    }
265
266    /// The ordinal of a vocabulary name, by scanning the (small) vocabulary table.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`StoreError`] when the database cannot be read.
271    pub fn vocabulary_ordinal(
272        &self,
273        kind: Vocabulary,
274        name: &str,
275    ) -> Result<Option<u32>, StoreError> {
276        let txn = self.db.begin_read()?;
277        let table = open_table!(txn, kind.table())?;
278        for entry in table.iter()? {
279            let (key, value) = entry?;
280            if value.value() == name {
281                return Ok(Some(key.value()));
282            }
283        }
284        Ok(None)
285    }
286}
287
288/// The small name tables of an artifact.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum Vocabulary {
291    /// Property key names.
292    PropertyKeys,
293    /// Designation use codes.
294    DesignationUses,
295    /// Language reference set codes.
296    LanguageRefsets,
297    /// Acceptability codes.
298    Acceptabilities,
299}
300
301impl Vocabulary {
302    fn table(self) -> redb::TableDefinition<'static, u32, &'static str> {
303        match self {
304            Self::PropertyKeys => tables::PROPERTY_KEYS,
305            Self::DesignationUses => tables::DESIGNATION_USES,
306            Self::LanguageRefsets => tables::LANGUAGE_REFSETS,
307            Self::Acceptabilities => tables::ACCEPTABILITIES,
308        }
309    }
310}