1use std::collections::BTreeSet;
6
7use redb::{ReadableDatabase, ReadableMultimapTable, ReadableTable};
8use sinter_core::{Edge, Evidence, FileFacts, Reference};
9
10use crate::error::StoreError;
11use crate::search::{node_tokens, trigrams};
12use crate::store::{
13 FILE_FACTS, FILE_HASH, IMPORTS, IN_EDGES, INTERN, INTERN_REV, META, NAME_NODES, NAME_REFS,
14 NODES, OUT_EDGES, PENDING, Store, TOKENS_WORDS, TRIGRAMS, UNRESOLVED,
15};
16
17pub(crate) fn encode_facts(facts: &FileFacts) -> Result<Vec<u8>, StoreError> {
20 let raw = postcard::to_allocvec(facts)?;
21 zstd::encode_all(raw.as_slice(), 1).map_err(StoreError::Compress)
22}
23
24pub(crate) fn decode_facts(bytes: &[u8]) -> Result<FileFacts, StoreError> {
25 let raw = zstd::decode_all(bytes).map_err(StoreError::Compress)?;
26 Ok(postcard::from_bytes(&raw)?)
27}
28
29#[derive(Debug, Default)]
35pub struct NameDelta {
36 pub def_names: BTreeSet<String>,
37 pub dependent_files: BTreeSet<String>,
38}
39
40fn file_of_id(id: &str) -> &str {
42 id.split_once('#').map_or(id, |(file, _)| file)
43}
44
45struct PreparedFile {
49 encoded: Vec<u8>,
51 nodes: Vec<(Vec<u8>, Vec<String>, BTreeSet<String>)>,
54 edges: Vec<Vec<u8>>,
56 imports: Vec<Vec<u8>>,
58}
59
60fn prepare_file(facts: &FileFacts) -> Result<PreparedFile, StoreError> {
61 Ok(PreparedFile {
62 encoded: encode_facts(facts)?,
63 nodes: facts
64 .nodes
65 .iter()
66 .map(|n| Ok((postcard::to_allocvec(n)?, trigrams(&n.name), node_tokens(n))))
67 .collect::<Result<_, StoreError>>()?,
68 edges: facts
69 .contains
70 .iter()
71 .map(postcard::to_allocvec)
72 .collect::<Result<_, _>>()?,
73 imports: facts
74 .references
75 .iter()
76 .filter(|r| r.relation == sinter_core::Relation::Imports)
77 .map(postcard::to_allocvec)
78 .collect::<Result<_, _>>()?,
79 })
80}
81
82fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> R + Sync) -> Vec<R> {
86 let workers = std::thread::available_parallelism()
87 .map_or(1, |n| n.get())
88 .min(items.len());
89 if workers <= 1 {
90 return items.iter().map(f).collect();
91 }
92 let next = std::sync::atomic::AtomicUsize::new(0);
93 let chunks: Vec<Vec<(usize, R)>> = std::thread::scope(|s| {
94 let handles: Vec<_> = (0..workers)
95 .map(|_| {
96 s.spawn(|| {
97 let mut out = Vec::new();
98 loop {
99 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
100 let Some(item) = items.get(i) else { break };
101 out.push((i, f(item)));
102 }
103 out
104 })
105 })
106 .collect();
107 handles
108 .into_iter()
109 .map(|h| h.join().expect("prepare worker panicked"))
110 .collect()
111 });
112 let mut slots: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
113 for chunk in chunks {
114 for (i, r) in chunk {
115 slots[i] = Some(r);
116 }
117 }
118 slots
119 .into_iter()
120 .map(|s| s.expect("every index visited"))
121 .collect()
122}
123
124type PendingSets = (BTreeSet<String>, BTreeSet<String>);
126
127const PREPARE_CHUNK: usize = 512;
131
132impl Store {
133 pub fn update_files(
143 &self,
144 changed: &[FileFacts],
145 removed: &[String],
146 ) -> Result<NameDelta, StoreError> {
147 let mut delta = NameDelta::default();
148 if changed.is_empty() && removed.is_empty() {
150 return Ok(delta);
151 }
152 let txn = self.db.begin_write()?;
153 {
154 let mut nodes = txn.open_table(NODES)?;
155 let mut facts_table = txn.open_table(FILE_FACTS)?;
156 let mut hash_table = txn.open_table(FILE_HASH)?;
157 let mut out = txn.open_multimap_table(OUT_EDGES)?;
158 let mut inn = txn.open_multimap_table(IN_EDGES)?;
159 let mut unresolved = txn.open_multimap_table(UNRESOLVED)?;
160 let mut name_refs = txn.open_multimap_table(NAME_REFS)?;
161 let mut name_nodes = txn.open_multimap_table(NAME_NODES)?;
162 let mut grams = txn.open_multimap_table(TRIGRAMS)?;
163 let mut tokens = txn.open_multimap_table(TOKENS_WORDS)?;
164 let mut imports = txn.open_multimap_table(IMPORTS)?;
165 let mut intern = txn.open_table(INTERN)?;
166 let mut intern_rev = txn.open_table(INTERN_REV)?;
167 let mut meta = txn.open_table(META)?;
168 let mut pending = txn.open_table(PENDING)?;
169 let mut next_intern = meta.get("intern_next")?.map(|g| g.value()).unwrap_or(0);
170
171 let touched: Vec<&str> = changed
172 .iter()
173 .map(|f| f.file.as_str())
174 .chain(removed.iter().map(String::as_str))
175 .collect();
176
177 for file in &touched {
179 let Some(old): Option<FileFacts> = facts_table
180 .get(*file)?
181 .map(|g| decode_facts(g.value()))
182 .transpose()?
183 else {
184 continue;
185 };
186 for node in &old.nodes {
187 let id = node.id.as_str();
188 let out_bytes: Vec<Vec<u8>> = collect_values(out.get(id)?)?;
191 for bytes in out_bytes {
192 let edge: Edge = postcard::from_bytes(&bytes)?;
193 inn.remove(edge.dst.as_str(), bytes.as_slice())?;
194 }
195 let in_bytes: Vec<Vec<u8>> = collect_values(inn.get(id)?)?;
196 for bytes in in_bytes {
197 let edge: Edge = postcard::from_bytes(&bytes)?;
198 out.remove(edge.src.as_str(), bytes.as_slice())?;
199 if edge.evidence != Evidence::Structural {
202 delta
203 .dependent_files
204 .insert(file_of_id(edge.src.as_str()).to_string());
205 }
206 }
207 out.remove_all(id)?;
208 inn.remove_all(id)?;
209 nodes.remove(id)?;
210 let interned_opt = intern_rev.get(id)?.map(|g| g.value());
211 if let Some(interned) = interned_opt {
212 name_nodes.remove(node.name.as_str(), interned)?;
213 for gram in trigrams(&node.name) {
214 grams.remove(gram.as_str(), interned)?;
215 }
216 for word in node_tokens(node) {
217 tokens.remove(word.as_str(), interned)?;
218 }
219 intern.remove(interned)?;
220 intern_rev.remove(id)?;
221 }
222 delta.def_names.insert(node.name.clone());
223 }
224 for r in &old.references {
225 name_refs.remove(r.name.as_str(), *file)?;
226 }
227 imports.remove_all(*file)?;
228 unresolved.remove_all(*file)?;
229 facts_table.remove(*file)?;
230 hash_table.remove(*file)?;
231 }
232
233 for chunk in changed.chunks(PREPARE_CHUNK) {
241 let prepared: Vec<PreparedFile> = par_map(chunk, prepare_file)
242 .into_iter()
243 .collect::<Result<_, _>>()?;
244 let mut name_pairs: Vec<(&str, u32)> = Vec::new();
245 let mut gram_pairs: Vec<(&str, u32)> = Vec::new();
246 let mut token_pairs: Vec<(&str, u32)> = Vec::new();
247 let mut ref_pairs: Vec<(&str, &str)> = Vec::new();
248 for (facts, prep) in chunk.iter().zip(&prepared) {
249 let file = facts.file.as_str();
250 facts_table.insert(file, prep.encoded.as_slice())?;
251 for (node, (blob, node_grams, node_words)) in
255 facts.nodes.iter().zip(&prep.nodes)
256 {
257 let id = node.id.as_str();
258 nodes.insert(id, blob.as_slice())?;
259 let interned_existing = intern_rev.get(id)?.map(|g| g.value());
260 let interned = match interned_existing {
261 Some(existing) => existing,
262 None => {
263 let assigned = next_intern;
264 next_intern += 1;
265 intern.insert(assigned, id)?;
266 intern_rev.insert(id, assigned)?;
267 assigned
268 }
269 };
270 name_pairs.push((node.name.as_str(), interned));
271 for gram in node_grams {
272 gram_pairs.push((gram.as_str(), interned));
273 }
274 for word in node_words {
275 token_pairs.push((word.as_str(), interned));
276 }
277 delta.def_names.insert(node.name.clone());
278 }
279 for (edge, bytes) in facts.contains.iter().zip(&prep.edges) {
280 out.insert(edge.src.as_str(), bytes.as_slice())?;
281 inn.insert(edge.dst.as_str(), bytes.as_slice())?;
282 }
283 for r in &facts.references {
284 ref_pairs.push((r.name.as_str(), file));
285 }
286 for bytes in &prep.imports {
287 imports.insert(file, bytes.as_slice())?;
288 }
289 }
290 name_pairs.sort_unstable();
291 for (name, interned) in name_pairs {
292 name_nodes.insert(name, interned)?;
293 }
294 gram_pairs.sort_unstable();
295 for (gram, interned) in gram_pairs {
296 grams.insert(gram, interned)?;
297 }
298 token_pairs.sort_unstable();
299 for (word, interned) in token_pairs {
300 tokens.insert(word, interned)?;
301 }
302 ref_pairs.sort_unstable();
303 for (name, file) in ref_pairs {
304 name_refs.insert(name, file)?;
305 }
306 }
307 meta.insert("intern_next", next_intern)?;
308
309 let mut merged: PendingSets = match pending.get(PENDING_KEY)? {
312 Some(guard) => postcard::from_bytes(guard.value())?,
313 None => Default::default(),
314 };
315 merged.0.extend(delta.def_names.iter().cloned());
316 merged.1.extend(delta.dependent_files.iter().cloned());
317 pending.insert(PENDING_KEY, postcard::to_allocvec(&merged)?.as_slice())?;
318 }
319 txn.commit()?;
320 Ok(delta)
321 }
322
323 pub fn pending_delta(&self) -> Result<NameDelta, StoreError> {
327 let txn = self.db.begin_read()?;
328 let table = match txn.open_table(PENDING) {
329 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(NameDelta::default()),
330 other => other?,
331 };
332 let Some(guard) = table.get(PENDING_KEY)? else {
333 return Ok(NameDelta::default());
334 };
335 let (def_names, dependent_files): PendingSets = postcard::from_bytes(guard.value())?;
336 Ok(NameDelta {
337 def_names,
338 dependent_files,
339 })
340 }
341
342 pub fn clear_pending_delta(&self) -> Result<(), StoreError> {
347 let residue = self.pending_delta()?;
348 if residue.def_names.is_empty() && residue.dependent_files.is_empty() {
350 return Ok(());
351 }
352 let txn = self.db.begin_write()?;
353 {
354 let mut table = txn.open_table(PENDING)?;
355 table.remove(PENDING_KEY)?;
356 }
357 txn.commit()?;
358 Ok(())
359 }
360
361 pub fn commit_hashes(&self, changed: &[FileFacts]) -> Result<(), StoreError> {
366 if changed.is_empty() {
367 return Ok(());
368 }
369 let txn = self.db.begin_write()?;
370 {
371 let mut hash_table = txn.open_table(FILE_HASH)?;
372 for facts in changed {
373 hash_table.insert(facts.file.as_str(), facts.content_hash.as_str())?;
374 }
375 }
376 txn.commit()?;
377 Ok(())
378 }
379
380 pub fn commit_stamps(&self, rows: &[(String, crate::FileStamp)]) -> Result<(), StoreError> {
385 if rows.is_empty() {
386 return Ok(());
387 }
388 let txn = self.db.begin_write()?;
389 {
390 let mut hash_table = txn.open_table(FILE_HASH)?;
391 for (file, stamp) in rows {
392 hash_table.insert(file.as_str(), stamp.encode().as_str())?;
393 }
394 }
395 txn.commit()?;
396 Ok(())
397 }
398
399 pub fn ref_files(&self, names: &BTreeSet<String>) -> Result<BTreeSet<String>, StoreError> {
402 let txn = self.db.begin_read()?;
403 let table = txn.open_multimap_table(NAME_REFS)?;
404 let mut files = BTreeSet::new();
405 for name in names {
406 for guard in table.get(name.as_str())? {
407 files.insert(guard?.value().to_string());
408 }
409 }
410 Ok(files)
411 }
412
413 pub fn dynamic_edge_dst_files(
419 &self,
420 files: &BTreeSet<String>,
421 ) -> Result<BTreeSet<String>, StoreError> {
422 let mut dynamic_dst_files = BTreeSet::new();
423 let txn = self.db.begin_read()?;
424 let facts_table = txn.open_table(FILE_FACTS)?;
425 let out = txn.open_multimap_table(OUT_EDGES)?;
426 for file in files {
427 let Some(facts): Option<FileFacts> = facts_table
428 .get(file.as_str())?
429 .map(|g| decode_facts(g.value()))
430 .transpose()?
431 else {
432 continue;
433 };
434 for node in &facts.nodes {
435 for guard in out.get(node.id.as_str())? {
436 let edge: Edge = postcard::from_bytes(guard?.value())?;
437 if edge.evidence == Evidence::Dynamic {
438 dynamic_dst_files.insert(file_of_id(edge.dst.as_str()).to_string());
439 }
440 }
441 }
442 }
443 Ok(dynamic_dst_files)
444 }
445
446 pub fn apply_resolution(
453 &self,
454 teardown: &BTreeSet<String>,
455 edges: &[Edge],
456 unresolved_files: &BTreeSet<String>,
457 unresolved: &[Reference],
458 ) -> Result<(), StoreError> {
459 let txn = self.db.begin_write()?;
460 {
461 let facts_table = txn.open_table(FILE_FACTS)?;
462 let mut out = txn.open_multimap_table(OUT_EDGES)?;
463 let mut inn = txn.open_multimap_table(IN_EDGES)?;
464 for file in teardown {
465 let Some(facts): Option<FileFacts> = facts_table
466 .get(file.as_str())?
467 .map(|g| decode_facts(g.value()))
468 .transpose()?
469 else {
470 continue;
471 };
472 for node in &facts.nodes {
473 let bytes_list = collect_values(out.get(node.id.as_str())?)?;
474 for bytes in bytes_list {
475 let edge: Edge = postcard::from_bytes(&bytes)?;
476 if edge.evidence != Evidence::Structural {
477 out.remove(node.id.as_str(), bytes.as_slice())?;
478 inn.remove(edge.dst.as_str(), bytes.as_slice())?;
479 }
480 }
481 }
482 }
483 for edge in representative_sites(edges) {
484 let bytes = postcard::to_allocvec(edge)?;
485 out.insert(edge.src.as_str(), bytes.as_slice())?;
486 inn.insert(edge.dst.as_str(), bytes.as_slice())?;
487 }
488 let mut table = txn.open_multimap_table(UNRESOLVED)?;
489 for file in unresolved_files {
490 table.remove_all(file.as_str())?;
491 }
492 for r in unresolved {
493 table.insert(r.file.as_str(), postcard::to_allocvec(r)?.as_slice())?;
494 }
495 }
496 txn.commit()?;
497 Ok(())
498 }
499
500 pub fn insert_edges(&self, edges: &[Edge]) -> Result<(), StoreError> {
502 let txn = self.db.begin_write()?;
503 {
504 let mut out = txn.open_multimap_table(OUT_EDGES)?;
505 let mut inn = txn.open_multimap_table(IN_EDGES)?;
506 for edge in representative_sites(edges) {
507 let bytes = postcard::to_allocvec(edge)?;
508 out.insert(edge.src.as_str(), bytes.as_slice())?;
509 inn.insert(edge.dst.as_str(), bytes.as_slice())?;
510 }
511 }
512 txn.commit()?;
513 Ok(())
514 }
515}
516
517fn representative_sites(edges: &[Edge]) -> Vec<&Edge> {
523 let mut ordered: Vec<&Edge> = edges.iter().collect();
524 ordered.sort();
527 ordered.dedup_by(|a, b| a.identity() == b.identity());
528 ordered
529}
530
531const PENDING_KEY: &str = "delta";
532
533fn collect_values(
534 values: redb::MultimapValue<'_, &'static [u8]>,
535) -> Result<Vec<Vec<u8>>, StoreError> {
536 let mut out = Vec::new();
537 for guard in values {
538 out.push(guard?.value().to_vec());
539 }
540 Ok(out)
541}