1use std::collections::HashMap;
2use std::path::Path;
3
4use redb::{
5 Database, MultimapTableDefinition, ReadableDatabase, ReadableMultimapTable, ReadableTable,
6 ReadableTableMetadata, TableDefinition,
7};
8use sinter_core::{
9 CorpusScope, Edge, FileFacts, Graph, Node, NodeId, Reference, UnresolvedReference,
10};
11
12use crate::error::StoreError;
13
14pub(crate) const NODES: TableDefinition<&str, &[u8]> = TableDefinition::new("nodes");
15pub(crate) const OUT_EDGES: MultimapTableDefinition<&str, &[u8]> =
19 MultimapTableDefinition::new("out_edges");
20pub(crate) const IN_EDGES: MultimapTableDefinition<&str, &[u8]> =
22 MultimapTableDefinition::new("in_edges");
23pub(crate) const UNRESOLVED: MultimapTableDefinition<&str, &[u8]> =
26 MultimapTableDefinition::new("unresolved");
27pub(crate) const FILE_FACTS: TableDefinition<&str, &[u8]> = TableDefinition::new("file_facts");
30pub(crate) const FILE_HASH: TableDefinition<&str, &str> = TableDefinition::new("file_hash");
32pub(crate) const FILE_SCOPE: TableDefinition<&str, &str> = TableDefinition::new("file_scope");
35pub(crate) const NODE_SCOPE: TableDefinition<&str, &str> = TableDefinition::new("node_scope");
38pub(crate) const NAME_REFS: MultimapTableDefinition<&str, &str> =
41 MultimapTableDefinition::new("name_refs");
42pub(crate) const NAME_NODES: MultimapTableDefinition<&str, u32> =
44 MultimapTableDefinition::new("name_nodes");
45pub(crate) const TRIGRAMS: MultimapTableDefinition<&str, u32> =
47 MultimapTableDefinition::new("trigrams");
48pub(crate) const TOKENS_WORDS: MultimapTableDefinition<&str, u32> =
53 MultimapTableDefinition::new("tokens_words");
54pub(crate) const BODY_TERMS: MultimapTableDefinition<&str, u32> =
58 MultimapTableDefinition::new("body_terms");
59pub(crate) const INTERN: TableDefinition<u32, &str> = TableDefinition::new("intern");
62pub(crate) const INTERN_REV: TableDefinition<&str, u32> = TableDefinition::new("intern_rev");
63pub(crate) const IMPORTS: MultimapTableDefinition<&str, &[u8]> =
66 MultimapTableDefinition::new("imports");
67pub(crate) const META: TableDefinition<&str, u32> = TableDefinition::new("meta");
70pub(crate) const RESOLVE_META: TableDefinition<&str, &str> = TableDefinition::new("resolve_meta");
75pub(crate) const PENDING: TableDefinition<&str, &[u8]> = TableDefinition::new("pending_delta");
80const SCHEMA_VERSION: u32 = 12;
85
86#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct FileStamp {
93 pub hash: String,
94 pub identity_nanos: u128,
95 pub len: u64,
96}
97
98impl FileStamp {
99 pub(crate) fn encode(&self) -> String {
100 format!("{}|{}|{}", self.hash, self.identity_nanos, self.len)
101 }
102
103 pub(crate) fn decode(value: &str) -> Self {
104 let mut parts = value.split('|');
105 let hash = parts.next().unwrap_or_default().to_string();
106 Self {
107 hash,
108 identity_nanos: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
109 len: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
111 }
112 }
113}
114
115impl Store {
116 pub const CURRENT_SCHEMA: u32 = SCHEMA_VERSION;
118
119 pub fn schema_of(path: impl AsRef<Path>) -> Result<Option<u32>, StoreError> {
122 Self::open(path)?.schema()
123 }
124}
125
126pub struct Store {
128 pub(crate) db: Database,
129}
130
131pub(crate) fn open_retrying(
139 path: &Path,
140 open: fn(&Path) -> Result<Database, redb::DatabaseError>,
141) -> Result<Database, redb::DatabaseError> {
142 let budget = std::time::Duration::from_secs(if cfg!(windows) { 20 } else { 5 });
143 let started = std::time::Instant::now();
144 let mut delay = std::time::Duration::from_millis(10);
145 loop {
146 match open(path) {
147 Err(redb::DatabaseError::DatabaseAlreadyOpen) if started.elapsed() < budget => {
148 std::thread::sleep(delay);
149 delay = (delay * 2).min(std::time::Duration::from_millis(200));
150 }
151 other => return other,
152 }
153 }
154}
155
156pub fn create_database(path: &Path) -> Result<Database, StoreError> {
160 Ok(open_retrying(path, |p| Database::create(p))?)
161}
162
163impl Store {
164 pub fn create(path: impl AsRef<Path>) -> Result<Self, StoreError> {
168 let path = path.as_ref();
169 if path.exists() {
170 let db = open_retrying(path, |p| Database::open(p))?;
171 let txn = db.begin_read()?;
172 let stored = match txn.open_table(META) {
173 Ok(table) => table.get("schema")?.map(|g| g.value()),
174 Err(redb::TableError::TableDoesNotExist(_)) => None,
175 Err(e) => return Err(e.into()),
176 };
177 if let Some(v) = stored
181 && v > SCHEMA_VERSION
182 {
183 return Err(StoreError::NewerSchema {
184 stored: v,
185 supported: SCHEMA_VERSION,
186 });
187 }
188 if stored != Some(SCHEMA_VERSION) {
189 drop(txn);
190 drop(db);
191 std::fs::remove_file(path).map_err(StoreError::Reset)?;
192 }
193 }
194 let store = Self {
195 db: open_retrying(path, |p| Database::create(p))?,
196 };
197 let txn = store.db.begin_write()?;
198 {
199 let mut meta = txn.open_table(META)?;
200 meta.insert("schema", SCHEMA_VERSION)?;
201 drop(meta);
202 txn.open_table(NODES)?;
203 txn.open_table(FILE_FACTS)?;
204 txn.open_table(FILE_HASH)?;
205 txn.open_table(FILE_SCOPE)?;
206 txn.open_table(NODE_SCOPE)?;
207 txn.open_multimap_table(OUT_EDGES)?;
208 txn.open_multimap_table(IN_EDGES)?;
209 txn.open_multimap_table(UNRESOLVED)?;
210 txn.open_multimap_table(NAME_REFS)?;
211 txn.open_multimap_table(NAME_NODES)?;
212 txn.open_multimap_table(TRIGRAMS)?;
213 txn.open_multimap_table(TOKENS_WORDS)?;
214 txn.open_multimap_table(BODY_TERMS)?;
215 txn.open_multimap_table(IMPORTS)?;
216 txn.open_table(INTERN)?;
217 txn.open_table(INTERN_REV)?;
218 txn.open_table(RESOLVE_META)?;
219 txn.open_table(PENDING)?;
220 }
221 txn.commit()?;
222 Ok(store)
223 }
224
225 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
226 Ok(Self {
227 db: open_retrying(path.as_ref(), |p| Database::open(p))?,
228 })
229 }
230
231 pub fn schema(&self) -> Result<Option<u32>, StoreError> {
233 let txn = self.db.begin_read()?;
234 match txn.open_table(META) {
235 Ok(table) => Ok(table.get("schema")?.map(|g| g.value())),
236 Err(redb::TableError::TableDoesNotExist(_)) => Ok(None),
237 Err(e) => Err(e.into()),
238 }
239 }
240
241 pub fn write_graph(&self, graph: &Graph) -> Result<(), StoreError> {
244 let txn = self.db.begin_write()?;
245 {
246 let mut nodes = txn.open_table(NODES)?;
247 let mut scopes = txn.open_table(FILE_SCOPE)?;
248 let mut out = txn.open_multimap_table(OUT_EDGES)?;
249 let mut inn = txn.open_multimap_table(IN_EDGES)?;
250 for node in graph.nodes() {
251 nodes.insert(node.id.as_str(), postcard::to_allocvec(node)?.as_slice())?;
252 scopes.insert(
253 node.file.as_str(),
254 CorpusScope::classify_path(&node.file).as_str(),
255 )?;
256 }
257 for edge in graph.edges() {
258 let bytes = postcard::to_allocvec(edge)?;
259 out.insert(edge.src.as_str(), bytes.as_slice())?;
260 inn.insert(edge.dst.as_str(), bytes.as_slice())?;
261 }
262 }
263 txn.commit()?;
264 Ok(())
265 }
266
267 pub fn unresolved_count(&self) -> Result<u64, StoreError> {
269 let txn = self.db.begin_read()?;
270 let table = match txn.open_multimap_table(UNRESOLVED) {
271 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(0),
272 other => other?,
273 };
274 Ok(table.len()?)
275 }
276
277 pub fn all_unresolved(&self) -> Result<Vec<Reference>, StoreError> {
280 Ok(self
281 .all_unresolved_details()?
282 .into_iter()
283 .map(|u| u.reference)
284 .collect())
285 }
286
287 pub fn all_unresolved_details(&self) -> Result<Vec<UnresolvedReference>, StoreError> {
291 let txn = self.db.begin_read()?;
292 let table = match txn.open_multimap_table(UNRESOLVED) {
293 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
294 other => other?,
295 };
296 let mut refs = Vec::new();
297 for entry in table.iter()? {
298 let (_, values) = entry?;
299 for guard in values {
300 refs.push(postcard::from_bytes(guard?.value())?);
301 }
302 }
303 Ok(refs)
304 }
305
306 pub fn unresolved_refs(
310 &self,
311 file: Option<&str>,
312 name: Option<&str>,
313 ) -> Result<Vec<Reference>, StoreError> {
314 let mut refs = match file {
315 Some(file) => self.references_in(file)?,
316 None => self.all_unresolved()?,
317 };
318 if let Some(name) = name {
319 refs.retain(|r| name_tail_matches(&r.name, name));
320 }
321 Ok(refs)
322 }
323
324 pub fn unresolved_details(
325 &self,
326 file: Option<&str>,
327 name: Option<&str>,
328 ) -> Result<Vec<UnresolvedReference>, StoreError> {
329 let mut refs = match file {
330 Some(file) => self.unresolved_details_in(file)?,
331 None => self.all_unresolved_details()?,
332 };
333 if let Some(name) = name {
334 refs.retain(|u| name_tail_matches(&u.reference.name, name));
335 }
336 Ok(refs)
337 }
338
339 pub fn references_in(&self, file: &str) -> Result<Vec<Reference>, StoreError> {
341 Ok(self
342 .unresolved_details_in(file)?
343 .into_iter()
344 .map(|u| u.reference)
345 .collect())
346 }
347
348 pub fn unresolved_details_in(
349 &self,
350 file: &str,
351 ) -> Result<Vec<UnresolvedReference>, StoreError> {
352 let txn = self.db.begin_read()?;
353 let table = match txn.open_multimap_table(UNRESOLVED) {
354 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
355 other => other?,
356 };
357 let mut refs = Vec::new();
358 for guard in table.get(file)? {
359 refs.push(postcard::from_bytes(guard?.value())?);
360 }
361 Ok(refs)
362 }
363
364 pub fn resolve_fingerprint(&self, key: &str) -> Result<Option<String>, StoreError> {
367 let txn = self.db.begin_read()?;
368 let table = match txn.open_table(RESOLVE_META) {
369 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
370 other => other?,
371 };
372 Ok(table.get(key)?.map(|g| g.value().to_string()))
373 }
374
375 pub fn set_resolve_fingerprint(
379 &self,
380 key: &str,
381 fingerprint: Option<&str>,
382 ) -> Result<(), StoreError> {
383 if self.resolve_fingerprint(key)?.as_deref() == fingerprint {
384 return Ok(());
385 }
386 let txn = self.db.begin_write()?;
387 {
388 let mut table = txn.open_table(RESOLVE_META)?;
389 match fingerprint {
390 Some(f) => {
391 table.insert(key, f)?;
392 }
393 None => {
394 table.remove(key)?;
395 }
396 }
397 }
398 txn.commit()?;
399 Ok(())
400 }
401
402 pub fn unresolved_named(&self, name: &str) -> Result<usize, StoreError> {
406 let files = self.ref_files(&std::collections::BTreeSet::from([name.to_string()]))?;
407 let mut count = 0;
408 for file in files {
409 count += self
410 .references_in(&file)?
411 .iter()
412 .filter(|r| name_tail_matches(&r.name, name))
413 .count();
414 }
415 Ok(count)
416 }
417
418 pub fn node(&self, id: &NodeId) -> Result<Option<Node>, StoreError> {
419 let txn = self.db.begin_read()?;
420 let table = txn.open_table(NODES)?;
421 match table.get(id.as_str())? {
422 Some(guard) => Ok(Some(postcard::from_bytes(guard.value())?)),
423 None => Ok(None),
424 }
425 }
426
427 pub fn node_count(&self) -> Result<u64, StoreError> {
428 let txn = self.db.begin_read()?;
429 Ok(txn.open_table(NODES)?.len()?)
430 }
431
432 pub fn edge_count(&self) -> Result<u64, StoreError> {
433 let txn = self.db.begin_read()?;
434 Ok(txn.open_multimap_table(OUT_EDGES)?.len()?)
435 }
436
437 pub fn out_edges(&self, id: &NodeId) -> Result<Vec<Edge>, StoreError> {
439 self.adjacent(OUT_EDGES, id)
440 }
441
442 pub fn in_edges(&self, id: &NodeId) -> Result<Vec<Edge>, StoreError> {
444 self.adjacent(IN_EDGES, id)
445 }
446
447 pub fn in_edges_many(&self, ids: &[NodeId]) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
450 self.adjacent_many(IN_EDGES, ids)
451 }
452
453 pub fn out_edges_many(&self, ids: &[NodeId]) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
455 self.adjacent_many(OUT_EDGES, ids)
456 }
457
458 fn adjacent_many(
459 &self,
460 table: MultimapTableDefinition<&str, &[u8]>,
461 ids: &[NodeId],
462 ) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
463 let txn = self.db.begin_read()?;
464 let table = txn.open_multimap_table(table)?;
465 let mut found = HashMap::with_capacity(ids.len());
466 for id in ids {
467 let mut edges = Vec::new();
468 for guard in table.get(id.as_str())? {
469 edges.push(postcard::from_bytes(guard?.value())?);
470 }
471 found.insert(id.clone(), edges);
472 }
473 Ok(found)
474 }
475
476 fn adjacent(
477 &self,
478 table: MultimapTableDefinition<&str, &[u8]>,
479 id: &NodeId,
480 ) -> Result<Vec<Edge>, StoreError> {
481 let txn = self.db.begin_read()?;
482 let table = txn.open_multimap_table(table)?;
483 let mut edges = Vec::new();
484 for guard in table.get(id.as_str())? {
485 edges.push(postcard::from_bytes(guard?.value())?);
486 }
487 Ok(edges)
488 }
489
490 pub fn file_hashes(&self) -> Result<Vec<(String, FileStamp)>, StoreError> {
492 let txn = self.db.begin_read()?;
493 let table = txn.open_table(FILE_HASH)?;
494 let mut out = Vec::new();
495 for entry in table.iter()? {
496 let (k, v) = entry?;
497 out.push((k.value().to_string(), FileStamp::decode(v.value())));
498 }
499 Ok(out)
500 }
501
502 pub fn set_file_scopes(&self, rows: &[(String, CorpusScope)]) -> Result<usize, StoreError> {
508 if rows.is_empty() {
509 return Ok(0);
510 }
511 let existing = self.file_scopes()?;
512 let stale: Vec<&(String, CorpusScope)> = rows
513 .iter()
514 .filter(|(file, scope)| existing.get(file) != Some(scope))
515 .collect();
516 if stale.is_empty() {
517 return Ok(0);
518 }
519 let txn = self.db.begin_write()?;
520 {
521 let mut table = txn.open_table(FILE_SCOPE)?;
522 for (file, scope) in &stale {
523 table.insert(file.as_str(), scope.as_str())?;
524 }
525 }
526 txn.commit()?;
527 Ok(stale.len())
528 }
529
530 pub fn file_scopes(&self) -> Result<HashMap<String, CorpusScope>, StoreError> {
533 let txn = self.db.begin_read()?;
534 let table = txn.open_table(FILE_SCOPE)?;
535 let mut scopes = HashMap::new();
536 for entry in table.iter()? {
537 let (file, scope) = entry?;
538 let file = file.value().to_string();
539 scopes.insert(
540 file.clone(),
541 CorpusScope::from_str_opt(scope.value())
542 .unwrap_or_else(|| CorpusScope::classify_path(&file)),
543 );
544 }
545 Ok(scopes)
546 }
547
548 pub fn file_scope(&self, file: &str) -> Result<CorpusScope, StoreError> {
549 let txn = self.db.begin_read()?;
550 let table = txn.open_table(FILE_SCOPE)?;
551 Ok(table
552 .get(file)?
553 .and_then(|guard| CorpusScope::from_str_opt(guard.value()))
554 .unwrap_or_else(|| CorpusScope::classify_path(file)))
555 }
556
557 pub fn facts(&self, file: &str) -> Result<Option<FileFacts>, StoreError> {
558 let txn = self.db.begin_read()?;
559 let table = txn.open_table(FILE_FACTS)?;
560 match table.get(file)? {
561 Some(guard) => Ok(Some(crate::update::decode_facts(guard.value())?)),
562 None => Ok(None),
563 }
564 }
565
566 pub fn syntax_error_files(&self) -> Result<Vec<String>, StoreError> {
570 let txn = self.db.begin_read()?;
571 let table = txn.open_table(FILE_FACTS)?;
572 let mut files = Vec::new();
573 for entry in table.iter()? {
574 let (file, bytes) = entry?;
575 let facts = crate::update::decode_facts(bytes.value())?;
576 if facts.has_syntax_errors {
577 files.push(file.value().to_string());
578 }
579 }
580 files.sort();
581 Ok(files)
582 }
583
584 pub fn compact(&mut self) -> Result<bool, StoreError> {
589 let mut any = false;
590 for _ in 0..16 {
591 if !self.db.compact()? {
592 break;
593 }
594 any = true;
595 }
596 Ok(any)
597 }
598
599 pub fn all_imports(&self) -> Result<Vec<Reference>, StoreError> {
601 let txn = self.db.begin_read()?;
602 let table = txn.open_multimap_table(IMPORTS)?;
603 let mut refs = Vec::new();
604 for entry in table.iter()? {
605 let (_, values) = entry?;
606 for guard in values {
607 refs.push(postcard::from_bytes(guard?.value())?);
608 }
609 }
610 Ok(refs)
611 }
612
613 pub fn all_nodes(&self) -> Result<Vec<Node>, StoreError> {
616 let txn = self.db.begin_read()?;
617 let table = txn.open_table(NODES)?;
618 let mut nodes = Vec::new();
619 for entry in table.iter()? {
620 nodes.push(postcard::from_bytes(entry?.1.value())?);
621 }
622 Ok(nodes)
623 }
624
625 pub fn in_degrees(&self) -> Result<Vec<(String, usize)>, StoreError> {
630 let txn = self.db.begin_read()?;
631 let table = txn.open_multimap_table(IN_EDGES)?;
632 let mut out = Vec::new();
633 for entry in table.iter()? {
634 let (key, values) = entry?;
635 let mut n = 0usize;
636 for guard in values {
637 let edge: Edge = postcard::from_bytes(guard?.value())?;
638 if edge.relation != sinter_core::Relation::Contains {
639 n += 1;
640 }
641 }
642 if n > 0 {
643 out.push((key.value().to_string(), n));
644 }
645 }
646 Ok(out)
647 }
648
649 pub fn read_graph(&self) -> Result<Graph, StoreError> {
652 let txn = self.db.begin_read()?;
653 let mut graph = Graph::new();
654 {
655 let nodes = txn.open_table(NODES)?;
656 for entry in nodes.iter()? {
657 let (_, value) = entry?;
658 graph.add_node(postcard::from_bytes(value.value())?)?;
659 }
660 }
661 {
662 let out = txn.open_multimap_table(OUT_EDGES)?;
663 for entry in out.iter()? {
664 let (_, values) = entry?;
665 for guard in values {
666 graph.add_edge(postcard::from_bytes(guard?.value())?)?;
667 }
668 }
669 }
670 Ok(graph)
671 }
672}
673
674fn name_tail_matches(written: &str, name: &str) -> bool {
677 let tail = written.rsplit("::").next().unwrap_or(written);
681 let tail = tail.rsplit(['/', '.']).next().unwrap_or(tail);
682 tail == name
683}