1use std::collections::HashSet;
2use std::fs::File;
3use std::path::Path;
4use std::time::{Duration, Instant};
5
6use fs2::FileExt;
7
8use crate::error::GriteError;
9use crate::types::event::{DependencyType, Event, EventKind};
10use crate::types::ids::{EventId, IssueId};
11use crate::types::issue::{IssueProjection, IssueSummary};
12use crate::types::event::IssueState;
13use crate::types::context::{FileContext, ProjectContextEntry};
14use crate::types::issue::Version;
15
16pub const DEFAULT_REBUILD_EVENTS_THRESHOLD: usize = 10000;
18
19pub const DEFAULT_REBUILD_DAYS_THRESHOLD: u32 = 7;
21
22#[derive(Debug, Default)]
24pub struct IssueFilter {
25 pub state: Option<IssueState>,
26 pub label: Option<String>,
27}
28
29#[derive(Debug)]
31pub struct DbStats {
32 pub path: String,
33 pub size_bytes: u64,
34 pub event_count: usize,
35 pub issue_count: usize,
36 pub last_rebuild_ts: Option<u64>,
37 pub events_since_rebuild: usize,
39 pub days_since_rebuild: Option<u32>,
41 pub rebuild_recommended: bool,
43}
44
45#[derive(Debug)]
47pub struct RebuildStats {
48 pub event_count: usize,
49 pub issue_count: usize,
50}
51
52pub struct LockedStore {
58 _lock_file: File,
60 store: GritStore,
62}
63
64impl std::fmt::Debug for LockedStore {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("LockedStore")
67 .field("store", &"GritStore { ... }")
68 .finish()
69 }
70}
71
72impl LockedStore {
73 pub fn inner(&self) -> &GritStore {
75 &self.store
76 }
77}
78
79impl std::ops::Deref for LockedStore {
80 type Target = GritStore;
81
82 fn deref(&self) -> &Self::Target {
83 &self.store
84 }
85}
86
87impl std::ops::DerefMut for LockedStore {
88 fn deref_mut(&mut self) -> &mut Self::Target {
89 &mut self.store
90 }
91}
92
93pub struct GritStore {
95 db: sled::Db,
96 events: sled::Tree,
97 issue_states: sled::Tree,
98 issue_events: sled::Tree,
99 label_index: sled::Tree,
100 metadata: sled::Tree,
101 dep_forward: sled::Tree,
102 dep_reverse: sled::Tree,
103 context_files: sled::Tree,
104 context_symbols: sled::Tree,
105 context_project: sled::Tree,
106}
107
108impl GritStore {
109 pub fn open(path: &Path) -> Result<Self, GriteError> {
111 let db = sled::open(path)?;
112 let events = db.open_tree("events")?;
113 let issue_states = db.open_tree("issue_states")?;
114 let issue_events = db.open_tree("issue_events")?;
115 let label_index = db.open_tree("label_index")?;
116 let metadata = db.open_tree("metadata")?;
117 let dep_forward = db.open_tree("dep_forward")?;
118 let dep_reverse = db.open_tree("dep_reverse")?;
119 let context_files = db.open_tree("context_files")?;
120 let context_symbols = db.open_tree("context_symbols")?;
121 let context_project = db.open_tree("context_project")?;
122
123 Ok(Self {
124 db,
125 events,
126 issue_states,
127 issue_events,
128 label_index,
129 metadata,
130 dep_forward,
131 dep_reverse,
132 context_files,
133 context_symbols,
134 context_project,
135 })
136 }
137
138 pub fn open_locked(path: &Path) -> Result<LockedStore, GriteError> {
143 let lock_path = path.with_extension("lock");
144
145 let lock_file = File::create(&lock_path)?;
147
148 lock_file.try_lock_exclusive().map_err(|e| {
150 GriteError::DbBusy(format!("Database locked by another process: {}", e))
151 })?;
152
153 let store = Self::open(path)?;
155
156 Ok(LockedStore {
157 _lock_file: lock_file,
158 store,
159 })
160 }
161
162 pub fn open_locked_blocking(path: &Path, timeout: Duration) -> Result<LockedStore, GriteError> {
167 let lock_path = path.with_extension("lock");
168 let lock_file = File::create(&lock_path)?;
169
170 let start = Instant::now();
171 let mut delay = Duration::from_millis(10);
172
173 loop {
174 match lock_file.try_lock_exclusive() {
175 Ok(()) => break,
176 Err(_) if start.elapsed() < timeout => {
177 std::thread::sleep(delay);
178 delay = (delay * 2).min(Duration::from_millis(200));
179 }
180 Err(e) => {
181 return Err(GriteError::DbBusy(format!(
182 "Timeout waiting for database lock: {}",
183 e
184 )))
185 }
186 }
187 }
188
189 let store = Self::open(path)?;
190 Ok(LockedStore {
191 _lock_file: lock_file,
192 store,
193 })
194 }
195
196 pub fn insert_event(&self, event: &Event) -> Result<(), GriteError> {
198 let event_key = event_key(&event.event_id);
200 let event_json = serde_json::to_vec(event)?;
201 self.events.insert(&event_key, event_json)?;
202
203 let issue_events_key = issue_events_key(&event.issue_id, event.ts_unix_ms, &event.event_id);
205 self.issue_events.insert(&issue_events_key, &[])?;
206
207 self.update_projection(event)?;
209
210 self.increment_events_since_rebuild()?;
212
213 Ok(())
214 }
215
216 fn increment_events_since_rebuild(&self) -> Result<(), GriteError> {
218 let current = self.metadata.get("events_since_rebuild")?.map(|bytes| {
219 let arr: [u8; 8] = bytes.as_ref().try_into().unwrap_or([0; 8]);
220 u64::from_le_bytes(arr)
221 }).unwrap_or(0);
222
223 let new_count = current + 1;
224 self.metadata.insert("events_since_rebuild", &new_count.to_le_bytes())?;
225 Ok(())
226 }
227
228 fn update_projection(&self, event: &Event) -> Result<(), GriteError> {
230 match &event.kind {
232 EventKind::ContextUpdated { path, language, symbols, summary, content_hash } => {
233 return self.update_file_context(event, path, language, symbols, summary, content_hash);
234 }
235 EventKind::ProjectContextUpdated { key, value } => {
236 return self.update_project_context(event, key, value);
237 }
238 _ => {}
239 }
240
241 let issue_key = issue_state_key(&event.issue_id);
242
243 let mut projection = match self.issue_states.get(&issue_key)? {
244 Some(bytes) => serde_json::from_slice(&bytes)?,
245 None => {
246 IssueProjection::from_event(event)?
248 }
249 };
250
251 if self.issue_states.get(&issue_key)?.is_some() {
253 projection.apply(event)?;
254 }
255
256 for label in &projection.labels {
258 let label_key = label_index_key(label, &event.issue_id);
259 self.label_index.insert(&label_key, &[])?;
260 }
261
262 match &event.kind {
264 EventKind::DependencyAdded { target, dep_type } => {
265 let fwd = dep_forward_key(&event.issue_id, target, dep_type);
266 self.dep_forward.insert(&fwd, &[])?;
267 let rev = dep_reverse_key(target, &event.issue_id, dep_type);
268 self.dep_reverse.insert(&rev, &[])?;
269 }
270 EventKind::DependencyRemoved { target, dep_type } => {
271 let fwd = dep_forward_key(&event.issue_id, target, dep_type);
272 self.dep_forward.remove(&fwd)?;
273 let rev = dep_reverse_key(target, &event.issue_id, dep_type);
274 self.dep_reverse.remove(&rev)?;
275 }
276 _ => {}
277 }
278
279 let proj_json = serde_json::to_vec(&projection)?;
281 self.issue_states.insert(&issue_key, proj_json)?;
282
283 Ok(())
284 }
285
286 fn update_file_context(
288 &self,
289 event: &Event,
290 path: &str,
291 language: &str,
292 symbols: &[crate::types::event::SymbolInfo],
293 summary: &str,
294 content_hash: &[u8; 32],
295 ) -> Result<(), GriteError> {
296 let file_key = context_file_key(path);
297 let new_version = Version::new(event.ts_unix_ms, event.actor, event.event_id);
298
299 let should_update = match self.context_files.get(&file_key)? {
300 Some(existing_bytes) => {
301 let existing: FileContext = serde_json::from_slice(&existing_bytes)?;
302 new_version.is_newer_than(&existing.version)
303 }
304 None => true,
305 };
306
307 if should_update {
308 let sym_path_suffix = format!("/{}", path);
310 for result in self.context_symbols.iter() {
311 let (key, _) = result?;
312 if let Ok(key_str) = std::str::from_utf8(&key) {
313 if key_str.ends_with(&sym_path_suffix) {
314 self.context_symbols.remove(&key)?;
315 }
316 }
317 }
318
319 let ctx = FileContext {
320 path: path.to_string(),
321 language: language.to_string(),
322 symbols: symbols.to_vec(),
323 summary: summary.to_string(),
324 content_hash: *content_hash,
325 version: new_version,
326 };
327
328 self.context_files.insert(&file_key, serde_json::to_vec(&ctx)?)?;
330
331 for sym in symbols {
333 let sym_key = context_symbol_key(&sym.name, path);
334 self.context_symbols.insert(&sym_key, &[])?;
335 }
336 }
337
338 Ok(())
339 }
340
341 fn update_project_context(
343 &self,
344 event: &Event,
345 key: &str,
346 value: &str,
347 ) -> Result<(), GriteError> {
348 let proj_key = context_project_key(key);
349 let new_version = Version::new(event.ts_unix_ms, event.actor, event.event_id);
350
351 let should_update = match self.context_project.get(&proj_key)? {
352 Some(existing_bytes) => {
353 let existing: ProjectContextEntry = serde_json::from_slice(&existing_bytes)?;
354 new_version.is_newer_than(&existing.version)
355 }
356 None => true,
357 };
358
359 if should_update {
360 let entry = ProjectContextEntry {
361 value: value.to_string(),
362 version: new_version,
363 };
364 self.context_project.insert(&proj_key, serde_json::to_vec(&entry)?)?;
365 }
366
367 Ok(())
368 }
369
370 pub fn get_event(&self, event_id: &EventId) -> Result<Option<Event>, GriteError> {
372 let key = event_key(event_id);
373 match self.events.get(&key)? {
374 Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
375 None => Ok(None),
376 }
377 }
378
379 pub fn get_issue(&self, issue_id: &IssueId) -> Result<Option<IssueProjection>, GriteError> {
381 let key = issue_state_key(issue_id);
382 match self.issue_states.get(&key)? {
383 Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
384 None => Ok(None),
385 }
386 }
387
388 pub fn list_issues(&self, filter: &IssueFilter) -> Result<Vec<IssueSummary>, GriteError> {
390 let mut summaries = Vec::new();
391
392 for result in self.issue_states.iter() {
393 let (_, value) = result?;
394 let proj: IssueProjection = serde_json::from_slice(&value)?;
395
396 if let Some(state) = filter.state {
398 if proj.state != state {
399 continue;
400 }
401 }
402 if let Some(ref label) = filter.label {
403 if !proj.labels.contains(label) {
404 continue;
405 }
406 }
407
408 summaries.push(IssueSummary::from(&proj));
409 }
410
411 summaries.sort_by(|a, b| a.issue_id.cmp(&b.issue_id));
413
414 Ok(summaries)
415 }
416
417 pub fn get_issue_events(&self, issue_id: &IssueId) -> Result<Vec<Event>, GriteError> {
419 let prefix = issue_events_prefix(issue_id);
420 let mut events = Vec::new();
421
422 for result in self.issue_events.scan_prefix(&prefix) {
423 let (key, _) = result?;
424 let event_id = extract_event_id_from_issue_events_key(&key)?;
426 if let Some(event) = self.get_event(&event_id)? {
427 events.push(event);
428 }
429 }
430
431 events.sort_by(|a, b| {
433 (a.ts_unix_ms, &a.actor, &a.event_id).cmp(&(b.ts_unix_ms, &b.actor, &b.event_id))
434 });
435
436 Ok(events)
437 }
438
439 pub fn get_all_events(&self) -> Result<Vec<Event>, GriteError> {
441 let mut events = Vec::new();
442 for result in self.events.iter() {
443 let (_, value) = result?;
444 let event: Event = serde_json::from_slice(&value)?;
445 events.push(event);
446 }
447 events.sort_by(|a, b| {
449 (&a.issue_id, a.ts_unix_ms, &a.actor, &a.event_id)
450 .cmp(&(&b.issue_id, b.ts_unix_ms, &b.actor, &b.event_id))
451 });
452 Ok(events)
453 }
454
455 pub fn rebuild(&self) -> Result<RebuildStats, GriteError> {
457 self.issue_states.clear()?;
459 self.label_index.clear()?;
460 self.dep_forward.clear()?;
461 self.dep_reverse.clear()?;
462 self.context_files.clear()?;
463 self.context_symbols.clear()?;
464 self.context_project.clear()?;
465
466 let mut events = self.get_all_events()?;
468
469 events.sort_by(|a, b| {
471 (&a.issue_id, a.ts_unix_ms, &a.actor, &a.event_id)
472 .cmp(&(&b.issue_id, b.ts_unix_ms, &b.actor, &b.event_id))
473 });
474
475 for event in &events {
477 self.update_projection(event)?;
478 }
479
480 let issue_count = self.issue_states.len();
481
482 let now = std::time::SystemTime::now()
484 .duration_since(std::time::UNIX_EPOCH)
485 .unwrap()
486 .as_millis() as u64;
487 self.metadata.insert("last_rebuild_ts", &now.to_le_bytes())?;
488 self.metadata.insert("events_since_rebuild", &0u64.to_le_bytes())?;
489
490 Ok(RebuildStats {
491 event_count: events.len(),
492 issue_count,
493 })
494 }
495
496 pub fn rebuild_from_events(&self, events: &[Event]) -> Result<RebuildStats, GriteError> {
501 self.issue_states.clear()?;
503 self.label_index.clear()?;
504 self.dep_forward.clear()?;
505 self.dep_reverse.clear()?;
506 self.context_files.clear()?;
507 self.context_symbols.clear()?;
508 self.context_project.clear()?;
509 self.events.clear()?;
510
511 let mut sorted_events: Vec<_> = events.to_vec();
513 sorted_events.sort_by(|a, b| {
514 (&a.issue_id, a.ts_unix_ms, &a.actor, &a.event_id)
515 .cmp(&(&b.issue_id, b.ts_unix_ms, &b.actor, &b.event_id))
516 });
517
518 for event in &sorted_events {
520 let ev_key = event_key(&event.event_id);
522 let event_json = serde_json::to_vec(event)?;
523 self.events.insert(&ev_key, event_json)?;
524
525 let ie_key = issue_events_key(&event.issue_id, event.ts_unix_ms, &event.event_id);
527 self.issue_events.insert(&ie_key, &[])?;
528
529 self.update_projection(event)?;
531 }
532
533 let issue_count = self.issue_states.len();
534
535 let now = std::time::SystemTime::now()
537 .duration_since(std::time::UNIX_EPOCH)
538 .unwrap()
539 .as_millis() as u64;
540 self.metadata.insert("last_rebuild_ts", &now.to_le_bytes())?;
541 self.metadata.insert("events_since_rebuild", &0u64.to_le_bytes())?;
542
543 Ok(RebuildStats {
544 event_count: sorted_events.len(),
545 issue_count,
546 })
547 }
548
549 pub fn stats(&self, path: &Path) -> Result<DbStats, GriteError> {
551 let event_count = self.events.len();
552 let issue_count = self.issue_states.len();
553
554 let size_bytes = dir_size(path).unwrap_or(0);
556
557 let last_rebuild_ts = self.metadata.get("last_rebuild_ts")?.map(|bytes| {
558 let arr: [u8; 8] = bytes.as_ref().try_into().unwrap_or([0; 8]);
559 u64::from_le_bytes(arr)
560 });
561
562 let events_since_rebuild = self.metadata.get("events_since_rebuild")?.map(|bytes| {
563 let arr: [u8; 8] = bytes.as_ref().try_into().unwrap_or([0; 8]);
564 u64::from_le_bytes(arr) as usize
565 }).unwrap_or(event_count); let now_ms = std::time::SystemTime::now()
569 .duration_since(std::time::UNIX_EPOCH)
570 .unwrap()
571 .as_millis() as u64;
572
573 let days_since_rebuild = last_rebuild_ts.map(|ts| {
574 let ms_diff = now_ms.saturating_sub(ts);
575 (ms_diff / (24 * 60 * 60 * 1000)) as u32
576 });
577
578 let rebuild_recommended = events_since_rebuild > DEFAULT_REBUILD_EVENTS_THRESHOLD
580 || days_since_rebuild.map(|d| d > DEFAULT_REBUILD_DAYS_THRESHOLD).unwrap_or(false);
581
582 Ok(DbStats {
583 path: path.to_string_lossy().to_string(),
584 size_bytes,
585 event_count,
586 issue_count,
587 last_rebuild_ts,
588 events_since_rebuild,
589 days_since_rebuild,
590 rebuild_recommended,
591 })
592 }
593
594 pub fn get_dependencies(&self, issue_id: &IssueId) -> Result<Vec<(IssueId, DependencyType)>, GriteError> {
598 let prefix = dep_forward_prefix(issue_id);
599 let mut deps = Vec::new();
600
601 for result in self.dep_forward.scan_prefix(&prefix) {
602 let (key, _) = result?;
603 if let Some((target, dep_type)) = parse_dep_key_suffix(&key, prefix.len()) {
604 deps.push((target, dep_type));
605 }
606 }
607
608 Ok(deps)
609 }
610
611 pub fn get_dependents(&self, issue_id: &IssueId) -> Result<Vec<(IssueId, DependencyType)>, GriteError> {
613 let prefix = dep_reverse_prefix(issue_id);
614 let mut deps = Vec::new();
615
616 for result in self.dep_reverse.scan_prefix(&prefix) {
617 let (key, _) = result?;
618 if let Some((source, dep_type)) = parse_dep_key_suffix(&key, prefix.len()) {
619 deps.push((source, dep_type));
620 }
621 }
622
623 Ok(deps)
624 }
625
626 pub fn would_create_cycle(
629 &self,
630 source: &IssueId,
631 target: &IssueId,
632 dep_type: &DependencyType,
633 ) -> Result<bool, GriteError> {
634 if !dep_type.is_acyclic() {
635 return Ok(false);
636 }
637
638 let mut visited = HashSet::new();
640 let mut stack = vec![*target];
641
642 while let Some(current) = stack.pop() {
643 if current == *source {
644 return Ok(true);
645 }
646 if !visited.insert(current) {
647 continue;
648 }
649 for (dep_target, dt) in self.get_dependencies(¤t)? {
650 if dt == *dep_type {
651 stack.push(dep_target);
652 }
653 }
654 }
655
656 Ok(false)
657 }
658
659 pub fn topological_order(&self, filter: &IssueFilter) -> Result<Vec<IssueSummary>, GriteError> {
662 let issues = self.list_issues(filter)?;
663 let issue_ids: HashSet<IssueId> = issues.iter().map(|i| i.issue_id).collect();
664
665 let mut in_degree: std::collections::HashMap<IssueId, usize> = std::collections::HashMap::new();
667 let mut adj: std::collections::HashMap<IssueId, Vec<IssueId>> = std::collections::HashMap::new();
668
669 for issue in &issues {
670 in_degree.entry(issue.issue_id).or_insert(0);
671 adj.entry(issue.issue_id).or_default();
672
673 for (target, dep_type) in self.get_dependencies(&issue.issue_id)? {
674 if dep_type.is_acyclic() && issue_ids.contains(&target) {
675 adj.entry(target).or_default().push(issue.issue_id);
677 *in_degree.entry(issue.issue_id).or_insert(0) += 1;
678 }
679 }
680 }
681
682 let mut queue: std::collections::VecDeque<IssueId> = in_degree.iter()
684 .filter(|(_, °)| deg == 0)
685 .map(|(&id, _)| id)
686 .collect();
687
688 let mut sorted_ids = Vec::new();
689 while let Some(id) = queue.pop_front() {
690 sorted_ids.push(id);
691 if let Some(neighbors) = adj.get(&id) {
692 for &neighbor in neighbors {
693 if let Some(deg) = in_degree.get_mut(&neighbor) {
694 *deg -= 1;
695 if *deg == 0 {
696 queue.push_back(neighbor);
697 }
698 }
699 }
700 }
701 }
702
703 for issue in &issues {
705 if !sorted_ids.contains(&issue.issue_id) {
706 sorted_ids.push(issue.issue_id);
707 }
708 }
709
710 let issue_map: std::collections::HashMap<IssueId, &IssueSummary> =
712 issues.iter().map(|i| (i.issue_id, i)).collect();
713 let result = sorted_ids.iter()
714 .filter_map(|id| issue_map.get(id).map(|s| (*s).clone()))
715 .collect();
716
717 Ok(result)
718 }
719
720 pub fn get_file_context(&self, path: &str) -> Result<Option<FileContext>, GriteError> {
724 let key = context_file_key(path);
725 match self.context_files.get(&key)? {
726 Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
727 None => Ok(None),
728 }
729 }
730
731 pub fn query_symbols(&self, query: &str) -> Result<Vec<(String, String)>, GriteError> {
733 let prefix = context_symbol_prefix(query);
734 let mut results = Vec::new();
735
736 for result in self.context_symbols.scan_prefix(&prefix) {
737 let (key, _) = result?;
738 if let Ok(key_str) = std::str::from_utf8(&key) {
739 if let Some(rest) = key_str.strip_prefix("ctx/sym/") {
741 if let Some(slash_pos) = rest.find('/') {
742 let name = rest[..slash_pos].to_string();
743 let path = rest[slash_pos + 1..].to_string();
744 results.push((name, path));
745 }
746 }
747 }
748 }
749
750 Ok(results)
751 }
752
753 pub fn list_context_files(&self) -> Result<Vec<String>, GriteError> {
755 let mut paths = Vec::new();
756 for result in self.context_files.iter() {
757 let (key, _) = result?;
758 if let Ok(key_str) = std::str::from_utf8(&key) {
759 if let Some(path) = key_str.strip_prefix("ctx/file/") {
760 paths.push(path.to_string());
761 }
762 }
763 }
764 Ok(paths)
765 }
766
767 pub fn get_project_context(&self, key: &str) -> Result<Option<ProjectContextEntry>, GriteError> {
769 let k = context_project_key(key);
770 match self.context_project.get(&k)? {
771 Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
772 None => Ok(None),
773 }
774 }
775
776 pub fn list_project_context(&self) -> Result<Vec<(String, ProjectContextEntry)>, GriteError> {
778 let mut entries = Vec::new();
779 for result in self.context_project.iter() {
780 let (key, value) = result?;
781 if let Ok(key_str) = std::str::from_utf8(&key) {
782 if let Some(k) = key_str.strip_prefix("ctx/proj/") {
783 let entry: ProjectContextEntry = serde_json::from_slice(&value)?;
784 entries.push((k.to_string(), entry));
785 }
786 }
787 }
788 Ok(entries)
789 }
790
791 pub fn flush(&self) -> Result<(), GriteError> {
793 self.db.flush()?;
794 Ok(())
795 }
796}
797
798fn event_key(event_id: &EventId) -> Vec<u8> {
801 let mut key = Vec::with_capacity(6 + 32);
802 key.extend_from_slice(b"event/");
803 key.extend_from_slice(event_id);
804 key
805}
806
807fn issue_state_key(issue_id: &IssueId) -> Vec<u8> {
808 let mut key = Vec::with_capacity(12 + 16);
809 key.extend_from_slice(b"issue_state/");
810 key.extend_from_slice(issue_id);
811 key
812}
813
814fn issue_events_prefix(issue_id: &IssueId) -> Vec<u8> {
815 let mut key = Vec::with_capacity(13 + 16);
816 key.extend_from_slice(b"issue_events/");
817 key.extend_from_slice(issue_id);
818 key.push(b'/');
819 key
820}
821
822fn issue_events_key(issue_id: &IssueId, ts: u64, event_id: &EventId) -> Vec<u8> {
823 let mut key = issue_events_prefix(issue_id);
824 key.extend_from_slice(&ts.to_be_bytes());
825 key.push(b'/');
826 key.extend_from_slice(event_id);
827 key
828}
829
830fn label_index_key(label: &str, issue_id: &IssueId) -> Vec<u8> {
831 let mut key = Vec::with_capacity(12 + label.len() + 1 + 16);
832 key.extend_from_slice(b"label_index/");
833 key.extend_from_slice(label.as_bytes());
834 key.push(b'/');
835 key.extend_from_slice(issue_id);
836 key
837}
838
839fn dep_type_to_byte(dep_type: &DependencyType) -> u8 {
842 match dep_type {
843 DependencyType::Blocks => b'B',
844 DependencyType::DependsOn => b'D',
845 DependencyType::RelatedTo => b'R',
846 }
847}
848
849fn byte_to_dep_type(b: u8) -> Option<DependencyType> {
850 match b {
851 b'B' => Some(DependencyType::Blocks),
852 b'D' => Some(DependencyType::DependsOn),
853 b'R' => Some(DependencyType::RelatedTo),
854 _ => None,
855 }
856}
857
858fn dep_forward_prefix(source: &IssueId) -> Vec<u8> {
859 let mut key = Vec::with_capacity(8 + 16 + 1);
860 key.extend_from_slice(b"dep_fwd/");
861 key.extend_from_slice(source);
862 key.push(b'/');
863 key
864}
865
866fn dep_forward_key(source: &IssueId, target: &IssueId, dep_type: &DependencyType) -> Vec<u8> {
867 let mut key = dep_forward_prefix(source);
868 key.extend_from_slice(target);
869 key.push(b'/');
870 key.push(dep_type_to_byte(dep_type));
871 key
872}
873
874fn dep_reverse_prefix(target: &IssueId) -> Vec<u8> {
875 let mut key = Vec::with_capacity(8 + 16 + 1);
876 key.extend_from_slice(b"dep_rev/");
877 key.extend_from_slice(target);
878 key.push(b'/');
879 key
880}
881
882fn dep_reverse_key(target: &IssueId, source: &IssueId, dep_type: &DependencyType) -> Vec<u8> {
883 let mut key = dep_reverse_prefix(target);
884 key.extend_from_slice(source);
885 key.push(b'/');
886 key.push(dep_type_to_byte(dep_type));
887 key
888}
889
890fn parse_dep_key_suffix(key: &[u8], prefix_len: usize) -> Option<(IssueId, DependencyType)> {
892 let suffix = &key[prefix_len..];
894 if suffix.len() != 16 + 1 + 1 {
895 return None;
896 }
897 let mut issue_id = [0u8; 16];
898 issue_id.copy_from_slice(&suffix[..16]);
899 let dep_type = byte_to_dep_type(suffix[17])?;
901 Some((issue_id, dep_type))
902}
903
904fn context_file_key(path: &str) -> Vec<u8> {
907 let mut key = Vec::new();
908 key.extend_from_slice(b"ctx/file/");
909 key.extend_from_slice(path.as_bytes());
910 key
911}
912
913fn context_symbol_prefix(name: &str) -> Vec<u8> {
914 let mut key = Vec::new();
915 key.extend_from_slice(b"ctx/sym/");
916 key.extend_from_slice(name.as_bytes());
917 key
918}
919
920fn context_symbol_key(name: &str, path: &str) -> Vec<u8> {
921 let mut key = context_symbol_prefix(name);
922 key.push(b'/');
923 key.extend_from_slice(path.as_bytes());
924 key
925}
926
927fn context_project_key(key_name: &str) -> Vec<u8> {
928 let mut key = Vec::new();
929 key.extend_from_slice(b"ctx/proj/");
930 key.extend_from_slice(key_name.as_bytes());
931 key
932}
933
934fn extract_event_id_from_issue_events_key(key: &[u8]) -> Result<EventId, GriteError> {
935 if key.len() < 71 {
938 return Err(GriteError::Internal("Invalid issue_events key".to_string()));
939 }
940 let event_id_start = key.len() - 32;
941 let mut event_id = [0u8; 32];
942 event_id.copy_from_slice(&key[event_id_start..]);
943 Ok(event_id)
944}
945
946fn dir_size(path: &Path) -> std::io::Result<u64> {
947 let mut size = 0;
948 if path.is_dir() {
949 for entry in std::fs::read_dir(path)? {
950 let entry = entry?;
951 let meta = entry.metadata()?;
952 if meta.is_dir() {
953 size += dir_size(&entry.path())?;
954 } else {
955 size += meta.len();
956 }
957 }
958 }
959 Ok(size)
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use crate::hash::compute_event_id;
966 use crate::types::event::EventKind;
967 use crate::types::ids::generate_issue_id;
968 use tempfile::tempdir;
969
970 fn make_event(issue_id: IssueId, actor: [u8; 16], ts: u64, kind: EventKind) -> Event {
971 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
972 Event::new(event_id, issue_id, actor, ts, None, kind)
973 }
974
975 #[test]
976 fn test_store_basic_operations() {
977 let dir = tempdir().unwrap();
978 let store = GritStore::open(dir.path()).unwrap();
979
980 let issue_id = generate_issue_id();
981 let actor = [1u8; 16];
982
983 let create_event = make_event(
985 issue_id,
986 actor,
987 1000,
988 EventKind::IssueCreated {
989 title: "Test Issue".to_string(),
990 body: "Test body".to_string(),
991 labels: vec!["bug".to_string()],
992 },
993 );
994
995 store.insert_event(&create_event).unwrap();
996
997 let retrieved = store.get_event(&create_event.event_id).unwrap().unwrap();
999 assert_eq!(retrieved.event_id, create_event.event_id);
1000
1001 let proj = store.get_issue(&issue_id).unwrap().unwrap();
1003 assert_eq!(proj.title, "Test Issue");
1004 assert!(proj.labels.contains("bug"));
1005 }
1006
1007 #[test]
1008 fn test_store_list_issues() {
1009 let dir = tempdir().unwrap();
1010 let store = GritStore::open(dir.path()).unwrap();
1011
1012 let actor = [1u8; 16];
1013
1014 for i in 0..2 {
1016 let issue_id = generate_issue_id();
1017 let event = make_event(
1018 issue_id,
1019 actor,
1020 1000 + i,
1021 EventKind::IssueCreated {
1022 title: format!("Issue {}", i),
1023 body: "Body".to_string(),
1024 labels: vec![],
1025 },
1026 );
1027 store.insert_event(&event).unwrap();
1028 }
1029
1030 let issues = store.list_issues(&IssueFilter::default()).unwrap();
1031 assert_eq!(issues.len(), 2);
1032 }
1033
1034 #[test]
1035 fn test_store_rebuild() {
1036 let dir = tempdir().unwrap();
1037 let store = GritStore::open(dir.path()).unwrap();
1038
1039 let issue_id = generate_issue_id();
1040 let actor = [1u8; 16];
1041
1042 let events = vec![
1044 make_event(
1045 issue_id,
1046 actor,
1047 1000,
1048 EventKind::IssueCreated {
1049 title: "Test".to_string(),
1050 body: "Body".to_string(),
1051 labels: vec![],
1052 },
1053 ),
1054 make_event(
1055 issue_id,
1056 actor,
1057 2000,
1058 EventKind::IssueUpdated {
1059 title: Some("Updated".to_string()),
1060 body: None,
1061 },
1062 ),
1063 ];
1064
1065 for event in &events {
1066 store.insert_event(event).unwrap();
1067 }
1068
1069 let proj_before = store.get_issue(&issue_id).unwrap().unwrap();
1071 assert_eq!(proj_before.title, "Updated");
1072
1073 let stats = store.rebuild().unwrap();
1075 assert_eq!(stats.event_count, 2);
1076 assert_eq!(stats.issue_count, 1);
1077
1078 let proj_after = store.get_issue(&issue_id).unwrap().unwrap();
1080 assert_eq!(proj_after.title, "Updated");
1081 }
1082
1083 #[test]
1084 fn test_locked_store_creates_lock_file() {
1085 let dir = tempdir().unwrap();
1086 let store_path = dir.path().join("sled");
1087 let lock_path = dir.path().join("sled.lock");
1088
1089 assert!(!lock_path.exists());
1091
1092 let _store = GritStore::open_locked(&store_path).unwrap();
1094
1095 assert!(lock_path.exists());
1097 }
1098
1099 #[test]
1100 fn test_locked_store_second_open_fails() {
1101 let dir = tempdir().unwrap();
1102 let store_path = dir.path().join("sled");
1103
1104 let _store1 = GritStore::open_locked(&store_path).unwrap();
1106
1107 let result = GritStore::open_locked(&store_path);
1109 assert!(result.is_err());
1110 match result.unwrap_err() {
1111 GriteError::DbBusy(msg) => {
1112 assert!(msg.contains("locked"));
1113 }
1114 other => panic!("Expected DbBusy error, got {:?}", other),
1115 }
1116 }
1117
1118 #[test]
1119 fn test_locked_store_released_on_drop() {
1120 let dir = tempdir().unwrap();
1121 let store_path = dir.path().join("sled");
1122
1123 {
1125 let _store = GritStore::open_locked(&store_path).unwrap();
1126 }
1128
1129 let _store2 = GritStore::open_locked(&store_path).unwrap();
1131 }
1132
1133 #[test]
1134 fn test_locked_store_blocking_timeout() {
1135 let dir = tempdir().unwrap();
1136 let store_path = dir.path().join("sled");
1137
1138 let _store1 = GritStore::open_locked(&store_path).unwrap();
1140
1141 let result = GritStore::open_locked_blocking(&store_path, Duration::from_millis(50));
1143 assert!(result.is_err());
1144 match result.unwrap_err() {
1145 GriteError::DbBusy(msg) => {
1146 assert!(msg.contains("Timeout"));
1147 }
1148 other => panic!("Expected DbBusy timeout error, got {:?}", other),
1149 }
1150 }
1151
1152 #[test]
1153 fn test_locked_store_deref_access() {
1154 let dir = tempdir().unwrap();
1155 let store_path = dir.path().join("sled");
1156
1157 let store = GritStore::open_locked(&store_path).unwrap();
1158
1159 let issue_id = generate_issue_id();
1161 let actor = [1u8; 16];
1162 let event = make_event(
1163 issue_id,
1164 actor,
1165 1000,
1166 EventKind::IssueCreated {
1167 title: "Test".to_string(),
1168 body: "Body".to_string(),
1169 labels: vec![],
1170 },
1171 );
1172
1173 store.insert_event(&event).unwrap();
1175 let retrieved = store.get_event(&event.event_id).unwrap();
1176 assert!(retrieved.is_some());
1177 }
1178}