1use 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#[derive(Debug, thiserror::Error)]
27pub enum StoreError {
28 #[error("cannot open the artifact at {path}")]
30 Open {
31 path: PathBuf,
33 #[source]
35 source: redb::DatabaseError,
36 },
37 #[error("cannot begin a read transaction")]
39 Transaction(#[from] redb::TransactionError),
40 #[error("cannot open table {table}")]
42 Table {
43 table: String,
45 #[source]
47 source: redb::TableError,
48 },
49 #[error("storage read failed")]
51 Storage(#[from] redb::StorageError),
52 #[error("artifact layout {found:?}, expected {expected:?}")]
54 Layout {
55 found: Option<String>,
57 expected: &'static str,
59 },
60 #[error("the {column} column does not read from {path}")]
62 Column {
63 column: String,
65 path: PathBuf,
67 #[source]
69 source: ColumnError,
70 },
71 #[error("damaged record in table {table} at key {key}")]
73 Record {
74 table: String,
76 key: String,
78 #[source]
80 source: RecordError,
81 },
82}
83
84const CACHE_BYTES: usize = 64 << 20;
90
91pub struct Store {
93 path: PathBuf,
94 db: ReadOnlyDatabase,
95 concepts: Column,
98 displays: Column,
101 properties: Column,
104 acceptability: Column,
106 code_index: ReadOnlyTable<&'static str, u32>,
110 designation_rows: ReadOnlyTable<u32, &'static [u8]>,
114 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 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 #[must_use]
199 pub fn path(&self) -> &Path {
200 &self.path
201 }
202
203 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub enum Vocabulary {
514 PropertyKeys,
516 DesignationUses,
518 LanguageRefsets,
520 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}