1use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10use concept_graph::ordinal::Ordinal;
11use redb::{Database, TableHandle};
12
13use crate::record::{Concept, Designation, PropertyValue};
14use crate::tables;
15
16#[derive(Debug, thiserror::Error)]
18pub enum BuildError {
19 #[error("cannot create the artifact at {path}")]
21 Create {
22 path: PathBuf,
24 #[source]
26 source: redb::DatabaseError,
27 },
28 #[error("transaction failed")]
30 Transaction(#[from] redb::TransactionError),
31 #[error("cannot open table")]
33 Table(#[from] redb::TableError),
34 #[error("storage write failed")]
36 Storage(#[from] redb::StorageError),
37 #[error("commit failed")]
39 Commit(#[from] redb::CommitError),
40 #[error("compaction failed")]
42 Compaction(#[from] redb::CompactionError),
43 #[error("{kind} {name:?} is already ordinal {existing}, not {requested}")]
45 Vocabulary {
46 kind: String,
48 name: String,
50 existing: u32,
52 requested: u32,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct PreferredRule {
62 pub preferred: u32,
64}
65
66pub struct StoreBuilder {
73 path: PathBuf,
74 db: Database,
75 system: String,
76 version: String,
77 vocabularies: BTreeMap<&'static str, BTreeMap<u32, String>>,
78 codes: BTreeMap<String, u32>,
79 concepts: BTreeMap<u32, Vec<u8>>,
80 designations: BTreeMap<(u32, u32), Vec<u8>>,
81 acceptability: BTreeMap<(u32, u32, u32), u32>,
82 designation_uses: BTreeMap<(u32, u32), u32>,
83 properties: BTreeMap<(u32, u32), Vec<u8>>,
84}
85
86impl std::fmt::Debug for StoreBuilder {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("StoreBuilder")
89 .field("path", &self.path)
90 .field("concepts", &self.concepts.len())
91 .field("designations", &self.designations.len())
92 .finish_non_exhaustive()
93 }
94}
95
96impl StoreBuilder {
97 pub fn create(path: &Path, system: &str, version: &str) -> Result<Self, BuildError> {
104 if path.exists() {
105 std::fs::remove_file(path).map_err(|source| BuildError::Create {
106 path: path.to_path_buf(),
107 source: redb::DatabaseError::Storage(redb::StorageError::Io(source)),
108 })?;
109 }
110 let db = Database::create(path).map_err(|source| BuildError::Create {
111 path: path.to_path_buf(),
112 source,
113 })?;
114 Ok(Self {
115 path: path.to_path_buf(),
116 db,
117 system: system.to_owned(),
118 version: version.to_owned(),
119 vocabularies: BTreeMap::new(),
120 codes: BTreeMap::new(),
121 concepts: BTreeMap::new(),
122 designations: BTreeMap::new(),
123 acceptability: BTreeMap::new(),
124 designation_uses: BTreeMap::new(),
125 properties: BTreeMap::new(),
126 })
127 }
128
129 pub fn vocabulary(
136 &mut self,
137 kind: crate::store::Vocabulary,
138 ordinal: u32,
139 name: &str,
140 ) -> Result<(), BuildError> {
141 let table_name = match kind {
142 crate::store::Vocabulary::PropertyKeys => tables::PROPERTY_KEYS.name(),
143 crate::store::Vocabulary::DesignationUses => tables::DESIGNATION_USES.name(),
144 crate::store::Vocabulary::LanguageRefsets => tables::LANGUAGE_REFSETS.name(),
145 crate::store::Vocabulary::Acceptabilities => tables::ACCEPTABILITIES.name(),
146 };
147 let entries = self.vocabularies.entry(table_name).or_default();
148 if let Some((existing, _)) = entries
149 .iter()
150 .find(|(key, value)| value.as_str() == name && **key != ordinal)
151 {
152 return Err(BuildError::Vocabulary {
153 kind: table_name.to_owned(),
154 name: name.to_owned(),
155 existing: *existing,
156 requested: ordinal,
157 });
158 }
159 entries.insert(ordinal, name.to_owned());
160 Ok(())
161 }
162
163 #[expect(
169 clippy::unnecessary_wraps,
170 reason = "the build seam is fallible by contract: a full buffer fails here"
171 )]
172 pub fn concept(&mut self, ordinal: Ordinal, concept: &Concept) -> Result<(), BuildError> {
173 self.codes.insert(concept.code.clone(), ordinal.index());
174 self.concepts.insert(ordinal.index(), concept.encode());
175 Ok(())
176 }
177
178 #[expect(
184 clippy::unnecessary_wraps,
185 reason = "the build seam is fallible by contract: a full buffer fails here"
186 )]
187 pub fn designation(
188 &mut self,
189 ordinal: Ordinal,
190 index: u32,
191 designation: &Designation,
192 ) -> Result<(), BuildError> {
193 self.designations
194 .insert((ordinal.index(), index), designation.encode());
195 self.designation_uses
196 .insert((ordinal.index(), index), designation.use_ordinal);
197 Ok(())
198 }
199
200 #[expect(
206 clippy::unnecessary_wraps,
207 reason = "the build seam is fallible by contract: a full buffer fails here"
208 )]
209 pub fn acceptability(
210 &mut self,
211 ordinal: Ordinal,
212 index: u32,
213 language_refset: u32,
214 acceptability: u32,
215 ) -> Result<(), BuildError> {
216 self.acceptability
217 .insert((ordinal.index(), index, language_refset), acceptability);
218 Ok(())
219 }
220
221 #[expect(
227 clippy::unnecessary_wraps,
228 reason = "the build seam is fallible by contract: a full buffer fails here"
229 )]
230 pub fn properties(
231 &mut self,
232 ordinal: Ordinal,
233 key: u32,
234 values: &[PropertyValue],
235 ) -> Result<(), BuildError> {
236 self.properties
237 .insert((ordinal.index(), key), PropertyValue::encode_list(values));
238 Ok(())
239 }
240
241 pub fn finish(self, rule: &PreferredRule) -> Result<PathBuf, BuildError> {
249 let txn = self.db.begin_write()?;
250 {
251 let mut meta = txn.open_table(tables::META)?;
252 meta.insert(tables::META_LAYOUT, tables::LAYOUT_VERSION)?;
253 meta.insert(tables::META_SYSTEM, self.system.as_str())?;
254 meta.insert(tables::META_VERSION, self.version.as_str())?;
255 meta.insert(
256 tables::META_CONCEPTS,
257 self.concepts.len().to_string().as_str(),
258 )?;
259 }
260 {
261 let mut codes = txn.open_table(tables::CODES)?;
262 for (code, ordinal) in &self.codes {
263 codes.insert(code.as_str(), *ordinal)?;
264 }
265 }
266 {
267 let mut concepts = txn.open_table(tables::CONCEPTS)?;
268 for (ordinal, bytes) in &self.concepts {
269 concepts.insert(*ordinal, bytes.as_slice())?;
270 }
271 }
272 {
273 let mut designations = txn.open_table(tables::DESIGNATIONS)?;
274 for (key, bytes) in &self.designations {
275 designations.insert(*key, bytes.as_slice())?;
276 }
277 }
278 {
279 let mut acceptability = txn.open_table(tables::ACCEPTABILITY)?;
280 for (key, value) in &self.acceptability {
281 acceptability.insert(*key, *value)?;
282 }
283 }
284 {
285 let mut properties = txn.open_table(tables::PROPERTIES)?;
286 for (key, bytes) in &self.properties {
287 properties.insert(*key, bytes.as_slice())?;
288 }
289 }
290 for table_def in [
291 tables::PROPERTY_KEYS,
292 tables::DESIGNATION_USES,
293 tables::LANGUAGE_REFSETS,
294 tables::ACCEPTABILITIES,
295 ] {
296 let mut table = txn.open_table(table_def)?;
297 if let Some(entries) = self.vocabularies.get(table_def.name()) {
298 for (ordinal, name) in entries {
299 table.insert(*ordinal, name.as_str())?;
300 }
301 }
302 }
303 {
304 let mut chosen: BTreeMap<(u32, u32, u32), u32> = BTreeMap::new();
307 for ((concept, index, refset), acceptability) in &self.acceptability {
308 if *acceptability != rule.preferred {
309 continue;
310 }
311 let Some(use_ordinal) = self.designation_uses.get(&(*concept, *index)) else {
312 continue;
313 };
314 chosen
315 .entry((*concept, *refset, *use_ordinal))
316 .and_modify(|existing| *existing = (*existing).min(*index))
317 .or_insert(*index);
318 }
319 let mut preferred = txn.open_table(tables::PREFERRED)?;
320 for (key, index) in &chosen {
321 preferred.insert(*key, *index)?;
322 }
323 }
324 txn.commit()?;
325 let mut db = self.db;
329 while db.compact()? {}
330 drop(db);
331 Ok(self.path)
332 }
333}