1use std::collections::BTreeMap;
9use std::path::{Path, PathBuf};
10
11use concept_graph::ordinal::Ordinal;
12use redb::{Database, TableHandle};
13
14use crate::column::Column;
15use crate::record::{self, Concept, Designation, PropertyValue};
16use crate::tables;
17
18#[derive(Debug, thiserror::Error)]
20pub enum BuildError {
21 #[error("cannot create the artifact at {path}")]
23 Create {
24 path: PathBuf,
26 #[source]
28 source: redb::DatabaseError,
29 },
30 #[error("transaction failed")]
32 Transaction(#[from] redb::TransactionError),
33 #[error("cannot open table")]
35 Table(#[from] redb::TableError),
36 #[error("storage write failed")]
38 Storage(#[from] redb::StorageError),
39 #[error("commit failed")]
41 Commit(#[from] redb::CommitError),
42 #[error("compaction failed")]
44 Compaction(#[from] redb::CompactionError),
45 #[error("{kind} {name:?} is already ordinal {existing}, not {requested}")]
47 Vocabulary {
48 kind: String,
50 name: String,
52 existing: u32,
54 requested: u32,
56 },
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct PreferredRule {
64 pub preferred: u32,
66 pub display_use: Option<u32>,
72}
73
74pub struct StoreBuilder {
81 path: PathBuf,
82 db: Database,
83 system: String,
84 version: String,
85 vocabularies: BTreeMap<&'static str, BTreeMap<u32, String>>,
86 codes: BTreeMap<String, u32>,
87 concepts: BTreeMap<u32, Vec<u8>>,
88 designations: BTreeMap<(u32, u32), Vec<u8>>,
89 acceptability: BTreeMap<(u32, u32, u32), u32>,
90 designation_uses: BTreeMap<(u32, u32), u32>,
91 properties: BTreeMap<(u32, u32), Vec<u8>>,
92}
93
94impl std::fmt::Debug for StoreBuilder {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("StoreBuilder")
97 .field("path", &self.path)
98 .field("concepts", &self.concepts.len())
99 .field("designations", &self.designations.len())
100 .finish_non_exhaustive()
101 }
102}
103
104impl StoreBuilder {
105 pub fn create(path: &Path, system: &str, version: &str) -> Result<Self, BuildError> {
112 if path.exists() {
113 std::fs::remove_file(path).map_err(|source| BuildError::Create {
114 path: path.to_path_buf(),
115 source: redb::DatabaseError::Storage(redb::StorageError::Io(source)),
116 })?;
117 }
118 let db = Database::create(path).map_err(|source| BuildError::Create {
119 path: path.to_path_buf(),
120 source,
121 })?;
122 Ok(Self {
123 path: path.to_path_buf(),
124 db,
125 system: system.to_owned(),
126 version: version.to_owned(),
127 vocabularies: BTreeMap::new(),
128 codes: BTreeMap::new(),
129 concepts: BTreeMap::new(),
130 designations: BTreeMap::new(),
131 acceptability: BTreeMap::new(),
132 designation_uses: BTreeMap::new(),
133 properties: BTreeMap::new(),
134 })
135 }
136
137 pub fn vocabulary(
144 &mut self,
145 kind: crate::store::Vocabulary,
146 ordinal: u32,
147 name: &str,
148 ) -> Result<(), BuildError> {
149 let table_name = match kind {
150 crate::store::Vocabulary::PropertyKeys => tables::PROPERTY_KEYS.name(),
151 crate::store::Vocabulary::DesignationUses => tables::DESIGNATION_USES.name(),
152 crate::store::Vocabulary::LanguageRefsets => tables::LANGUAGE_REFSETS.name(),
153 crate::store::Vocabulary::Acceptabilities => tables::ACCEPTABILITIES.name(),
154 };
155 let entries = self.vocabularies.entry(table_name).or_default();
156 if let Some((existing, _)) = entries
157 .iter()
158 .find(|(key, value)| value.as_str() == name && **key != ordinal)
159 {
160 return Err(BuildError::Vocabulary {
161 kind: table_name.to_owned(),
162 name: name.to_owned(),
163 existing: *existing,
164 requested: ordinal,
165 });
166 }
167 entries.insert(ordinal, name.to_owned());
168 Ok(())
169 }
170
171 #[expect(
177 clippy::unnecessary_wraps,
178 reason = "the build seam is fallible by contract: a full buffer fails here"
179 )]
180 pub fn concept(&mut self, ordinal: Ordinal, concept: &Concept) -> Result<(), BuildError> {
181 self.codes.insert(concept.code.clone(), ordinal.index());
182 self.concepts.insert(ordinal.index(), concept.encode());
183 Ok(())
184 }
185
186 #[expect(
192 clippy::unnecessary_wraps,
193 reason = "the build seam is fallible by contract: a full buffer fails here"
194 )]
195 pub fn designation(
196 &mut self,
197 ordinal: Ordinal,
198 index: u32,
199 designation: &Designation,
200 ) -> Result<(), BuildError> {
201 self.designations
202 .insert((ordinal.index(), index), designation.encode());
203 self.designation_uses
204 .insert((ordinal.index(), index), designation.use_ordinal);
205 Ok(())
206 }
207
208 #[expect(
214 clippy::unnecessary_wraps,
215 reason = "the build seam is fallible by contract: a full buffer fails here"
216 )]
217 pub fn acceptability(
218 &mut self,
219 ordinal: Ordinal,
220 index: u32,
221 language_refset: u32,
222 acceptability: u32,
223 ) -> Result<(), BuildError> {
224 self.acceptability
225 .insert((ordinal.index(), index, language_refset), acceptability);
226 Ok(())
227 }
228
229 #[expect(
235 clippy::unnecessary_wraps,
236 reason = "the build seam is fallible by contract: a full buffer fails here"
237 )]
238 pub fn properties(
239 &mut self,
240 ordinal: Ordinal,
241 key: u32,
242 values: &[PropertyValue],
243 ) -> Result<(), BuildError> {
244 self.properties
245 .insert((ordinal.index(), key), PropertyValue::encode_list(values));
246 Ok(())
247 }
248
249 fn chosen_preferred(&self, rule: &PreferredRule) -> BTreeMap<(u32, u32, u32), u32> {
252 let mut chosen: BTreeMap<(u32, u32, u32), u32> = BTreeMap::new();
253 for ((concept, index, refset), acceptability) in &self.acceptability {
254 if *acceptability != rule.preferred {
255 continue;
256 }
257 let Some(use_ordinal) = self.designation_uses.get(&(*concept, *index)) else {
258 continue;
259 };
260 chosen
261 .entry((*concept, *refset, *use_ordinal))
262 .and_modify(|existing| *existing = (*existing).min(*index))
263 .or_insert(*index);
264 }
265 chosen
266 }
267
268 fn display_column(
273 &self,
274 chosen: &BTreeMap<(u32, u32, u32), u32>,
275 display_use: u32,
276 count: u32,
277 ) -> Vec<u8> {
278 let mut by_concept: BTreeMap<u32, Vec<(u32, String, String)>> = BTreeMap::new();
279 for ((concept, refset, use_ordinal), index) in chosen {
280 if *use_ordinal != display_use {
281 continue;
282 }
283 let Some(bytes) = self.designations.get(&(*concept, *index)) else {
284 continue;
285 };
286 let Ok(designation) = Designation::decode(bytes) else {
287 continue;
288 };
289 by_concept.entry(*concept).or_default().push((
290 *refset,
291 designation.language,
292 designation.term,
293 ));
294 }
295 let packed: BTreeMap<u32, Vec<u8>> = by_concept
296 .into_iter()
297 .map(|(concept, entries)| {
298 let borrowed: Vec<(u32, &str, &str)> = entries
299 .iter()
300 .map(|(refset, language, term)| (*refset, language.as_str(), term.as_str()))
301 .collect();
302 (concept, record::Displays::encode(&borrowed))
303 })
304 .collect();
305 Column::pack(
306 count,
307 packed
308 .iter()
309 .map(|(concept, bytes)| (Ordinal::new(*concept), bytes.as_slice())),
310 )
311 }
312
313 fn per_concept<'a, K: 'a, V: 'a, R>(
320 rows: &'a BTreeMap<K, V>,
321 concept: impl Fn(&K) -> u32,
322 row: impl Fn(&'a K, &'a V) -> R,
323 mut write: impl FnMut(u32, &[R]) -> Result<(), BuildError>,
324 ) -> Result<(), BuildError> {
325 let mut at = None;
326 let mut group: Vec<R> = Vec::new();
327 for (key, value) in rows {
328 let ordinal = concept(key);
329 if at != Some(ordinal) {
330 if let Some(previous) = at {
331 write(previous, &group)?;
332 }
333 at = Some(ordinal);
334 group.clear();
335 }
336 group.push(row(key, value));
337 }
338 match at {
339 Some(previous) => write(previous, &group),
340 None => Ok(()),
341 }
342 }
343
344 fn column<'a, K: 'a, V: 'a, R>(
346 count: u32,
347 rows: &'a BTreeMap<K, V>,
348 concept: impl Fn(&K) -> u32,
349 row: impl Fn(&'a K, &'a V) -> R,
350 pack: impl Fn(&[R]) -> Vec<u8>,
351 ) -> Result<Vec<u8>, BuildError> {
352 let mut packed: BTreeMap<u32, Vec<u8>> = BTreeMap::new();
353 Self::per_concept(rows, concept, row, |ordinal, group| {
354 packed.insert(ordinal, pack(group));
355 Ok(())
356 })?;
357 Ok(Column::pack(
358 count,
359 packed
360 .iter()
361 .map(|(ordinal, bytes)| (Ordinal::new(*ordinal), bytes.as_slice())),
362 ))
363 }
364
365 pub fn finish(self, rule: &PreferredRule) -> Result<PathBuf, BuildError> {
373 let txn = self.db.begin_write()?;
374 {
375 let mut meta = txn.open_table(tables::META)?;
376 meta.insert(tables::META_LAYOUT, tables::LAYOUT_VERSION)?;
377 meta.insert(tables::META_SYSTEM, self.system.as_str())?;
378 meta.insert(tables::META_VERSION, self.version.as_str())?;
379 meta.insert(
380 tables::META_CONCEPTS,
381 self.concepts.len().to_string().as_str(),
382 )?;
383 meta.insert(tables::META_PREFERRED, rule.preferred.to_string().as_str())?;
384 }
385 {
386 let mut codes = txn.open_table(tables::CODES)?;
387 for (code, ordinal) in &self.codes {
388 codes.insert(code.as_str(), *ordinal)?;
389 }
390 }
391 let count = self
394 .concepts
395 .keys()
396 .next_back()
397 .map_or(0, |highest| highest.saturating_add(1));
398 {
399 let mut columns = txn.open_table(tables::COLUMNS)?;
400 let concepts = Column::pack(
403 count,
404 self.concepts
405 .iter()
406 .map(|(ordinal, bytes)| (Ordinal::new(*ordinal), bytes.as_slice())),
407 );
408 columns.insert(tables::COLUMN_CONCEPTS, concepts.as_slice())?;
409 let acceptability = Self::column(
410 count,
411 &self.acceptability,
412 |(concept, _, _)| *concept,
413 |(_, index, refset), acceptability| (*index, *refset, *acceptability),
414 record::Acceptability::encode,
415 )?;
416 columns.insert(tables::COLUMN_ACCEPTABILITY, acceptability.as_slice())?;
417 let properties = Self::column(
418 count,
419 &self.properties,
420 |(concept, _)| *concept,
421 |(_, key), values| (*key, values.as_slice()),
422 record::Properties::encode,
423 )?;
424 columns.insert(tables::COLUMN_PROPERTIES, properties.as_slice())?;
425 }
426 self.write_designations(&txn)?;
427 for table_def in [
428 tables::PROPERTY_KEYS,
429 tables::DESIGNATION_USES,
430 tables::LANGUAGE_REFSETS,
431 tables::ACCEPTABILITIES,
432 ] {
433 let mut table = txn.open_table(table_def)?;
434 if let Some(entries) = self.vocabularies.get(table_def.name()) {
435 for (ordinal, name) in entries {
436 table.insert(*ordinal, name.as_str())?;
437 }
438 }
439 }
440 if let Some(display_use) = rule.display_use {
441 let chosen = self.chosen_preferred(rule);
442 let displays = self.display_column(&chosen, display_use, count);
443 let mut columns = txn.open_table(tables::COLUMNS)?;
444 columns.insert(tables::COLUMN_DISPLAYS, displays.as_slice())?;
445 }
446 txn.commit()?;
447 let mut db = self.db;
451 while db.compact()? {}
452 drop(db);
453 Ok(self.path)
454 }
455
456 fn write_designations(&self, txn: &redb::WriteTransaction) -> Result<(), BuildError> {
462 let mut designations = txn.open_table(tables::DESIGNATIONS)?;
463 Self::per_concept(
464 &self.designations,
465 |(concept, _)| *concept,
466 |(_, index), bytes| (*index, bytes.as_slice()),
467 |ordinal, group| {
468 designations.insert(ordinal, record::Designations::encode(group).as_slice())?;
469 Ok(())
470 },
471 )
472 }
473}