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 = 13;
87
88#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct FileStamp {
95 pub hash: String,
96 pub identity_nanos: u128,
97 pub len: u64,
98}
99
100impl FileStamp {
101 pub(crate) fn encode(&self) -> String {
102 format!("{}|{}|{}", self.hash, self.identity_nanos, self.len)
103 }
104
105 pub(crate) fn decode(value: &str) -> Self {
106 let mut parts = value.split('|');
107 let hash = parts.next().unwrap_or_default().to_string();
108 Self {
109 hash,
110 identity_nanos: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
111 len: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
113 }
114 }
115}
116
117impl Store {
118 pub const CURRENT_SCHEMA: u32 = SCHEMA_VERSION;
120
121 pub fn schema_of(path: impl AsRef<Path>) -> Result<Option<u32>, StoreError> {
124 let path = path.as_ref();
125 match Self::open_read_only(path) {
126 Ok(store) => store.schema(),
127 Err(_) => Self::open(path)?.schema(),
129 }
130 }
131}
132
133pub struct Store {
135 pub(crate) db: Db,
136}
137
138pub(crate) enum Db {
144 Writable(Database),
145 ReadOnly(redb::ReadOnlyDatabase),
146}
147
148impl Db {
149 pub(crate) fn begin_read(&self) -> Result<redb::ReadTransaction, redb::TransactionError> {
150 match self {
151 Self::Writable(db) => db.begin_read(),
152 Self::ReadOnly(db) => db.begin_read(),
153 }
154 }
155
156 pub(crate) fn begin_write(&self) -> Result<redb::WriteTransaction, StoreError> {
157 match self {
158 Self::Writable(db) => Ok(db.begin_write()?),
159 Self::ReadOnly(_) => Err(StoreError::ReadOnly),
160 }
161 }
162
163 fn compact(&mut self) -> Result<bool, StoreError> {
164 match self {
165 Self::Writable(db) => Ok(db.compact()?),
166 Self::ReadOnly(_) => Err(StoreError::ReadOnly),
167 }
168 }
169}
170
171pub(crate) fn open_retrying<D>(
179 path: &Path,
180 open: fn(&Path) -> Result<D, redb::DatabaseError>,
181) -> Result<D, redb::DatabaseError> {
182 let budget = std::time::Duration::from_secs(if cfg!(windows) { 20 } else { 5 });
183 let started = std::time::Instant::now();
184 let mut delay = std::time::Duration::from_millis(10);
185 loop {
186 match open(path) {
187 Err(redb::DatabaseError::DatabaseAlreadyOpen) if started.elapsed() < budget => {
188 std::thread::sleep(delay);
189 delay = (delay * 2).min(std::time::Duration::from_millis(200));
190 }
191 other => return other,
192 }
193 }
194}
195
196pub fn create_database(path: &Path) -> Result<Database, StoreError> {
200 Ok(open_retrying(path, |p| Database::create(p))?)
201}
202
203impl Store {
204 pub fn create(path: impl AsRef<Path>) -> Result<Self, StoreError> {
208 let path = path.as_ref();
209 if path.exists() {
210 let db = open_retrying(path, |p| Database::open(p))?;
211 let txn = db.begin_read()?;
212 let stored = match txn.open_table(META) {
213 Ok(table) => table.get("schema")?.map(|g| g.value()),
214 Err(redb::TableError::TableDoesNotExist(_)) => None,
215 Err(e) => return Err(e.into()),
216 };
217 if let Some(v) = stored
221 && v > SCHEMA_VERSION
222 {
223 return Err(StoreError::NewerSchema {
224 stored: v,
225 supported: SCHEMA_VERSION,
226 });
227 }
228 if stored != Some(SCHEMA_VERSION) {
229 drop(txn);
230 drop(db);
231 std::fs::remove_file(path).map_err(StoreError::Reset)?;
232 }
233 }
234 let store = Self {
235 db: Db::Writable(open_retrying(path, |p| Database::create(p))?),
236 };
237 let txn = store.db.begin_write()?;
238 {
239 let mut meta = txn.open_table(META)?;
240 meta.insert("schema", SCHEMA_VERSION)?;
241 drop(meta);
242 txn.open_table(NODES)?;
243 txn.open_table(FILE_FACTS)?;
244 txn.open_table(FILE_HASH)?;
245 txn.open_table(FILE_SCOPE)?;
246 txn.open_table(NODE_SCOPE)?;
247 txn.open_multimap_table(OUT_EDGES)?;
248 txn.open_multimap_table(IN_EDGES)?;
249 txn.open_multimap_table(UNRESOLVED)?;
250 txn.open_multimap_table(NAME_REFS)?;
251 txn.open_multimap_table(NAME_NODES)?;
252 txn.open_multimap_table(TRIGRAMS)?;
253 txn.open_multimap_table(TOKENS_WORDS)?;
254 txn.open_multimap_table(BODY_TERMS)?;
255 txn.open_multimap_table(IMPORTS)?;
256 txn.open_table(INTERN)?;
257 txn.open_table(INTERN_REV)?;
258 txn.open_table(RESOLVE_META)?;
259 txn.open_table(PENDING)?;
260 }
261 txn.commit()?;
262 Ok(store)
263 }
264
265 pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
266 Ok(Self {
267 db: Db::Writable(open_retrying(path.as_ref(), |p| Database::open(p))?),
268 })
269 }
270
271 pub fn open_read_only(path: impl AsRef<Path>) -> Result<Self, StoreError> {
276 Ok(Self {
277 db: Db::ReadOnly(open_retrying(path.as_ref(), |p| {
278 redb::ReadOnlyDatabase::open(p)
279 })?),
280 })
281 }
282
283 pub fn is_read_only(&self) -> bool {
286 matches!(self.db, Db::ReadOnly(_))
287 }
288
289 pub fn schema(&self) -> Result<Option<u32>, StoreError> {
291 let txn = self.db.begin_read()?;
292 match txn.open_table(META) {
293 Ok(table) => Ok(table.get("schema")?.map(|g| g.value())),
294 Err(redb::TableError::TableDoesNotExist(_)) => Ok(None),
295 Err(e) => Err(e.into()),
296 }
297 }
298
299 pub fn write_graph(&self, graph: &Graph) -> Result<(), StoreError> {
302 let txn = self.db.begin_write()?;
303 {
304 let mut nodes = txn.open_table(NODES)?;
305 let mut scopes = txn.open_table(FILE_SCOPE)?;
306 let mut out = txn.open_multimap_table(OUT_EDGES)?;
307 let mut inn = txn.open_multimap_table(IN_EDGES)?;
308 for node in graph.nodes() {
309 nodes.insert(node.id.as_str(), postcard::to_allocvec(node)?.as_slice())?;
310 scopes.insert(
311 node.file.as_str(),
312 CorpusScope::classify_path(&node.file).as_str(),
313 )?;
314 }
315 for edge in graph.edges() {
316 let bytes = postcard::to_allocvec(edge)?;
317 out.insert(edge.src.as_str(), bytes.as_slice())?;
318 inn.insert(edge.dst.as_str(), bytes.as_slice())?;
319 }
320 }
321 txn.commit()?;
322 Ok(())
323 }
324
325 pub fn unresolved_count(&self) -> Result<u64, StoreError> {
327 let txn = self.db.begin_read()?;
328 let table = match txn.open_multimap_table(UNRESOLVED) {
329 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(0),
330 other => other?,
331 };
332 Ok(table.len()?)
333 }
334
335 pub fn all_unresolved(&self) -> Result<Vec<Reference>, StoreError> {
338 Ok(self
339 .all_unresolved_details()?
340 .into_iter()
341 .map(|u| u.reference)
342 .collect())
343 }
344
345 pub fn all_unresolved_details(&self) -> Result<Vec<UnresolvedReference>, StoreError> {
349 let txn = self.db.begin_read()?;
350 let table = match txn.open_multimap_table(UNRESOLVED) {
351 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
352 other => other?,
353 };
354 let mut refs = Vec::new();
355 for entry in table.iter()? {
356 let (_, values) = entry?;
357 for guard in values {
358 refs.push(postcard::from_bytes(guard?.value())?);
359 }
360 }
361 Ok(refs)
362 }
363
364 pub fn unresolved_refs(
368 &self,
369 file: Option<&str>,
370 name: Option<&str>,
371 ) -> Result<Vec<Reference>, StoreError> {
372 let mut refs = match file {
373 Some(file) => self.references_in(file)?,
374 None => self.all_unresolved()?,
375 };
376 if let Some(name) = name {
377 refs.retain(|r| name_tail_matches(&r.name, name));
378 }
379 Ok(refs)
380 }
381
382 pub fn unresolved_details(
383 &self,
384 file: Option<&str>,
385 name: Option<&str>,
386 ) -> Result<Vec<UnresolvedReference>, StoreError> {
387 let mut refs = match file {
388 Some(file) => self.unresolved_details_in(file)?,
389 None => self.all_unresolved_details()?,
390 };
391 if let Some(name) = name {
392 refs.retain(|u| name_tail_matches(&u.reference.name, name));
393 }
394 Ok(refs)
395 }
396
397 pub fn references_in(&self, file: &str) -> Result<Vec<Reference>, StoreError> {
399 Ok(self
400 .unresolved_details_in(file)?
401 .into_iter()
402 .map(|u| u.reference)
403 .collect())
404 }
405
406 pub fn unresolved_details_in(
407 &self,
408 file: &str,
409 ) -> Result<Vec<UnresolvedReference>, StoreError> {
410 let txn = self.db.begin_read()?;
411 let table = match txn.open_multimap_table(UNRESOLVED) {
412 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
413 other => other?,
414 };
415 let mut refs = Vec::new();
416 for guard in table.get(file)? {
417 refs.push(postcard::from_bytes(guard?.value())?);
418 }
419 Ok(refs)
420 }
421
422 pub fn resolve_fingerprint(&self, key: &str) -> Result<Option<String>, StoreError> {
425 let txn = self.db.begin_read()?;
426 let table = match txn.open_table(RESOLVE_META) {
427 Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
428 other => other?,
429 };
430 Ok(table.get(key)?.map(|g| g.value().to_string()))
431 }
432
433 pub fn set_resolve_fingerprint(
437 &self,
438 key: &str,
439 fingerprint: Option<&str>,
440 ) -> Result<(), StoreError> {
441 if self.resolve_fingerprint(key)?.as_deref() == fingerprint {
442 return Ok(());
443 }
444 let txn = self.db.begin_write()?;
445 {
446 let mut table = txn.open_table(RESOLVE_META)?;
447 match fingerprint {
448 Some(f) => {
449 table.insert(key, f)?;
450 }
451 None => {
452 table.remove(key)?;
453 }
454 }
455 }
456 txn.commit()?;
457 Ok(())
458 }
459
460 pub fn unresolved_named(&self, name: &str) -> Result<usize, StoreError> {
464 let files = self.ref_files(&std::collections::BTreeSet::from([name.to_string()]))?;
465 let mut count = 0;
466 for file in files {
467 count += self
468 .references_in(&file)?
469 .iter()
470 .filter(|r| name_tail_matches(&r.name, name))
471 .count();
472 }
473 Ok(count)
474 }
475
476 pub fn node(&self, id: &NodeId) -> Result<Option<Node>, StoreError> {
477 let txn = self.db.begin_read()?;
478 let table = txn.open_table(NODES)?;
479 match table.get(id.as_str())? {
480 Some(guard) => Ok(Some(postcard::from_bytes(guard.value())?)),
481 None => Ok(None),
482 }
483 }
484
485 pub fn node_count(&self) -> Result<u64, StoreError> {
486 let txn = self.db.begin_read()?;
487 Ok(txn.open_table(NODES)?.len()?)
488 }
489
490 pub fn edge_count(&self) -> Result<u64, StoreError> {
491 let txn = self.db.begin_read()?;
492 Ok(txn.open_multimap_table(OUT_EDGES)?.len()?)
493 }
494
495 pub fn out_edges(&self, id: &NodeId) -> Result<Vec<Edge>, StoreError> {
497 self.adjacent(OUT_EDGES, id)
498 }
499
500 pub fn in_edges(&self, id: &NodeId) -> Result<Vec<Edge>, StoreError> {
502 self.adjacent(IN_EDGES, id)
503 }
504
505 pub fn in_edges_many(&self, ids: &[NodeId]) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
508 self.adjacent_many(IN_EDGES, ids)
509 }
510
511 pub fn out_edges_many(&self, ids: &[NodeId]) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
513 self.adjacent_many(OUT_EDGES, ids)
514 }
515
516 fn adjacent_many(
517 &self,
518 table: MultimapTableDefinition<&str, &[u8]>,
519 ids: &[NodeId],
520 ) -> Result<HashMap<NodeId, Vec<Edge>>, StoreError> {
521 let txn = self.db.begin_read()?;
522 let table = txn.open_multimap_table(table)?;
523 let mut found = HashMap::with_capacity(ids.len());
524 for id in ids {
525 let mut edges = Vec::new();
526 for guard in table.get(id.as_str())? {
527 edges.push(postcard::from_bytes(guard?.value())?);
528 }
529 found.insert(id.clone(), edges);
530 }
531 Ok(found)
532 }
533
534 fn adjacent(
535 &self,
536 table: MultimapTableDefinition<&str, &[u8]>,
537 id: &NodeId,
538 ) -> Result<Vec<Edge>, StoreError> {
539 let txn = self.db.begin_read()?;
540 let table = txn.open_multimap_table(table)?;
541 let mut edges = Vec::new();
542 for guard in table.get(id.as_str())? {
543 edges.push(postcard::from_bytes(guard?.value())?);
544 }
545 Ok(edges)
546 }
547
548 pub fn file_hashes(&self) -> Result<Vec<(String, FileStamp)>, StoreError> {
550 let txn = self.db.begin_read()?;
551 let table = txn.open_table(FILE_HASH)?;
552 let mut out = Vec::new();
553 for entry in table.iter()? {
554 let (k, v) = entry?;
555 out.push((k.value().to_string(), FileStamp::decode(v.value())));
556 }
557 Ok(out)
558 }
559
560 pub fn set_file_scopes(&self, rows: &[(String, CorpusScope)]) -> Result<usize, StoreError> {
566 if rows.is_empty() {
567 return Ok(0);
568 }
569 let existing = self.file_scopes()?;
570 let stale: Vec<&(String, CorpusScope)> = rows
571 .iter()
572 .filter(|(file, scope)| existing.get(file) != Some(scope))
573 .collect();
574 if stale.is_empty() {
575 return Ok(0);
576 }
577 let txn = self.db.begin_write()?;
578 {
579 let mut table = txn.open_table(FILE_SCOPE)?;
580 for (file, scope) in &stale {
581 table.insert(file.as_str(), scope.as_str())?;
582 }
583 }
584 txn.commit()?;
585 Ok(stale.len())
586 }
587
588 pub fn file_scopes(&self) -> Result<HashMap<String, CorpusScope>, StoreError> {
591 let txn = self.db.begin_read()?;
592 let table = txn.open_table(FILE_SCOPE)?;
593 let mut scopes = HashMap::new();
594 for entry in table.iter()? {
595 let (file, scope) = entry?;
596 let file = file.value().to_string();
597 scopes.insert(
598 file.clone(),
599 CorpusScope::from_str_opt(scope.value())
600 .unwrap_or_else(|| CorpusScope::classify_path(&file)),
601 );
602 }
603 Ok(scopes)
604 }
605
606 pub fn file_scope(&self, file: &str) -> Result<CorpusScope, StoreError> {
607 let txn = self.db.begin_read()?;
608 let table = txn.open_table(FILE_SCOPE)?;
609 Ok(table
610 .get(file)?
611 .and_then(|guard| CorpusScope::from_str_opt(guard.value()))
612 .unwrap_or_else(|| CorpusScope::classify_path(file)))
613 }
614
615 pub fn facts(&self, file: &str) -> Result<Option<FileFacts>, StoreError> {
616 let txn = self.db.begin_read()?;
617 let table = txn.open_table(FILE_FACTS)?;
618 match table.get(file)? {
619 Some(guard) => Ok(Some(crate::update::decode_facts(guard.value())?)),
620 None => Ok(None),
621 }
622 }
623
624 pub fn syntax_error_files(&self) -> Result<Vec<String>, StoreError> {
628 let txn = self.db.begin_read()?;
629 let table = txn.open_table(FILE_FACTS)?;
630 let mut files = Vec::new();
631 for entry in table.iter()? {
632 let (file, bytes) = entry?;
633 let facts = crate::update::decode_facts(bytes.value())?;
634 if facts.has_syntax_errors {
635 files.push(file.value().to_string());
636 }
637 }
638 files.sort();
639 Ok(files)
640 }
641
642 pub fn compact(&mut self) -> Result<bool, StoreError> {
647 let mut any = false;
648 for _ in 0..16 {
649 if !self.db.compact()? {
650 break;
651 }
652 any = true;
653 }
654 Ok(any)
655 }
656
657 pub fn all_imports(&self) -> Result<Vec<Reference>, StoreError> {
659 let txn = self.db.begin_read()?;
660 let table = txn.open_multimap_table(IMPORTS)?;
661 let mut refs = Vec::new();
662 for entry in table.iter()? {
663 let (_, values) = entry?;
664 for guard in values {
665 refs.push(postcard::from_bytes(guard?.value())?);
666 }
667 }
668 Ok(refs)
669 }
670
671 pub fn all_nodes(&self) -> Result<Vec<Node>, StoreError> {
674 let txn = self.db.begin_read()?;
675 let table = txn.open_table(NODES)?;
676 let mut nodes = Vec::new();
677 for entry in table.iter()? {
678 nodes.push(postcard::from_bytes(entry?.1.value())?);
679 }
680 Ok(nodes)
681 }
682
683 pub fn in_degrees(&self) -> Result<Vec<(String, usize)>, StoreError> {
688 let txn = self.db.begin_read()?;
689 let table = txn.open_multimap_table(IN_EDGES)?;
690 let mut out = Vec::new();
691 for entry in table.iter()? {
692 let (key, values) = entry?;
693 let mut n = 0usize;
694 for guard in values {
695 let edge: Edge = postcard::from_bytes(guard?.value())?;
696 if edge.relation != sinter_core::Relation::Contains {
697 n += 1;
698 }
699 }
700 if n > 0 {
701 out.push((key.value().to_string(), n));
702 }
703 }
704 Ok(out)
705 }
706
707 pub fn read_graph(&self) -> Result<Graph, StoreError> {
710 let txn = self.db.begin_read()?;
711 let mut graph = Graph::new();
712 {
713 let nodes = txn.open_table(NODES)?;
714 for entry in nodes.iter()? {
715 let (_, value) = entry?;
716 graph.add_node(postcard::from_bytes(value.value())?)?;
717 }
718 }
719 {
720 let out = txn.open_multimap_table(OUT_EDGES)?;
721 for entry in out.iter()? {
722 let (_, values) = entry?;
723 for guard in values {
724 graph.add_edge(postcard::from_bytes(guard?.value())?)?;
725 }
726 }
727 }
728 Ok(graph)
729 }
730}
731
732fn name_tail_matches(written: &str, name: &str) -> bool {
735 let tail = written.rsplit("::").next().unwrap_or(written);
739 let tail = tail.rsplit(['/', '.']).next().unwrap_or(tail);
740 tail == name
741}