Skip to main content

relay_knowledge/storage/
mod.rs

1//! Storage contracts and SQLite-backed graph state.
2//!
3//! Storage owns persisted graph facts, mutation log entries, derived index
4//! metadata, and health snapshots. Domain and interface modules must not depend
5//! on SQL or concrete database types.
6
7mod canvas;
8mod code;
9mod file_index;
10mod sqlite;
11
12use std::{error::Error, fmt, future::Future, pin::Pin};
13
14use serde::{Deserialize, Serialize};
15
16use crate::domain::{
17    AuditEventRecord, AuditStatus, CodeChunkRecord, CodeGraphBatch, CodeGraphCommitReceipt,
18    CodeParseStatusCounts, CodeReferenceRecord, CodeSymbolRecord, CommitReceipt,
19    GraphMutationBatch, GraphVersion, IndexKind, IndexModality, IndexStatus,
20    ProposalConflictRecord, ProposalConflictSeverity, ProposalKind, ProposalProvenance,
21    ProposalRecord, ProposalState, RetrievalHit, RetrieverSource, ServiceOperatorState,
22    ServiceOperatorStatus, WorkerKind, WorkerStatus, WorkerTaskRecord,
23};
24
25pub use canvas::{
26    GraphCanvasSelection, GraphCanvasStorageEdge, GraphCanvasStorageNode,
27    GraphCanvasStorageRequest, GraphCanvasStorageSnapshot,
28};
29pub use code::{
30    CodeImpactChanges, CodeIndexTaskClaimRequest, CodeIndexTaskCompletion, CodeIndexTaskFailure,
31    CodeIndexTaskSeed, CodeRepositorySetMemberSeed, CodeRepositorySetRefreshTaskClaimRequest,
32    CodeRepositorySetRefreshTaskCompletion, CodeRepositorySetRefreshTaskFailure,
33    CodeRepositorySetRefreshTaskSeed, CodeRepositorySetSeed, CodeRepositoryStore,
34    CodeScopeRetentionRequest,
35};
36pub use file_index::{
37    FileIndexDiagnostics, FileIndexEntry, FileIndexRoot, FileIndexRootStatus, FileIndexRootUpdate,
38    FileIndexScanSummary, FileSearchHit, FileSearchRequest,
39};
40pub use sqlite::SqliteGraphStore;
41
42pub type StorageFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, StorageError>> + Send + 'a>>;
43
44/// Synthetic scope used for graph-wide index work that is not tied to evidence.
45pub const DEFAULT_INDEX_SOURCE_SCOPE: &str = "graph";
46
47/// Graph fact persistence and query contract.
48pub trait GraphStore: Send + Sync {
49    fn commit_mutation_batch(&self, batch: GraphMutationBatch) -> StorageFuture<'_, CommitReceipt>;
50
51    fn inspect_graph(&self) -> StorageFuture<'_, GraphInspection>;
52
53    fn graph_canvas(
54        &self,
55        _request: GraphCanvasStorageRequest,
56    ) -> StorageFuture<'_, GraphCanvasStorageSnapshot> {
57        Box::pin(async {
58            Err(StorageError::InvalidInput(
59                "graph canvas storage is unavailable".to_owned(),
60            ))
61        })
62    }
63
64    fn search(&self, request: GraphSearchRequest) -> StorageFuture<'_, Vec<RetrievalHit>>;
65
66    fn current_graph_version(&self) -> StorageFuture<'_, GraphVersion>;
67}
68
69/// Mutation log contract consumed by reconcilers and indexers.
70pub trait MutationLogStore: Send + Sync {
71    fn read_after(
72        &self,
73        graph_version: GraphVersion,
74        limit: usize,
75    ) -> StorageFuture<'_, Vec<MutationLogEntry>>;
76}
77
78/// Derived index metadata contract.
79pub trait IndexStore: Send + Sync {
80    fn index_statuses(&self) -> StorageFuture<'_, Vec<IndexStatus>>;
81
82    fn mark_refresh_complete(
83        &self,
84        kind: IndexKind,
85        graph_version: GraphVersion,
86    ) -> StorageFuture<'_, IndexStatus>;
87
88    fn index_cursors(&self) -> StorageFuture<'_, Vec<IndexCursor>> {
89        Box::pin(async {
90            Err(StorageError::InvalidInput(
91                "index cursor storage is unavailable".to_owned(),
92            ))
93        })
94    }
95
96    fn queue_index_refreshes(
97        &self,
98        _request: IndexRefreshQueueRequest,
99    ) -> StorageFuture<'_, IndexRefreshDiagnostics> {
100        Box::pin(async {
101            Err(StorageError::InvalidInput(
102                "index refresh task storage is unavailable".to_owned(),
103            ))
104        })
105    }
106
107    fn claim_index_refresh_task(
108        &self,
109        _request: IndexRefreshClaimRequest,
110    ) -> StorageFuture<'_, Option<IndexRefreshTask>> {
111        Box::pin(async {
112            Err(StorageError::InvalidInput(
113                "index refresh task storage is unavailable".to_owned(),
114            ))
115        })
116    }
117
118    fn complete_index_refresh_task(
119        &self,
120        _request: IndexRefreshCompletion,
121    ) -> StorageFuture<'_, IndexRefreshTask> {
122        Box::pin(async {
123            Err(StorageError::InvalidInput(
124                "index refresh task storage is unavailable".to_owned(),
125            ))
126        })
127    }
128
129    fn fail_index_refresh_task(
130        &self,
131        _request: IndexRefreshFailure,
132    ) -> StorageFuture<'_, IndexRefreshTask> {
133        Box::pin(async {
134            Err(StorageError::InvalidInput(
135                "index refresh task storage is unavailable".to_owned(),
136            ))
137        })
138    }
139
140    fn index_refresh_diagnostics(
141        &self,
142        _now_ms: u64,
143    ) -> StorageFuture<'_, IndexRefreshDiagnostics> {
144        Box::pin(async {
145            Err(StorageError::InvalidInput(
146                "index refresh diagnostics are unavailable".to_owned(),
147            ))
148        })
149    }
150
151    fn queue_worker_tasks(
152        &self,
153        _tasks: Vec<WorkerTaskSeed>,
154    ) -> StorageFuture<'_, Vec<WorkerTaskRecord>> {
155        Box::pin(async { Ok(Vec::new()) })
156    }
157
158    fn worker_statuses(&self) -> StorageFuture<'_, Vec<WorkerStatus>> {
159        Box::pin(async { Ok(Vec::new()) })
160    }
161
162    fn claim_worker_task(
163        &self,
164        _request: WorkerTaskClaimRequest,
165    ) -> StorageFuture<'_, Option<WorkerTaskRecord>> {
166        Box::pin(async { Ok(None) })
167    }
168
169    fn complete_worker_task(
170        &self,
171        _request: WorkerTaskCompletion,
172    ) -> StorageFuture<'_, WorkerTaskRecord> {
173        Box::pin(async {
174            Err(StorageError::InvalidInput(
175                "worker task storage is unavailable".to_owned(),
176            ))
177        })
178    }
179
180    fn fail_worker_task(&self, _request: WorkerTaskFailure) -> StorageFuture<'_, WorkerTaskRecord> {
181        Box::pin(async {
182            Err(StorageError::InvalidInput(
183                "worker task storage is unavailable".to_owned(),
184            ))
185        })
186    }
187
188    fn insert_proposal(&self, _proposal: NewProposal) -> StorageFuture<'_, ProposalRecord> {
189        Box::pin(async {
190            Err(StorageError::InvalidInput(
191                "proposal storage is unavailable".to_owned(),
192            ))
193        })
194    }
195
196    fn list_proposals(
197        &self,
198        _request: ProposalListRequest,
199    ) -> StorageFuture<'_, Vec<ProposalRecord>> {
200        Box::pin(async { Ok(Vec::new()) })
201    }
202
203    fn proposal_by_id(&self, _proposal_id: String) -> StorageFuture<'_, Option<ProposalRecord>> {
204        Box::pin(async { Ok(None) })
205    }
206
207    fn proposal_conflicts(
208        &self,
209        _proposal_id: String,
210    ) -> StorageFuture<'_, Vec<ProposalConflictRecord>> {
211        Box::pin(async { Ok(Vec::new()) })
212    }
213
214    fn decide_proposal(&self, _request: ProposalDecision) -> StorageFuture<'_, ProposalRecord> {
215        Box::pin(async {
216            Err(StorageError::InvalidInput(
217                "proposal storage is unavailable".to_owned(),
218            ))
219        })
220    }
221
222    fn insert_audit_event(&self, _event: NewAuditEvent) -> StorageFuture<'_, AuditEventRecord> {
223        Box::pin(async {
224            Err(StorageError::InvalidInput(
225                "audit storage is unavailable".to_owned(),
226            ))
227        })
228    }
229
230    fn query_audit_events(
231        &self,
232        _request: AuditQueryRequest,
233    ) -> StorageFuture<'_, Vec<AuditEventRecord>> {
234        Box::pin(async { Ok(Vec::new()) })
235    }
236
237    fn audit_event_count(&self) -> StorageFuture<'_, usize> {
238        Box::pin(async { Ok(0) })
239    }
240
241    fn service_operator_status(&self) -> StorageFuture<'_, ServiceOperatorStatus> {
242        Box::pin(async {
243            Ok(ServiceOperatorStatus {
244                state: ServiceOperatorState::Disabled,
245                silent_updates_enabled: false,
246                allowed_scopes: Vec::new(),
247                last_run_at_ms: None,
248                next_retry_at_ms: None,
249                last_error: None,
250                updated_at_ms: 0,
251            })
252        })
253    }
254
255    fn update_service_operator(
256        &self,
257        _request: ServiceOperatorUpdate,
258    ) -> StorageFuture<'_, ServiceOperatorStatus> {
259        Box::pin(async {
260            Err(StorageError::InvalidInput(
261                "service operator storage is unavailable".to_owned(),
262            ))
263        })
264    }
265
266    fn replace_file_index_root(
267        &self,
268        _update: FileIndexRootUpdate,
269    ) -> StorageFuture<'_, FileIndexRootStatus> {
270        unavailable_file_index_storage()
271    }
272
273    fn mark_file_index_roots_unconfigured(
274        &self,
275        _active_roots: Vec<FileIndexRoot>,
276        _now_ms: u64,
277    ) -> StorageFuture<'_, FileIndexDiagnostics> {
278        unavailable_file_index_storage()
279    }
280
281    fn search_files(&self, _request: FileSearchRequest) -> StorageFuture<'_, Vec<FileSearchHit>> {
282        unavailable_file_index_storage()
283    }
284
285    fn file_index_diagnostics(&self) -> StorageFuture<'_, FileIndexDiagnostics> {
286        unavailable_file_index_storage()
287    }
288}
289
290fn unavailable_file_index_storage<T>() -> StorageFuture<'static, T> {
291    Box::pin(async {
292        Err(StorageError::InvalidInput(
293            "file index storage is unavailable".to_owned(),
294        ))
295    })
296}
297
298/// Code graph fact persistence and query contract for tree-sitter output.
299pub trait CodeGraphStore: Send + Sync {
300    fn commit_code_graph_batch(
301        &self,
302        batch: CodeGraphBatch,
303    ) -> StorageFuture<'_, CodeGraphCommitReceipt>;
304
305    fn search_code_symbols(
306        &self,
307        request: CodeSymbolSearchRequest,
308    ) -> StorageFuture<'_, Vec<CodeSymbolRecord>>;
309
310    fn search_code_references(
311        &self,
312        request: CodeReferenceSearchRequest,
313    ) -> StorageFuture<'_, Vec<CodeReferenceRecord>>;
314
315    fn search_code_chunks(
316        &self,
317        request: CodeChunkSearchRequest,
318    ) -> StorageFuture<'_, Vec<CodeChunkRecord>>;
319}
320
321/// Combined storage facade used by the application service.
322pub trait KnowledgeStore:
323    GraphStore + MutationLogStore + IndexStore + CodeGraphStore + CodeRepositoryStore
324{
325}
326
327impl<T> KnowledgeStore for T where
328    T: GraphStore + MutationLogStore + IndexStore + CodeGraphStore + CodeRepositoryStore
329{
330}
331
332/// Bounded graph search request against an explicit graph snapshot.
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct GraphSearchRequest {
335    pub query: String,
336    pub source_scope: Option<String>,
337    pub graph_version: GraphVersion,
338    pub limit: usize,
339    pub disabled_retriever_sources: Vec<RetrieverSource>,
340}
341
342impl GraphSearchRequest {
343    /// Returns whether storage may execute a retriever family for this request.
344    pub fn allows_retriever_source(&self, source: RetrieverSource) -> bool {
345        !self.disabled_retriever_sources.contains(&source)
346    }
347}
348
349/// Bounded code symbol search against an explicit graph snapshot.
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct CodeSymbolSearchRequest {
352    pub source_scope: Option<String>,
353    pub path: Option<String>,
354    pub name: Option<String>,
355    pub graph_version: GraphVersion,
356    pub limit: usize,
357}
358
359/// Bounded code reference search against an explicit graph snapshot.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct CodeReferenceSearchRequest {
362    pub source_scope: Option<String>,
363    pub path: Option<String>,
364    pub symbol_text: Option<String>,
365    pub target_symbol_id: Option<String>,
366    pub graph_version: GraphVersion,
367    pub limit: usize,
368}
369
370/// Bounded code chunk search against an explicit graph snapshot.
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct CodeChunkSearchRequest {
373    pub source_scope: Option<String>,
374    pub path: Option<String>,
375    pub query: Option<String>,
376    pub graph_version: GraphVersion,
377    pub limit: usize,
378}
379
380/// Worker task input inserted after graph changes or service reconciliation.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct WorkerTaskSeed {
383    pub kind: WorkerKind,
384    pub source_scope: String,
385    pub evidence_id: Option<String>,
386    pub target_graph_version: GraphVersion,
387    pub input_fingerprint: String,
388    pub payload_json: String,
389    pub now_ms: u64,
390}
391
392/// Worker lease acquisition request.
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct WorkerTaskClaimRequest {
395    pub kind: Option<WorkerKind>,
396    pub lease_owner: String,
397    pub lease_duration_ms: u64,
398    pub max_attempts: u32,
399    pub now_ms: u64,
400}
401
402/// Worker completion guarded by the active lease.
403#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct WorkerTaskCompletion {
405    pub task_id: String,
406    pub lease_owner: String,
407    pub attempt_count: u32,
408    pub now_ms: u64,
409}
410
411/// Worker failure report for retry and dead-letter handling.
412#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct WorkerTaskFailure {
414    pub task_id: String,
415    pub lease_owner: String,
416    pub attempt_count: u32,
417    pub error_kind: String,
418    pub error_message: String,
419    pub retry_backoff_ms: u64,
420    pub max_attempts: u32,
421    pub now_ms: u64,
422}
423
424/// New proposal to persist before manual approval.
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct NewProposal {
427    pub proposal_id: String,
428    pub source_scope: String,
429    pub kind: ProposalKind,
430    pub title: String,
431    pub summary: String,
432    pub payload_json: String,
433    pub origin: String,
434    pub provenance: ProposalProvenance,
435    pub confidence_basis_points: u16,
436    pub conflicts: Vec<NewProposalConflict>,
437    pub now_ms: u64,
438}
439
440/// New proposal conflict to persist with a proposal.
441#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct NewProposalConflict {
443    pub conflict_id: String,
444    pub existing_fact_kind: String,
445    pub existing_fact_id: String,
446    pub severity: ProposalConflictSeverity,
447    pub reason: String,
448}
449
450/// Proposal list filter.
451#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct ProposalListRequest {
453    pub state: Option<ProposalState>,
454    pub limit: usize,
455}
456
457/// Proposal decision request.
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct ProposalDecision {
460    pub proposal_id: String,
461    pub next_state: ProposalState,
462    pub actor: String,
463    pub reason: Option<String>,
464    pub now_ms: u64,
465}
466
467/// New durable audit event.
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub struct NewAuditEvent {
470    pub operation: String,
471    pub interface: String,
472    pub request_id: String,
473    pub trace_id: String,
474    pub status: AuditStatus,
475    pub actor: Option<String>,
476    pub source_scope: Option<String>,
477    pub graph_version: u64,
478    pub detail_json: String,
479    pub message: Option<String>,
480    pub now_ms: u64,
481}
482
483/// Audit query filter.
484#[derive(Debug, Clone, PartialEq, Eq)]
485pub struct AuditQueryRequest {
486    pub operation: Option<String>,
487    pub limit: usize,
488}
489
490/// Service operator state update.
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub struct ServiceOperatorUpdate {
493    pub state: ServiceOperatorState,
494    pub silent_updates_enabled: bool,
495    pub allowed_scopes: Vec<String>,
496    pub last_error: Option<String>,
497    pub now_ms: u64,
498}
499
500/// Aggregated graph status for diagnostics.
501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
502pub struct GraphInspection {
503    pub graph_version: GraphVersion,
504    pub entity_count: usize,
505    pub evidence_count: usize,
506    pub relation_count: usize,
507    pub claim_count: usize,
508    pub event_count: usize,
509    pub mutation_count: usize,
510    pub code_file_count: usize,
511    pub code_symbol_count: usize,
512    pub code_reference_count: usize,
513    pub code_chunk_count: usize,
514    pub code_parse_status_counts: CodeParseStatusCounts,
515}
516
517/// Mutation log entry returned for replay and index refresh planning.
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub struct MutationLogEntry {
520    pub graph_version: GraphVersion,
521    pub evidence_count: usize,
522    pub entity_count: usize,
523    pub relation_count: usize,
524    pub claim_count: usize,
525    pub event_count: usize,
526    pub affected_scopes: Vec<String>,
527    pub affected_entity_ids: Vec<String>,
528    pub evidence_ids: Vec<String>,
529    pub source_hashes: Vec<String>,
530}
531
532/// Scoped cursor for a derived index read model.
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub struct IndexCursor {
535    pub kind: IndexKind,
536    pub source_scope: String,
537    pub modality: IndexModality,
538    pub index_version: u64,
539    pub indexed_graph_version: GraphVersion,
540    pub state: crate::domain::IndexState,
541    pub last_error: Option<String>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub source_hash: Option<String>,
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub backend_cursor: Option<String>,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub model_name: Option<String>,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub model_dimension: Option<u32>,
550}
551
552/// Persistent index refresh task lifecycle state.
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
554#[serde(rename_all = "snake_case")]
555pub enum IndexRefreshTaskState {
556    Queued,
557    Running,
558    Succeeded,
559    Retrying,
560    Failed,
561    DeadLetter,
562}
563
564impl IndexRefreshTaskState {
565    /// Stable storage and API representation.
566    pub const fn as_str(self) -> &'static str {
567        match self {
568            Self::Queued => "queued",
569            Self::Running => "running",
570            Self::Succeeded => "succeeded",
571            Self::Retrying => "retrying",
572            Self::Failed => "failed",
573            Self::DeadLetter => "dead_letter",
574        }
575    }
576}
577
578/// Persistent task used by foreground refresh and startup recovery.
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580pub struct IndexRefreshTask {
581    pub task_id: String,
582    pub kind: IndexKind,
583    pub source_scope: String,
584    pub modality: IndexModality,
585    pub target_graph_version: GraphVersion,
586    pub state: IndexRefreshTaskState,
587    pub lease_owner: Option<String>,
588    pub lease_expires_at_ms: Option<u64>,
589    pub attempt_count: u32,
590    pub next_retry_at_ms: u64,
591    pub input_fingerprint: String,
592    pub cursor_before: GraphVersion,
593    pub cursor_after: Option<GraphVersion>,
594    pub last_error_kind: Option<String>,
595    pub last_error_message: Option<String>,
596    pub created_at_ms: u64,
597    pub updated_at_ms: u64,
598}
599
600/// Queue request created by refresh APIs or the reconciler.
601#[derive(Debug, Clone, PartialEq, Eq)]
602pub struct IndexRefreshQueueRequest {
603    pub kinds: Vec<IndexKind>,
604    pub target_graph_version: GraphVersion,
605    pub max_queue_depth: usize,
606    pub reset_dead_letter_tasks: bool,
607    pub now_ms: u64,
608}
609
610/// Lease acquisition request for bounded foreground workers.
611#[derive(Debug, Clone, PartialEq, Eq)]
612pub struct IndexRefreshClaimRequest {
613    pub lease_owner: String,
614    pub lease_duration_ms: u64,
615    pub max_attempts: u32,
616    pub now_ms: u64,
617}
618
619/// Completion report guarded by the active task lease and attempt token.
620#[derive(Debug, Clone, PartialEq, Eq)]
621pub struct IndexRefreshCompletion {
622    pub task_id: String,
623    pub lease_owner: String,
624    pub attempt_count: u32,
625    pub indexed_graph_version: GraphVersion,
626    pub model_name: Option<String>,
627    pub model_dimension: Option<u32>,
628    pub now_ms: u64,
629}
630
631/// Failure report for retry backoff and dead-letter isolation.
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct IndexRefreshFailure {
634    pub task_id: String,
635    pub lease_owner: String,
636    pub attempt_count: u32,
637    pub error_kind: String,
638    pub error_message: String,
639    pub retry_backoff_ms: u64,
640    pub max_attempts: u32,
641    pub now_ms: u64,
642}
643
644/// Per-kind lag included in diagnostics snapshots.
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
646pub struct IndexLag {
647    pub kind: IndexKind,
648    pub lag_versions: u64,
649}
650
651/// Structured reason explaining why an index family or scoped cursor is stale.
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
653pub struct IndexStalenessReason {
654    pub kind: IndexKind,
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub source_scope: Option<String>,
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub modality: Option<IndexModality>,
659    pub reason: String,
660    pub lag_versions: u64,
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub last_error: Option<String>,
663}
664
665/// Snapshot for queue, dead-letter, and stale-index diagnostics.
666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
667pub struct IndexRefreshDiagnostics {
668    pub queue_depth: usize,
669    pub running_count: usize,
670    pub retrying_count: usize,
671    pub dead_letter_count: usize,
672    pub oldest_unfinished_age_ms: Option<u64>,
673    pub index_lag_by_kind: Vec<IndexLag>,
674    pub max_index_lag_versions: u64,
675    pub stale_index_count: usize,
676    pub stale_reasons: Vec<IndexStalenessReason>,
677}
678
679/// Storage health surfaced to diagnostics.
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681pub struct StorageHealth {
682    pub graph_version: GraphVersion,
683    pub healthy: bool,
684    pub detail: String,
685}
686
687/// Storage boundary failure.
688#[derive(Debug)]
689pub enum StorageError {
690    Io(std::io::Error),
691    Sqlite(rusqlite::Error),
692    Join(tokio::task::JoinError),
693    LockPoisoned,
694    InvalidInput(String),
695}
696
697impl fmt::Display for StorageError {
698    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
699        match self {
700            Self::Io(error) => write!(formatter, "storage I/O failed: {error}"),
701            Self::Sqlite(error) => write!(formatter, "sqlite operation failed: {error}"),
702            Self::Join(error) => write!(formatter, "storage worker failed: {error}"),
703            Self::LockPoisoned => write!(formatter, "sqlite connection lock was poisoned"),
704            Self::InvalidInput(message) => write!(formatter, "invalid storage input: {message}"),
705        }
706    }
707}
708
709impl Error for StorageError {}
710
711impl From<std::io::Error> for StorageError {
712    fn from(error: std::io::Error) -> Self {
713        Self::Io(error)
714    }
715}
716
717impl From<rusqlite::Error> for StorageError {
718    fn from(error: rusqlite::Error) -> Self {
719        Self::Sqlite(error)
720    }
721}
722
723impl From<tokio::task::JoinError> for StorageError {
724    fn from(error: tokio::task::JoinError) -> Self {
725        Self::Join(error)
726    }
727}
728
729#[cfg(test)]
730#[path = "mod_tests.rs"]
731mod tests;