use std::path::{Path, PathBuf};
use concept_graph::ordinal::Ordinal;
use redb::{ReadOnlyDatabase, ReadOnlyTable, ReadableDatabase, ReadableTable, TableHandle};
use crate::column::{Column, ColumnError};
use crate::record::{self, Concept, Designation, PropertyValue, RecordError};
use crate::tables;
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("cannot open the artifact at {path}")]
Open {
path: PathBuf,
#[source]
source: redb::DatabaseError,
},
#[error("cannot begin a read transaction")]
Transaction(#[from] redb::TransactionError),
#[error("cannot open table {table}")]
Table {
table: String,
#[source]
source: redb::TableError,
},
#[error("storage read failed")]
Storage(#[from] redb::StorageError),
#[error("artifact layout {found:?}, expected {expected:?}")]
Layout {
found: Option<String>,
expected: &'static str,
},
#[error("the {column} column does not read from {path}")]
Column {
column: String,
path: PathBuf,
#[source]
source: ColumnError,
},
#[error("damaged record in table {table} at key {key}")]
Record {
table: String,
key: String,
#[source]
source: RecordError,
},
}
const CACHE_BYTES: usize = 64 << 20;
pub struct Store {
path: PathBuf,
db: ReadOnlyDatabase,
concepts: Column,
displays: Column,
properties: Column,
acceptability: Column,
code_index: ReadOnlyTable<&'static str, u32>,
designation_rows: ReadOnlyTable<u32, &'static [u8]>,
preferred: Option<u32>,
}
impl std::fmt::Debug for Store {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Store")
.field("path", &self.path)
.finish_non_exhaustive()
}
}
macro_rules! open_table {
($txn:expr, $def:expr) => {
$txn.open_table($def).map_err(|source| StoreError::Table {
table: $def.name().to_owned(),
source,
})
};
}
impl Store {
pub fn open(path: &Path) -> Result<Self, StoreError> {
let db = redb::Builder::new()
.set_cache_size(CACHE_BYTES)
.open_read_only(path)
.map_err(|source| StoreError::Open {
path: path.to_path_buf(),
source,
})?;
let txn = db.begin_read()?;
let meta = open_table!(txn, tables::META)?;
let layout = meta
.get(tables::META_LAYOUT)?
.map(|value| value.value().to_owned());
if layout.as_deref() != Some(tables::LAYOUT_VERSION) {
return Err(StoreError::Layout {
found: layout,
expected: tables::LAYOUT_VERSION,
});
}
let column = |name: &str| -> Result<Column, StoreError> {
let file = Column::file(path, name);
let failed = |source| StoreError::Column {
column: name.to_owned(),
path: file.clone(),
source,
};
let mut reader = concept_graph::read::buffered(&file)
.map_err(|source| failed(ColumnError::Io(source)))?;
Column::read_from(&mut reader).map_err(failed)
};
let concepts = column(tables::COLUMN_CONCEPTS)?;
let displays = column(tables::COLUMN_DISPLAYS)?;
let properties = column(tables::COLUMN_PROPERTIES)?;
let acceptability = column(tables::COLUMN_ACCEPTABILITY)?;
let preferred = meta
.get(tables::META_PREFERRED)?
.and_then(|value| value.value().parse().ok());
let code_index = open_table!(txn, tables::CODES)?;
let designation_rows = open_table!(txn, tables::DESIGNATIONS)?;
Ok(Self {
path: path.to_path_buf(),
db,
concepts,
displays,
properties,
acceptability,
code_index,
designation_rows,
preferred,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn column_sizes(&self) -> [(&'static str, usize); 4] {
[
(tables::COLUMN_CONCEPTS, self.concepts.size_in_bytes()),
(tables::COLUMN_DISPLAYS, self.displays.size_in_bytes()),
(tables::COLUMN_PROPERTIES, self.properties.size_in_bytes()),
(
tables::COLUMN_ACCEPTABILITY,
self.acceptability.size_in_bytes(),
),
]
}
#[must_use]
pub fn size_in_bytes(&self) -> usize {
self.column_sizes()
.into_iter()
.fold(0_usize, |total, (_, bytes)| total.saturating_add(bytes))
}
pub fn meta(&self, key: &str) -> Result<Option<String>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::META)?;
Ok(table.get(key)?.map(|v| v.value().to_owned()))
}
pub fn ordinal(&self, code: &str) -> Result<Option<Ordinal>, StoreError> {
Ok(self
.code_index
.get(code)?
.map(|value| Ordinal::new(value.value())))
}
pub fn concept(&self, ordinal: Ordinal) -> Result<Option<Concept>, StoreError> {
self.concepts
.get(ordinal)
.map(|bytes| Self::decode_concept(ordinal, bytes))
.transpose()
}
pub fn code(&self, ordinal: Ordinal) -> Result<Option<&str>, StoreError> {
self.concepts
.get(ordinal)
.map(|bytes| {
record::code(bytes).map_err(|source| StoreError::Record {
table: tables::COLUMN_CONCEPTS.to_owned(),
key: ordinal.to_string(),
source,
})
})
.transpose()
}
fn decode_concept(ordinal: Ordinal, bytes: &[u8]) -> Result<Concept, StoreError> {
Concept::decode(bytes).map_err(|source| StoreError::Record {
table: tables::COLUMN_CONCEPTS.to_owned(),
key: ordinal.to_string(),
source,
})
}
pub fn concepts(
&self,
ordinals: impl IntoIterator<Item = Ordinal>,
) -> Result<Vec<Option<Concept>>, StoreError> {
ordinals
.into_iter()
.map(|ordinal| self.concept(ordinal))
.collect()
}
pub fn codes(
&self,
ordinals: impl IntoIterator<Item = Ordinal>,
) -> Result<Vec<Option<String>>, StoreError> {
ordinals
.into_iter()
.map(|ordinal| Ok(self.code(ordinal)?.map(ToOwned::to_owned)))
.collect()
}
pub fn designations(&self, ordinal: Ordinal) -> Result<Vec<Designation>, StoreError> {
let Some(packed) = self.designation_rows.get(ordinal.index())? else {
return Ok(Vec::new());
};
record::Designations::decode(packed.value()).map_err(|source| StoreError::Record {
table: tables::DESIGNATIONS.name().to_owned(),
key: ordinal.to_string(),
source,
})
}
pub fn acceptability(
&self,
ordinal: Ordinal,
designation: u32,
language_refset: u32,
) -> Result<Option<u32>, StoreError> {
let Some(bytes) = self.acceptability.get(ordinal) else {
return Ok(None);
};
record::Acceptability::find(bytes, designation, language_refset).map_err(|source| {
StoreError::Record {
table: tables::COLUMN_ACCEPTABILITY.to_owned(),
key: ordinal.to_string(),
source,
}
})
}
pub fn preferred(
&self,
ordinal: Ordinal,
language_refset: u32,
use_ordinal: u32,
) -> Result<Option<Designation>, StoreError> {
let (Some(preferred), Some(bytes)) = (self.preferred, self.acceptability.get(ordinal))
else {
return Ok(None);
};
let entries =
record::Acceptability::decode(bytes).map_err(|source| StoreError::Record {
table: tables::COLUMN_ACCEPTABILITY.to_owned(),
key: ordinal.to_string(),
source,
})?;
let Some(packed) = self.designation_rows.get(ordinal.index())? else {
return Ok(None);
};
let damaged = |source| StoreError::Record {
table: tables::DESIGNATIONS.name().to_owned(),
key: ordinal.to_string(),
source,
};
let mut chosen: Option<(u32, Designation)> = None;
for (index, refset, acceptability) in entries {
if refset != language_refset || acceptability != preferred {
continue;
}
if chosen.as_ref().is_some_and(|(at, _)| *at <= index) {
continue;
}
let Some(designation) =
record::Designations::at(packed.value(), index).map_err(damaged)?
else {
continue;
};
if designation.use_ordinal == use_ordinal {
chosen = Some((index, designation));
}
}
Ok(chosen.map(|(_, designation)| designation))
}
pub fn display(
&self,
ordinal: Ordinal,
language_refsets: impl IntoIterator<Item = u32>,
accept: impl Fn(&str) -> bool,
) -> Result<Option<String>, StoreError> {
let Some(bytes) = self.displays.get(ordinal) else {
return Ok(None);
};
let damaged = |source| StoreError::Record {
table: tables::COLUMN_DISPLAYS.to_owned(),
key: ordinal.to_string(),
source,
};
for refset in language_refsets {
let Some((language, term)) = record::Displays::find(bytes, refset).map_err(damaged)?
else {
continue;
};
if accept(&language) {
return Ok(Some(term));
}
}
Ok(None)
}
pub fn properties(
&self,
ordinal: Ordinal,
) -> Result<Vec<(u32, Vec<PropertyValue>)>, StoreError> {
let Some(bytes) = self.properties.get(ordinal) else {
return Ok(Vec::new());
};
record::Properties::decode(bytes).map_err(|source| StoreError::Record {
table: tables::COLUMN_PROPERTIES.to_owned(),
key: ordinal.to_string(),
source,
})
}
pub fn vocabulary(&self, kind: Vocabulary, ordinal: u32) -> Result<Option<String>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, kind.table())?;
Ok(table.get(ordinal)?.map(|v| v.value().to_owned()))
}
pub fn vocabulary_ordinal(
&self,
kind: Vocabulary,
name: &str,
) -> Result<Option<u32>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, kind.table())?;
for entry in table.iter()? {
let (key, value) = entry?;
if value.value() == name {
return Ok(Some(key.value()));
}
}
Ok(None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vocabulary {
PropertyKeys,
DesignationUses,
LanguageRefsets,
Acceptabilities,
}
impl Vocabulary {
fn table(self) -> redb::TableDefinition<'static, u32, &'static str> {
match self {
Self::PropertyKeys => tables::PROPERTY_KEYS,
Self::DesignationUses => tables::DESIGNATION_USES,
Self::LanguageRefsets => tables::LANGUAGE_REFSETS,
Self::Acceptabilities => tables::ACCEPTABILITIES,
}
}
}