1use std::collections::BTreeMap;
12use std::io::{BufWriter, Write};
13use std::path::{Path, PathBuf};
14
15use concept_graph::ordinal::Ordinal;
16use redb::{Database, TableHandle};
17
18use crate::column::Column;
19use crate::record::{self, Concept, Designation, PropertyValue};
20use crate::tables;
21
22const WRITE_BUFFER: usize = 1 << 20;
27
28#[derive(Debug, thiserror::Error)]
30pub enum BuildError {
31 #[error("cannot create the artifact at {path}")]
33 Create {
34 path: PathBuf,
36 #[source]
38 source: redb::DatabaseError,
39 },
40 #[error("transaction failed")]
42 Transaction(#[from] redb::TransactionError),
43 #[error("cannot open table")]
45 Table(#[from] redb::TableError),
46 #[error("storage write failed")]
48 Storage(#[from] redb::StorageError),
49 #[error("commit failed")]
51 Commit(#[from] redb::CommitError),
52 #[error("compaction failed")]
54 Compaction(#[from] redb::CompactionError),
55 #[error("cannot write {path}")]
57 Io {
58 path: PathBuf,
60 #[source]
62 source: std::io::Error,
63 },
64 #[error("{kind} {name:?} is already ordinal {existing}, not {requested}")]
66 Vocabulary {
67 kind: String,
69 name: String,
71 existing: u32,
73 requested: u32,
75 },
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PreferredRule {
83 pub preferred: u32,
85 pub display_use: Option<u32>,
91}
92
93pub struct StoreBuilder {
100 path: PathBuf,
101 db: Database,
102 system: String,
103 version: String,
104 vocabularies: BTreeMap<&'static str, BTreeMap<u32, String>>,
105 codes: BTreeMap<String, u32>,
106 concepts: BTreeMap<u32, Vec<u8>>,
107 designations: BTreeMap<(u32, u32), Vec<u8>>,
108 acceptability: BTreeMap<(u32, u32, u32), u32>,
109 designation_uses: BTreeMap<(u32, u32), u32>,
110 properties: BTreeMap<(u32, u32), Vec<u8>>,
111}
112
113impl std::fmt::Debug for StoreBuilder {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("StoreBuilder")
116 .field("path", &self.path)
117 .field("concepts", &self.concepts.len())
118 .field("designations", &self.designations.len())
119 .finish_non_exhaustive()
120 }
121}
122
123impl StoreBuilder {
124 pub fn create(path: &Path, system: &str, version: &str) -> Result<Self, BuildError> {
131 if path.exists() {
132 std::fs::remove_file(path).map_err(|source| BuildError::Create {
133 path: path.to_path_buf(),
134 source: redb::DatabaseError::Storage(redb::StorageError::Io(source)),
135 })?;
136 }
137 let db = Database::create(path).map_err(|source| BuildError::Create {
138 path: path.to_path_buf(),
139 source,
140 })?;
141 Ok(Self {
142 path: path.to_path_buf(),
143 db,
144 system: system.to_owned(),
145 version: version.to_owned(),
146 vocabularies: BTreeMap::new(),
147 codes: BTreeMap::new(),
148 concepts: BTreeMap::new(),
149 designations: BTreeMap::new(),
150 acceptability: BTreeMap::new(),
151 designation_uses: BTreeMap::new(),
152 properties: BTreeMap::new(),
153 })
154 }
155
156 pub fn vocabulary(
163 &mut self,
164 kind: crate::store::Vocabulary,
165 ordinal: u32,
166 name: &str,
167 ) -> Result<(), BuildError> {
168 let table_name = match kind {
169 crate::store::Vocabulary::PropertyKeys => tables::PROPERTY_KEYS.name(),
170 crate::store::Vocabulary::DesignationUses => tables::DESIGNATION_USES.name(),
171 crate::store::Vocabulary::LanguageRefsets => tables::LANGUAGE_REFSETS.name(),
172 crate::store::Vocabulary::Acceptabilities => tables::ACCEPTABILITIES.name(),
173 };
174 let entries = self.vocabularies.entry(table_name).or_default();
175 if let Some((existing, _)) = entries
176 .iter()
177 .find(|(key, value)| value.as_str() == name && **key != ordinal)
178 {
179 return Err(BuildError::Vocabulary {
180 kind: table_name.to_owned(),
181 name: name.to_owned(),
182 existing: *existing,
183 requested: ordinal,
184 });
185 }
186 entries.insert(ordinal, name.to_owned());
187 Ok(())
188 }
189
190 #[expect(
196 clippy::unnecessary_wraps,
197 reason = "the build seam is fallible by contract: a full buffer fails here"
198 )]
199 pub fn concept(&mut self, ordinal: Ordinal, concept: &Concept) -> Result<(), BuildError> {
200 self.codes.insert(concept.code.clone(), ordinal.index());
201 self.concepts.insert(ordinal.index(), concept.encode());
202 Ok(())
203 }
204
205 #[expect(
211 clippy::unnecessary_wraps,
212 reason = "the build seam is fallible by contract: a full buffer fails here"
213 )]
214 pub fn designation(
215 &mut self,
216 ordinal: Ordinal,
217 index: u32,
218 designation: &Designation,
219 ) -> Result<(), BuildError> {
220 self.designations
221 .insert((ordinal.index(), index), designation.encode());
222 self.designation_uses
223 .insert((ordinal.index(), index), designation.use_ordinal);
224 Ok(())
225 }
226
227 #[expect(
233 clippy::unnecessary_wraps,
234 reason = "the build seam is fallible by contract: a full buffer fails here"
235 )]
236 pub fn acceptability(
237 &mut self,
238 ordinal: Ordinal,
239 index: u32,
240 language_refset: u32,
241 acceptability: u32,
242 ) -> Result<(), BuildError> {
243 self.acceptability
244 .insert((ordinal.index(), index, language_refset), acceptability);
245 Ok(())
246 }
247
248 #[expect(
254 clippy::unnecessary_wraps,
255 reason = "the build seam is fallible by contract: a full buffer fails here"
256 )]
257 pub fn properties(
258 &mut self,
259 ordinal: Ordinal,
260 key: u32,
261 values: &[PropertyValue],
262 ) -> Result<(), BuildError> {
263 self.properties
264 .insert((ordinal.index(), key), PropertyValue::encode_list(values));
265 Ok(())
266 }
267
268 fn chosen_preferred(&self, rule: &PreferredRule) -> BTreeMap<(u32, u32, u32), u32> {
271 let mut chosen: BTreeMap<(u32, u32, u32), u32> = BTreeMap::new();
272 for ((concept, index, refset), acceptability) in &self.acceptability {
273 if *acceptability != rule.preferred {
274 continue;
275 }
276 let Some(use_ordinal) = self.designation_uses.get(&(*concept, *index)) else {
277 continue;
278 };
279 chosen
280 .entry((*concept, *refset, *use_ordinal))
281 .and_modify(|existing| *existing = (*existing).min(*index))
282 .or_insert(*index);
283 }
284 chosen
285 }
286
287 fn display_rows(
292 &self,
293 chosen: &BTreeMap<(u32, u32, u32), u32>,
294 display_use: u32,
295 ) -> BTreeMap<u32, Vec<u8>> {
296 let mut by_concept: BTreeMap<u32, Vec<(u32, String, String)>> = BTreeMap::new();
297 for ((concept, refset, use_ordinal), index) in chosen {
298 if *use_ordinal != display_use {
299 continue;
300 }
301 let Some(bytes) = self.designations.get(&(*concept, *index)) else {
302 continue;
303 };
304 let Ok(designation) = Designation::decode(bytes) else {
305 continue;
306 };
307 by_concept.entry(*concept).or_default().push((
308 *refset,
309 designation.language,
310 designation.term,
311 ));
312 }
313 by_concept
314 .into_iter()
315 .map(|(concept, entries)| {
316 let borrowed: Vec<(u32, &str, &str)> = entries
317 .iter()
318 .map(|(refset, language, term)| (*refset, language.as_str(), term.as_str()))
319 .collect();
320 (concept, record::Displays::encode(&borrowed))
321 })
322 .collect()
323 }
324
325 fn per_concept<'a, K: 'a, V: 'a, R>(
332 rows: &'a BTreeMap<K, V>,
333 concept: impl Fn(&K) -> u32,
334 row: impl Fn(&'a K, &'a V) -> R,
335 mut write: impl FnMut(u32, &[R]) -> Result<(), BuildError>,
336 ) -> Result<(), BuildError> {
337 let mut at = None;
338 let mut group: Vec<R> = Vec::new();
339 for (key, value) in rows {
340 let ordinal = concept(key);
341 if at != Some(ordinal) {
342 if let Some(previous) = at {
343 write(previous, &group)?;
344 }
345 at = Some(ordinal);
346 group.clear();
347 }
348 group.push(row(key, value));
349 }
350 match at {
351 Some(previous) => write(previous, &group),
352 None => Ok(()),
353 }
354 }
355
356 fn column_rows<'a, K: 'a, V: 'a, R>(
358 rows: &'a BTreeMap<K, V>,
359 concept: impl Fn(&K) -> u32,
360 row: impl Fn(&'a K, &'a V) -> R,
361 pack: impl Fn(&[R]) -> Vec<u8>,
362 ) -> Result<BTreeMap<u32, Vec<u8>>, BuildError> {
363 let mut packed: BTreeMap<u32, Vec<u8>> = BTreeMap::new();
364 Self::per_concept(rows, concept, row, |ordinal, group| {
365 packed.insert(ordinal, pack(group));
366 Ok(())
367 })?;
368 Ok(packed)
369 }
370
371 fn write_column(
377 &self,
378 name: &str,
379 count: u32,
380 rows: &BTreeMap<u32, Vec<u8>>,
381 ) -> Result<(), BuildError> {
382 let path = Column::file(&self.path, name);
383 let failed = |source| BuildError::Io {
384 path: path.clone(),
385 source,
386 };
387 let file = std::fs::File::create(&path).map_err(failed)?;
388 let mut out = BufWriter::with_capacity(WRITE_BUFFER, file);
389 Column::write_to(
390 count,
391 rows.iter()
392 .map(|(ordinal, bytes)| (Ordinal::new(*ordinal), bytes.as_slice())),
393 &mut out,
394 )
395 .map_err(failed)?;
396 out.flush().map_err(failed)
397 }
398
399 pub fn finish(self, rule: &PreferredRule) -> Result<PathBuf, BuildError> {
407 let txn = self.db.begin_write()?;
408 {
409 let mut meta = txn.open_table(tables::META)?;
410 meta.insert(tables::META_LAYOUT, tables::LAYOUT_VERSION)?;
411 meta.insert(tables::META_SYSTEM, self.system.as_str())?;
412 meta.insert(tables::META_VERSION, self.version.as_str())?;
413 meta.insert(
414 tables::META_CONCEPTS,
415 self.concepts.len().to_string().as_str(),
416 )?;
417 meta.insert(tables::META_PREFERRED, rule.preferred.to_string().as_str())?;
418 }
419 {
420 let mut codes = txn.open_table(tables::CODES)?;
421 for (code, ordinal) in &self.codes {
422 codes.insert(code.as_str(), *ordinal)?;
423 }
424 }
425 let count = self
428 .concepts
429 .keys()
430 .next_back()
431 .map_or(0, |highest| highest.saturating_add(1));
432 self.write_column(tables::COLUMN_CONCEPTS, count, &self.concepts)?;
435 self.write_column(
436 tables::COLUMN_ACCEPTABILITY,
437 count,
438 &Self::column_rows(
439 &self.acceptability,
440 |(concept, _, _)| *concept,
441 |(_, index, refset), acceptability| (*index, *refset, *acceptability),
442 record::Acceptability::encode,
443 )?,
444 )?;
445 self.write_column(
446 tables::COLUMN_PROPERTIES,
447 count,
448 &Self::column_rows(
449 &self.properties,
450 |(concept, _)| *concept,
451 |(_, key), values| (*key, values.as_slice()),
452 record::Properties::encode,
453 )?,
454 )?;
455 let displays = rule.display_use.map_or_else(BTreeMap::new, |display_use| {
458 self.display_rows(&self.chosen_preferred(rule), display_use)
459 });
460 self.write_column(tables::COLUMN_DISPLAYS, count, &displays)?;
461 self.write_designations(&txn)?;
462 for table_def in [
463 tables::PROPERTY_KEYS,
464 tables::DESIGNATION_USES,
465 tables::LANGUAGE_REFSETS,
466 tables::ACCEPTABILITIES,
467 ] {
468 let mut table = txn.open_table(table_def)?;
469 if let Some(entries) = self.vocabularies.get(table_def.name()) {
470 for (ordinal, name) in entries {
471 table.insert(*ordinal, name.as_str())?;
472 }
473 }
474 }
475 txn.commit()?;
476 let mut db = self.db;
480 while db.compact()? {}
481 drop(db);
482 Ok(self.path)
483 }
484
485 fn write_designations(&self, txn: &redb::WriteTransaction) -> Result<(), BuildError> {
491 let mut designations = txn.open_table(tables::DESIGNATIONS)?;
492 Self::per_concept(
493 &self.designations,
494 |(concept, _)| *concept,
495 |(_, index), bytes| (*index, bytes.as_slice()),
496 |ordinal, group| {
497 designations.insert(ordinal, record::Designations::encode(group).as_slice())?;
498 Ok(())
499 },
500 )
501 }
502}