1use super::*;
15use std::sync::OnceLock;
16
17const MAGIC: &[u8; 8] = b"SNECLH01";
18pub const ASSOCIATIONS: u64 = 900000000000522004;
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
22#[serde(deny_unknown_fields)]
23pub struct HistoryManifest {
24 pub bytes: u64,
25 pub sha256: String,
26 pub rows: usize,
27 pub refsets: Vec<u64>,
29 #[serde(default)]
31 pub skipped: usize,
32}
33
34#[derive(Debug, Default)]
37struct Keyed {
38 keys: Vec<u32>,
39 offsets: Vec<u32>,
40 others: Vec<u32>,
41 kinds: Vec<u8>,
42}
43
44impl Keyed {
45 fn build(mut rows: Vec<(u32, u32, u8)>) -> Result<Self> {
46 rows.sort_unstable();
47 rows.dedup();
48 let mut keyed = Self {
49 offsets: vec![0],
50 ..Self::default()
51 };
52 for (key, other, kind) in rows {
53 if keyed.keys.last() != Some(&key) {
54 keyed.keys.push(key);
55 keyed.offsets.push(*keyed.offsets.last().expect("seeded"));
56 }
57 keyed.others.push(other);
58 keyed.kinds.push(kind);
59 *keyed.offsets.last_mut().expect("seeded") =
60 u32::try_from(keyed.others.len()).context("History rows exceed u32")?;
61 }
62 Ok(keyed)
63 }
64
65 fn get(&self, key: u32) -> (&[u32], &[u8]) {
66 match self.keys.binary_search(&key) {
67 Ok(i) => {
68 let range = self.offsets[i] as usize..self.offsets[i + 1] as usize;
69 (&self.others[range.clone()], &self.kinds[range])
70 }
71 Err(_) => (&[], &[]),
72 }
73 }
74
75 fn validate(&self, concepts: usize, kinds: usize) -> Result<()> {
76 ensure!(
77 self.offsets.len() == self.keys.len() + 1
78 && self.offsets.first() == Some(&0)
79 && self.offsets.last().map(|&v| v as usize) == Some(self.others.len())
80 && self.others.len() == self.kinds.len(),
81 "History index offsets do not cover their rows"
82 );
83 ensure!(
84 self.offsets.windows(2).all(|w| w[0] < w[1]),
85 "History index has an empty or unordered key"
86 );
87 ensure!(
88 self.keys.windows(2).all(|w| w[0] < w[1]),
89 "History keys are not sorted and unique"
90 );
91 ensure!(
92 self.keys.last().is_none_or(|&k| (k as usize) < concepts)
93 && self.others.iter().all(|&o| (o as usize) < concepts),
94 "History index names a concept outside the store"
95 );
96 ensure!(
97 self.kinds.iter().all(|&k| (k as usize) < kinds),
98 "History index names an unknown association"
99 );
100 Ok(())
101 }
102
103 fn write(&self, out: &mut impl Write) -> Result<()> {
104 put_u32s(out, &self.keys)?;
105 put_u32s(out, &self.offsets)?;
106 put_u32s(out, &self.others)?;
107 put_u64(out, self.kinds.len() as u64)?;
108 out.write_all(&self.kinds)?;
109 Ok(())
110 }
111
112 fn read(input: &mut Input) -> Result<Self> {
113 Ok(Self {
114 keys: input.u32s()?,
115 offsets: input.u32s()?,
116 others: input.u32s()?,
117 kinds: input.bytes()?,
118 })
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct Association {
126 pub concept: u32,
127 pub refset: u64,
128}
129
130#[derive(Debug, Default)]
131pub struct HistoryIndex {
132 refsets: Vec<u64>,
133 forward: Keyed,
135 backward: Keyed,
137}
138
139impl HistoryIndex {
140 pub fn build(store: &NumericStore) -> Result<(Self, usize)> {
146 let associations: Vec<u64> = store
147 .hierarchy(ASSOCIATIONS, false, false, false)
148 .into_iter()
149 .filter(|refset| store.member_tables.fields(*refset).is_some())
150 .collect();
151 let mut refsets = Vec::new();
152 let mut rows = Vec::new();
153 let mut skipped = 0;
154 for refset in associations {
155 let Some(table) = store.member_tables.get(refset)? else {
156 continue;
157 };
158 let Some(MemberColumn::Id(targets)) = table.column("targetComponentId") else {
159 continue;
160 };
161 let Some(MemberColumn::Id(references)) = table.column("referencedComponentId") else {
162 bail!("Association table {refset} has no referenced components");
163 };
164 let Some(MemberColumn::Boolean(active)) = table.column("active") else {
165 bail!("Association table {refset} has no active flags");
166 };
167 let kind = u8::try_from(refsets.len()).context("More than 255 associations")?;
168 refsets.push(refset);
169 for row in 0..table.len() {
170 if active[row] == 0 {
171 continue;
172 }
173 match (store.ordinal(references[row]), store.ordinal(targets[row])) {
174 (Some(source), Some(target)) => rows.push((source, target, kind)),
175 _ => skipped += 1,
176 }
177 }
178 }
179 let backward = Keyed::build(rows.iter().map(|&(s, t, k)| (t, s, k)).collect())?;
180 let forward = Keyed::build(rows)?;
181 let index = Self {
182 refsets,
183 forward,
184 backward,
185 };
186 index.validate(store.ids.len())?;
187 Ok((index, skipped))
188 }
189
190 pub fn rows(&self) -> usize {
191 self.forward.others.len()
192 }
193
194 pub fn refsets(&self) -> &[u64] {
195 &self.refsets
196 }
197
198 fn collect(&self, (others, kinds): (&[u32], &[u8])) -> Vec<Association> {
199 others
200 .iter()
201 .zip(kinds)
202 .map(|(&concept, &kind)| Association {
203 concept,
204 refset: self.refsets[kind as usize],
205 })
206 .collect()
207 }
208
209 pub fn successors(&self, concept: u32) -> Vec<Association> {
211 self.collect(self.forward.get(concept))
212 }
213
214 pub fn predecessors(&self, concept: u32) -> Vec<Association> {
216 self.collect(self.backward.get(concept))
217 }
218
219 pub(crate) fn walk(
225 &self,
226 backward: bool,
227 concepts: &[u32],
228 mut visit: impl FnMut(u32, &[u32], &[u8]),
229 ) {
230 let keyed = if backward {
231 &self.backward
232 } else {
233 &self.forward
234 };
235 let rows = |i: usize| {
236 let range = keyed.offsets[i] as usize..keyed.offsets[i + 1] as usize;
237 (&keyed.others[range.clone()], &keyed.kinds[range])
238 };
239 if concepts.len().saturating_mul(16) < keyed.keys.len() {
240 for &concept in concepts {
241 if let Ok(i) = keyed.keys.binary_search(&concept) {
242 let (others, kinds) = rows(i);
243 visit(concept, others, kinds);
244 }
245 }
246 } else {
247 let (mut a, mut b) = (0, 0);
248 while a < concepts.len() && b < keyed.keys.len() {
249 match concepts[a].cmp(&keyed.keys[b]) {
250 std::cmp::Ordering::Less => a += 1,
251 std::cmp::Ordering::Greater => b += 1,
252 std::cmp::Ordering::Equal => {
253 let (others, kinds) = rows(b);
254 visit(concepts[a], others, kinds);
255 a += 1;
256 b += 1;
257 }
258 }
259 }
260 }
261 }
262
263 pub(crate) fn kind_of(&self, refset: u64) -> Option<u8> {
264 self.refsets
265 .iter()
266 .position(|&r| r == refset)
267 .map(|i| i as u8)
268 }
269
270 fn validate(&self, concepts: usize) -> Result<()> {
271 self.forward.validate(concepts, self.refsets.len())?;
272 self.backward.validate(concepts, self.refsets.len())?;
273 ensure!(
274 self.forward.others.len() == self.backward.others.len(),
275 "History directions disagree on the number of rows"
276 );
277 Ok(())
278 }
279
280 pub fn write(&self, path: &Path, skipped: usize) -> Result<HistoryManifest> {
281 let mut out = BufWriter::new(File::create_new(path)?);
282 out.write_all(MAGIC)?;
283 put_u64(&mut out, self.refsets.len() as u64)?;
284 for &refset in &self.refsets {
285 put_u64(&mut out, refset)?;
286 }
287 self.forward.write(&mut out)?;
288 self.backward.write(&mut out)?;
289 out.flush()?;
290 out.get_ref().sync_all()?;
291 Ok(HistoryManifest {
292 bytes: path.metadata()?.len(),
293 sha256: sha256(path)?,
294 rows: self.rows(),
295 refsets: self.refsets.clone(),
296 skipped,
297 })
298 }
299
300 pub(super) fn open(
301 section: &Section,
302 manifest: &HistoryManifest,
303 concepts: usize,
304 ) -> Result<Self> {
305 let mut input = Input::open(section, MAGIC)?;
306 let count = input.count(8)?;
307 let mut refsets = Vec::with_capacity(count);
308 for _ in 0..count {
309 refsets.push(input.u64()?);
310 }
311 let forward = Keyed::read(&mut input)?;
312 let backward = Keyed::read(&mut input)?;
313 ensure!(input.remaining == 0, "Trailing history bytes");
314 let index = Self {
315 refsets,
316 forward,
317 backward,
318 };
319 index.validate(concepts)?;
320 ensure!(
321 index.rows() == manifest.rows && index.refsets == manifest.refsets,
322 "History index differs from manifest"
323 );
324 Ok(index)
325 }
326}
327
328pub fn add_history(directory: &Path) -> Result<Option<HistoryManifest>> {
333 let store = NumericStore::open(directory)?;
334 if !store.member_tables.is_available() {
335 return Ok(None);
336 }
337 let (index, skipped) = HistoryIndex::build(&store)?;
338 drop(store);
339 let path = directory.join("history.bin");
340 if path.exists() {
341 std::fs::remove_file(&path)?;
342 }
343 let written = index.write(&path, skipped)?;
344 let mut manifest = Manifest::read(directory)?;
345 manifest.history = Some(written.clone());
346 let file = directory.join("manifest.json");
347 let mut out = BufWriter::new(File::create(&file)?);
348 serde_json::to_writer_pretty(&mut out, &manifest)?;
349 out.flush()?;
350 out.get_ref().sync_all()?;
351 Ok(Some(written))
352}
353
354#[derive(Debug, Default)]
356pub struct HistoryStore {
357 source: Option<(Section, HistoryManifest, usize)>,
358 loaded: OnceLock<std::result::Result<HistoryIndex, String>>,
359}
360
361impl HistoryStore {
362 pub(super) fn lazy(
363 source: &IndexSource,
364 metadata: HistoryManifest,
365 concepts: usize,
366 ) -> Result<Self> {
367 Ok(Self {
368 source: Some((source.section("history.bin")?, metadata, concepts)),
369 loaded: OnceLock::new(),
370 })
371 }
372 pub fn loaded(index: HistoryIndex) -> Self {
374 Self {
375 source: None,
376 loaded: OnceLock::from(Ok(index)),
377 }
378 }
379 pub fn get(&self) -> Result<Option<&HistoryIndex>> {
380 if self.source.is_none() && self.loaded.get().is_none() {
381 return Ok(None);
382 }
383 match self.loaded.get_or_init(|| {
384 let (section, manifest, concepts) = self.source.as_ref().unwrap();
385 HistoryIndex::open(section, manifest, *concepts).map_err(|e| e.to_string())
386 }) {
387 Ok(index) => Ok(Some(index)),
388 Err(message) => bail!("History index: {message}"),
389 }
390 }
391}