1use 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#[derive(Debug, thiserror::Error)]
23pub enum StoreError {
24 #[error("cannot open the artifact at {path}")]
26 Open {
27 path: PathBuf,
29 #[source]
31 source: redb::DatabaseError,
32 },
33 #[error("cannot begin a read transaction")]
35 Transaction(#[from] redb::TransactionError),
36 #[error("cannot open table {table}")]
38 Table {
39 table: String,
41 #[source]
43 source: redb::TableError,
44 },
45 #[error("storage read failed")]
47 Storage(#[from] redb::StorageError),
48 #[error("artifact layout {found:?}, expected {expected:?}")]
50 Layout {
51 found: Option<String>,
53 expected: &'static str,
55 },
56 #[error("the {column} column does not read")]
58 Column {
59 column: String,
61 #[source]
63 source: ColumnError,
64 },
65 #[error("damaged record in table {table} at key {key}")]
67 Record {
68 table: String,
70 key: String,
72 #[source]
74 source: RecordError,
75 },
76}
77
78pub struct Store {
80 path: PathBuf,
81 db: ReadOnlyDatabase,
82 concepts: Column,
85 displays: Column,
88 properties: Column,
91 acceptability: Column,
93 code_index: ReadOnlyTable<&'static str, u32>,
97 designation_rows: ReadOnlyTable<u32, &'static [u8]>,
101 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 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 #[must_use]
179 pub fn path(&self) -> &Path {
180 &self.path
181 }
182
183 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum Vocabulary {
468 PropertyKeys,
470 DesignationUses,
472 LanguageRefsets,
474 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}