use std::path::{Path, PathBuf};
use concept_graph::ordinal::Ordinal;
use redb::{ReadOnlyDatabase, ReadableDatabase, ReadableTable, TableHandle};
use crate::record::{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("damaged record in table {table} at key {key}")]
Record {
table: String,
key: String,
#[source]
source: RecordError,
},
}
pub struct Store {
path: PathBuf,
db: ReadOnlyDatabase,
}
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 = ReadOnlyDatabase::open(path).map_err(|source| StoreError::Open {
path: path.to_path_buf(),
source,
})?;
let store = Self {
path: path.to_path_buf(),
db,
};
let layout = store.meta(tables::META_LAYOUT)?;
if layout.as_deref() != Some(tables::LAYOUT_VERSION) {
return Err(StoreError::Layout {
found: layout,
expected: tables::LAYOUT_VERSION,
});
}
Ok(store)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
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> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::CODES)?;
Ok(table.get(code)?.map(|v| Ordinal::new(v.value())))
}
pub fn concept(&self, ordinal: Ordinal) -> Result<Option<Concept>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::CONCEPTS)?;
table
.get(ordinal.index())?
.map(|v| {
Concept::decode(v.value()).map_err(|source| StoreError::Record {
table: tables::CONCEPTS.name().to_owned(),
key: ordinal.to_string(),
source,
})
})
.transpose()
}
pub fn designations(&self, ordinal: Ordinal) -> Result<Vec<Designation>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::DESIGNATIONS)?;
let mut out = Vec::new();
for entry in table.range((ordinal.index(), 0)..(ordinal.index(), u32::MAX))? {
let (key, value) = entry?;
out.push(
Designation::decode(value.value()).map_err(|source| StoreError::Record {
table: tables::DESIGNATIONS.name().to_owned(),
key: format!("{:?}", key.value()),
source,
})?,
);
}
Ok(out)
}
pub fn acceptability(
&self,
ordinal: Ordinal,
designation: u32,
language_refset: u32,
) -> Result<Option<u32>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::ACCEPTABILITY)?;
Ok(table
.get((ordinal.index(), designation, language_refset))?
.map(|v| v.value()))
}
pub fn preferred(
&self,
ordinal: Ordinal,
language_refset: u32,
use_ordinal: u32,
) -> Result<Option<Designation>, StoreError> {
let txn = self.db.begin_read()?;
let preferred = open_table!(txn, tables::PREFERRED)?;
let Some(index) = preferred.get((ordinal.index(), language_refset, use_ordinal))? else {
return Ok(None);
};
let designations = open_table!(txn, tables::DESIGNATIONS)?;
designations
.get((ordinal.index(), index.value()))?
.map(|v| {
Designation::decode(v.value()).map_err(|source| StoreError::Record {
table: tables::DESIGNATIONS.name().to_owned(),
key: format!("({ordinal}, {})", index.value()),
source,
})
})
.transpose()
}
pub fn properties(
&self,
ordinal: Ordinal,
) -> Result<Vec<(u32, Vec<PropertyValue>)>, StoreError> {
let txn = self.db.begin_read()?;
let table = open_table!(txn, tables::PROPERTIES)?;
let mut out = Vec::new();
for entry in table.range((ordinal.index(), 0)..(ordinal.index(), u32::MAX))? {
let (key, value) = entry?;
let values =
PropertyValue::decode_list(value.value()).map_err(|source| StoreError::Record {
table: tables::PROPERTIES.name().to_owned(),
key: format!("{:?}", key.value()),
source,
})?;
out.push((key.value().1, values));
}
Ok(out)
}
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,
}
}
}