use std::collections::BTreeMap;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use concept_graph::ordinal::Ordinal;
use redb::{Database, TableHandle};
use crate::column::Column;
use crate::record::{self, Concept, Designation, PropertyValue};
use crate::tables;
const WRITE_BUFFER: usize = 1 << 20;
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
#[error("cannot create the artifact at {path}")]
Create {
path: PathBuf,
#[source]
source: redb::DatabaseError,
},
#[error("transaction failed")]
Transaction(#[from] redb::TransactionError),
#[error("cannot open table")]
Table(#[from] redb::TableError),
#[error("storage write failed")]
Storage(#[from] redb::StorageError),
#[error("commit failed")]
Commit(#[from] redb::CommitError),
#[error("compaction failed")]
Compaction(#[from] redb::CompactionError),
#[error("cannot write {path}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{kind} {name:?} is already ordinal {existing}, not {requested}")]
Vocabulary {
kind: String,
name: String,
existing: u32,
requested: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreferredRule {
pub preferred: u32,
pub display_use: Option<u32>,
}
pub struct StoreBuilder {
path: PathBuf,
db: Database,
system: String,
version: String,
vocabularies: BTreeMap<&'static str, BTreeMap<u32, String>>,
codes: BTreeMap<String, u32>,
concepts: BTreeMap<u32, Vec<u8>>,
designations: BTreeMap<(u32, u32), Vec<u8>>,
acceptability: BTreeMap<(u32, u32, u32), u32>,
designation_uses: BTreeMap<(u32, u32), u32>,
properties: BTreeMap<(u32, u32), Vec<u8>>,
}
impl std::fmt::Debug for StoreBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoreBuilder")
.field("path", &self.path)
.field("concepts", &self.concepts.len())
.field("designations", &self.designations.len())
.finish_non_exhaustive()
}
}
impl StoreBuilder {
pub fn create(path: &Path, system: &str, version: &str) -> Result<Self, BuildError> {
if path.exists() {
std::fs::remove_file(path).map_err(|source| BuildError::Create {
path: path.to_path_buf(),
source: redb::DatabaseError::Storage(redb::StorageError::Io(source)),
})?;
}
let db = Database::create(path).map_err(|source| BuildError::Create {
path: path.to_path_buf(),
source,
})?;
Ok(Self {
path: path.to_path_buf(),
db,
system: system.to_owned(),
version: version.to_owned(),
vocabularies: BTreeMap::new(),
codes: BTreeMap::new(),
concepts: BTreeMap::new(),
designations: BTreeMap::new(),
acceptability: BTreeMap::new(),
designation_uses: BTreeMap::new(),
properties: BTreeMap::new(),
})
}
pub fn vocabulary(
&mut self,
kind: crate::store::Vocabulary,
ordinal: u32,
name: &str,
) -> Result<(), BuildError> {
let table_name = match kind {
crate::store::Vocabulary::PropertyKeys => tables::PROPERTY_KEYS.name(),
crate::store::Vocabulary::DesignationUses => tables::DESIGNATION_USES.name(),
crate::store::Vocabulary::LanguageRefsets => tables::LANGUAGE_REFSETS.name(),
crate::store::Vocabulary::Acceptabilities => tables::ACCEPTABILITIES.name(),
};
let entries = self.vocabularies.entry(table_name).or_default();
if let Some((existing, _)) = entries
.iter()
.find(|(key, value)| value.as_str() == name && **key != ordinal)
{
return Err(BuildError::Vocabulary {
kind: table_name.to_owned(),
name: name.to_owned(),
existing: *existing,
requested: ordinal,
});
}
entries.insert(ordinal, name.to_owned());
Ok(())
}
#[expect(
clippy::unnecessary_wraps,
reason = "the build seam is fallible by contract: a full buffer fails here"
)]
pub fn concept(&mut self, ordinal: Ordinal, concept: &Concept) -> Result<(), BuildError> {
self.codes.insert(concept.code.clone(), ordinal.index());
self.concepts.insert(ordinal.index(), concept.encode());
Ok(())
}
#[expect(
clippy::unnecessary_wraps,
reason = "the build seam is fallible by contract: a full buffer fails here"
)]
pub fn designation(
&mut self,
ordinal: Ordinal,
index: u32,
designation: &Designation,
) -> Result<(), BuildError> {
self.designations
.insert((ordinal.index(), index), designation.encode());
self.designation_uses
.insert((ordinal.index(), index), designation.use_ordinal);
Ok(())
}
#[expect(
clippy::unnecessary_wraps,
reason = "the build seam is fallible by contract: a full buffer fails here"
)]
pub fn acceptability(
&mut self,
ordinal: Ordinal,
index: u32,
language_refset: u32,
acceptability: u32,
) -> Result<(), BuildError> {
self.acceptability
.insert((ordinal.index(), index, language_refset), acceptability);
Ok(())
}
#[expect(
clippy::unnecessary_wraps,
reason = "the build seam is fallible by contract: a full buffer fails here"
)]
pub fn properties(
&mut self,
ordinal: Ordinal,
key: u32,
values: &[PropertyValue],
) -> Result<(), BuildError> {
self.properties
.insert((ordinal.index(), key), PropertyValue::encode_list(values));
Ok(())
}
fn chosen_preferred(&self, rule: &PreferredRule) -> BTreeMap<(u32, u32, u32), u32> {
let mut chosen: BTreeMap<(u32, u32, u32), u32> = BTreeMap::new();
for ((concept, index, refset), acceptability) in &self.acceptability {
if *acceptability != rule.preferred {
continue;
}
let Some(use_ordinal) = self.designation_uses.get(&(*concept, *index)) else {
continue;
};
chosen
.entry((*concept, *refset, *use_ordinal))
.and_modify(|existing| *existing = (*existing).min(*index))
.or_insert(*index);
}
chosen
}
fn display_rows(
&self,
chosen: &BTreeMap<(u32, u32, u32), u32>,
display_use: u32,
) -> BTreeMap<u32, Vec<u8>> {
let mut by_concept: BTreeMap<u32, Vec<(u32, String, String)>> = BTreeMap::new();
for ((concept, refset, use_ordinal), index) in chosen {
if *use_ordinal != display_use {
continue;
}
let Some(bytes) = self.designations.get(&(*concept, *index)) else {
continue;
};
let Ok(designation) = Designation::decode(bytes) else {
continue;
};
by_concept.entry(*concept).or_default().push((
*refset,
designation.language,
designation.term,
));
}
by_concept
.into_iter()
.map(|(concept, entries)| {
let borrowed: Vec<(u32, &str, &str)> = entries
.iter()
.map(|(refset, language, term)| (*refset, language.as_str(), term.as_str()))
.collect();
(concept, record::Displays::encode(&borrowed))
})
.collect()
}
fn per_concept<'a, K: 'a, V: 'a, R>(
rows: &'a BTreeMap<K, V>,
concept: impl Fn(&K) -> u32,
row: impl Fn(&'a K, &'a V) -> R,
mut write: impl FnMut(u32, &[R]) -> Result<(), BuildError>,
) -> Result<(), BuildError> {
let mut at = None;
let mut group: Vec<R> = Vec::new();
for (key, value) in rows {
let ordinal = concept(key);
if at != Some(ordinal) {
if let Some(previous) = at {
write(previous, &group)?;
}
at = Some(ordinal);
group.clear();
}
group.push(row(key, value));
}
match at {
Some(previous) => write(previous, &group),
None => Ok(()),
}
}
fn column_rows<'a, K: 'a, V: 'a, R>(
rows: &'a BTreeMap<K, V>,
concept: impl Fn(&K) -> u32,
row: impl Fn(&'a K, &'a V) -> R,
pack: impl Fn(&[R]) -> Vec<u8>,
) -> Result<BTreeMap<u32, Vec<u8>>, BuildError> {
let mut packed: BTreeMap<u32, Vec<u8>> = BTreeMap::new();
Self::per_concept(rows, concept, row, |ordinal, group| {
packed.insert(ordinal, pack(group));
Ok(())
})?;
Ok(packed)
}
fn write_column(
&self,
name: &str,
count: u32,
rows: &BTreeMap<u32, Vec<u8>>,
) -> Result<(), BuildError> {
let path = Column::file(&self.path, name);
let failed = |source| BuildError::Io {
path: path.clone(),
source,
};
let file = std::fs::File::create(&path).map_err(failed)?;
let mut out = BufWriter::with_capacity(WRITE_BUFFER, file);
Column::write_to(
count,
rows.iter()
.map(|(ordinal, bytes)| (Ordinal::new(*ordinal), bytes.as_slice())),
&mut out,
)
.map_err(failed)?;
out.flush().map_err(failed)
}
pub fn finish(self, rule: &PreferredRule) -> Result<PathBuf, BuildError> {
let txn = self.db.begin_write()?;
{
let mut meta = txn.open_table(tables::META)?;
meta.insert(tables::META_LAYOUT, tables::LAYOUT_VERSION)?;
meta.insert(tables::META_SYSTEM, self.system.as_str())?;
meta.insert(tables::META_VERSION, self.version.as_str())?;
meta.insert(
tables::META_CONCEPTS,
self.concepts.len().to_string().as_str(),
)?;
meta.insert(tables::META_PREFERRED, rule.preferred.to_string().as_str())?;
}
{
let mut codes = txn.open_table(tables::CODES)?;
for (code, ordinal) in &self.codes {
codes.insert(code.as_str(), *ordinal)?;
}
}
let count = self
.concepts
.keys()
.next_back()
.map_or(0, |highest| highest.saturating_add(1));
self.write_column(tables::COLUMN_CONCEPTS, count, &self.concepts)?;
self.write_column(
tables::COLUMN_ACCEPTABILITY,
count,
&Self::column_rows(
&self.acceptability,
|(concept, _, _)| *concept,
|(_, index, refset), acceptability| (*index, *refset, *acceptability),
record::Acceptability::encode,
)?,
)?;
self.write_column(
tables::COLUMN_PROPERTIES,
count,
&Self::column_rows(
&self.properties,
|(concept, _)| *concept,
|(_, key), values| (*key, values.as_slice()),
record::Properties::encode,
)?,
)?;
let displays = rule.display_use.map_or_else(BTreeMap::new, |display_use| {
self.display_rows(&self.chosen_preferred(rule), display_use)
});
self.write_column(tables::COLUMN_DISPLAYS, count, &displays)?;
self.write_designations(&txn)?;
for table_def in [
tables::PROPERTY_KEYS,
tables::DESIGNATION_USES,
tables::LANGUAGE_REFSETS,
tables::ACCEPTABILITIES,
] {
let mut table = txn.open_table(table_def)?;
if let Some(entries) = self.vocabularies.get(table_def.name()) {
for (ordinal, name) in entries {
table.insert(*ordinal, name.as_str())?;
}
}
}
txn.commit()?;
let mut db = self.db;
while db.compact()? {}
drop(db);
Ok(self.path)
}
fn write_designations(&self, txn: &redb::WriteTransaction) -> Result<(), BuildError> {
let mut designations = txn.open_table(tables::DESIGNATIONS)?;
Self::per_concept(
&self.designations,
|(concept, _)| *concept,
|(_, index), bytes| (*index, bytes.as_slice()),
|ordinal, group| {
designations.insert(ordinal, record::Designations::encode(group).as_slice())?;
Ok(())
},
)
}
}