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