Skip to main content

relay_knowledge/storage/partitioned/
mod.rs

1use std::{path::Path, sync::Arc};
2
3mod catalog;
4mod control_plane;
5mod diagnostics;
6mod framework;
7mod indexing;
8mod repository;
9mod repository_set_store;
10mod routing;
11mod status;
12mod totals;
13
14use crate::{
15    clock::system_now_millis_or_zero as now_millis,
16    domain::{
17        CodeFeatureFlagGraph, CodeFeatureFlagRequest, CodeIndexBatch, CodeIndexCheckpoint,
18        CodeIndexPublicationFence, CodeIndexSession, CodeIndexSnapshot, CodeIndexSummary,
19        CodeRepositoryRegistration, CodeRepositoryRemovalSummary, CodeRepositoryReport,
20        CodeRepositoryStatus, CodeRepositoryTotals, CodeRetrievalHit, CodeRetrievalRequest,
21        CodeSymbolGenerationCounts, SoftwareGlobalProjection, SoftwareGlobalRequest,
22    },
23    paths::RuntimePaths,
24    storage::{
25        BusinessKnowledgeStore, CodeImpactChanges, CodeIndexPublicationStore,
26        CodeIndexPublicationTarget, CodeIndexSourceStore, CodeIndexTaskClaimRequest,
27        CodeIndexTaskCompletion, CodeIndexTaskFailure, CodeIndexTaskLeaseRecord,
28        CodeIndexTaskLeaseRecovery, CodeIndexTaskLeaseRenewal, CodeIndexTaskStore,
29        CodeQueryReadStore, CodeScopeRetentionRequest, CodeScopeRetentionStore,
30        RepositoryCatalogStore, SoftwareProjectionStore, SqliteGraphStore, StorageError,
31        StorageFuture,
32    },
33};
34
35use catalog::{SqliteShardCatalog, initialize_catalog_schema};
36use routing::{report_matches_active_control, repository_store_for_report, source_scope_store};
37
38/// SQLite topology that keeps global control state in one DB and code facts in
39/// one DB per registered repository.
40#[derive(Clone)]
41pub struct PartitionedSqliteKnowledgeStore {
42    control: Arc<SqliteGraphStore>,
43    catalog: Arc<SqliteShardCatalog>,
44}
45
46impl PartitionedSqliteKnowledgeStore {
47    pub fn open(control_path: impl AsRef<Path>, paths: RuntimePaths) -> Result<Self, StorageError> {
48        let control_path = control_path.as_ref().to_path_buf();
49        let control = Arc::new(SqliteGraphStore::open(&control_path)?);
50        initialize_catalog_schema(&control_path)?;
51
52        Ok(Self {
53            control,
54            catalog: Arc::new(SqliteShardCatalog::new(control_path, paths)),
55        })
56    }
57}
58
59impl RepositoryCatalogStore for PartitionedSqliteKnowledgeStore {
60    fn upsert_code_repository(
61        &self,
62        registration: CodeRepositoryRegistration,
63    ) -> StorageFuture<'_, CodeRepositoryStatus> {
64        repository::upsert(self, registration)
65    }
66
67    fn code_repository_status(
68        &self,
69        repository: String,
70    ) -> StorageFuture<'_, Option<CodeRepositoryStatus>> {
71        repository::status(self, repository)
72    }
73
74    fn list_code_repositories(&self) -> StorageFuture<'_, Vec<CodeRepositoryStatus>> {
75        control_plane::list_code_repositories(self)
76    }
77
78    fn remove_code_repository(
79        &self,
80        repository: String,
81        now_ms: u64,
82    ) -> StorageFuture<'_, Option<CodeRepositoryRemovalSummary>> {
83        repository::remove(self, repository, now_ms)
84    }
85
86    fn code_repository_scope_status(
87        &self,
88        repository: String,
89        resolved_commit_sha: String,
90        path_filters: Vec<String>,
91        language_filters: Vec<String>,
92    ) -> StorageFuture<'_, Option<CodeRepositoryStatus>> {
93        repository::scope_status(
94            self,
95            repository,
96            resolved_commit_sha,
97            path_filters,
98            language_filters,
99        )
100    }
101
102    fn latest_code_repository_scope_status(
103        &self,
104        repository: String,
105        path_filters: Vec<String>,
106        language_filters: Vec<String>,
107    ) -> StorageFuture<'_, Option<CodeRepositoryStatus>> {
108        repository::latest_scope_status(self, repository, path_filters, language_filters)
109    }
110}
111
112impl CodeIndexTaskStore for PartitionedSqliteKnowledgeStore {
113    fn queue_code_index_task(
114        &self,
115        task: crate::storage::CodeIndexTaskSeed,
116    ) -> StorageFuture<'_, crate::domain::CodeIndexTaskRecord> {
117        let control = Arc::clone(&self.control);
118        Box::pin(async move { control.queue_code_index_task(task).await })
119    }
120
121    fn claim_code_index_task(
122        &self,
123        request: CodeIndexTaskClaimRequest,
124    ) -> StorageFuture<'_, Option<crate::domain::CodeIndexTaskRecord>> {
125        self.control.claim_code_index_task(request)
126    }
127
128    fn recover_code_index_task_leases(
129        &self,
130        now_ms: u64,
131        max_attempts: u32,
132    ) -> StorageFuture<'_, ()> {
133        self.control
134            .recover_code_index_task_leases(now_ms, max_attempts)
135    }
136
137    fn running_code_index_task_leases(&self) -> StorageFuture<'_, Vec<CodeIndexTaskLeaseRecord>> {
138        self.control.running_code_index_task_leases()
139    }
140
141    fn recover_code_index_task_leases_by_task(
142        &self,
143        request: CodeIndexTaskLeaseRecovery,
144    ) -> StorageFuture<'_, usize> {
145        self.control.recover_code_index_task_leases_by_task(request)
146    }
147
148    fn reset_code_index_tasks(
149        &self,
150        repository_id: String,
151        now_ms: u64,
152    ) -> StorageFuture<'_, Vec<crate::domain::CodeIndexTaskRecord>> {
153        self.control.reset_code_index_tasks(repository_id, now_ms)
154    }
155
156    fn renew_code_index_task_lease(
157        &self,
158        request: CodeIndexTaskLeaseRenewal,
159    ) -> StorageFuture<'_, crate::domain::CodeIndexTaskRecord> {
160        self.control.renew_code_index_task_lease(request)
161    }
162
163    fn complete_code_index_task(
164        &self,
165        request: CodeIndexTaskCompletion,
166    ) -> StorageFuture<'_, crate::domain::CodeIndexTaskRecord> {
167        self.control.complete_code_index_task(request)
168    }
169
170    fn run_code_index_post_maintenance(
171        &self,
172        repository_id: String,
173        source_scope: String,
174    ) -> StorageFuture<'_, ()> {
175        let this = self.clone();
176        Box::pin(async move {
177            let active = this
178                .catalog
179                .active_repository_for_scope(source_scope.clone())
180                .await?
181                .as_deref()
182                == Some(repository_id.as_str());
183            let shard = if active {
184                this.catalog
185                    .existing_repository_store(repository_id.clone())
186                    .await?
187            } else {
188                this.catalog
189                    .checkpoint_repository_store(repository_id.clone())
190                    .await?
191            }
192            .ok_or_else(|| {
193                StorageError::InvalidInput(format!(
194                    "repository shard for '{repository_id}' is unavailable for post-index maintenance"
195                ))
196            })?;
197            shard
198                .run_code_index_post_maintenance(repository_id, source_scope)
199                .await
200        })
201    }
202
203    fn code_index_publication_receipt(
204        &self,
205        task_id: String,
206        repository_id: String,
207        source_scope: String,
208        now_ms: u64,
209    ) -> StorageFuture<'_, bool> {
210        self.control
211            .code_index_publication_receipt(task_id, repository_id, source_scope, now_ms)
212    }
213
214    fn reconcile_code_index_publication_with_fence(
215        &self,
216        target: CodeIndexPublicationTarget,
217        fence: CodeIndexPublicationFence,
218    ) -> StorageFuture<'_, bool> {
219        indexing::publication::reconcile(self, target, fence)
220    }
221
222    fn fail_code_index_task(
223        &self,
224        request: CodeIndexTaskFailure,
225    ) -> StorageFuture<'_, crate::domain::CodeIndexTaskRecord> {
226        self.control.fail_code_index_task(request)
227    }
228
229    fn code_index_task(
230        &self,
231        task_id: String,
232    ) -> StorageFuture<'_, Option<crate::domain::CodeIndexTaskRecord>> {
233        self.control.code_index_task(task_id)
234    }
235
236    fn active_code_index_task(
237        &self,
238        repository_id: String,
239    ) -> StorageFuture<'_, Option<crate::domain::CodeIndexTaskRecord>> {
240        self.control.active_code_index_task(repository_id)
241    }
242
243    fn code_index_task_queue_status(
244        &self,
245    ) -> StorageFuture<'_, crate::domain::CodeIndexTaskQueueStatus> {
246        self.control.code_index_task_queue_status()
247    }
248}
249
250impl CodeScopeRetentionStore for PartitionedSqliteKnowledgeStore {
251    fn code_scope_retention(
252        &self,
253        repository_id: String,
254    ) -> StorageFuture<'_, crate::domain::CodeScopeRetentionSummary> {
255        indexing::retention::status(self, repository_id)
256    }
257
258    fn prune_code_repository_scopes(
259        &self,
260        request: CodeScopeRetentionRequest,
261    ) -> StorageFuture<'_, crate::domain::CodeScopeRetentionSummary> {
262        indexing::retention::prune(self, request)
263    }
264
265    fn schedule_code_repository_retention(
266        &self,
267        max_indexed_repositories: usize,
268        now_ms: u64,
269    ) -> StorageFuture<'_, Option<String>> {
270        self.control
271            .schedule_code_repository_retention(max_indexed_repositories, now_ms)
272    }
273
274    fn code_repository_retention_scan_pending(&self) -> StorageFuture<'_, bool> {
275        self.control.code_repository_retention_scan_pending()
276    }
277}
278
279impl CodeIndexSourceStore for PartitionedSqliteKnowledgeStore {
280    fn code_file_fingerprints(
281        &self,
282        repository_id: String,
283    ) -> StorageFuture<'_, Vec<crate::domain::CodeFileFingerprint>> {
284        indexing::file_index::fingerprints(self, repository_id)
285    }
286
287    fn code_file_fingerprints_for_scope(
288        &self,
289        source_scope: String,
290    ) -> StorageFuture<'_, Vec<crate::domain::CodeFileFingerprint>> {
291        indexing::file_index::fingerprints_for_scope(self, source_scope)
292    }
293
294    fn code_file_candidate_paths_for_scope(
295        &self,
296        source_scope: String,
297        path_filters: Vec<String>,
298        language_filters: Vec<String>,
299        exclude_generated: bool,
300        limit: usize,
301    ) -> StorageFuture<'_, Vec<String>> {
302        indexing::file_index::candidate_paths_for_scope(
303            self,
304            source_scope,
305            path_filters,
306            language_filters,
307            exclude_generated,
308            limit,
309        )
310    }
311
312    fn code_file_candidate_paths_for_query_scope(
313        &self,
314        source_scope: String,
315        query: String,
316        path_filters: Vec<String>,
317        language_filters: Vec<String>,
318        exclude_generated: bool,
319        limit: usize,
320    ) -> StorageFuture<'_, Vec<String>> {
321        indexing::file_index::candidate_paths_for_query_scope(
322            self,
323            source_scope,
324            query,
325            path_filters,
326            language_filters,
327            exclude_generated,
328            limit,
329        )
330    }
331
332    fn repository_documents_for_scope(
333        &self,
334        source_scope: String,
335        path_filters: Vec<String>,
336        max_files: usize,
337        max_bytes: usize,
338    ) -> StorageFuture<'_, Vec<crate::domain::IndexedRepositoryDocument>> {
339        routing::repository_documents_for_scope(
340            self.clone(),
341            source_scope,
342            path_filters,
343            max_files,
344            max_bytes,
345        )
346    }
347}
348
349impl CodeIndexPublicationStore for PartitionedSqliteKnowledgeStore {
350    fn code_index_checkpoint(
351        &self,
352        source_scope: String,
353    ) -> StorageFuture<'_, Option<CodeIndexCheckpoint>> {
354        indexing::checkpoint::by_scope(self, source_scope)
355    }
356
357    fn latest_code_index_checkpoint(
358        &self,
359        repository_id: String,
360    ) -> StorageFuture<'_, Option<CodeIndexCheckpoint>> {
361        indexing::checkpoint::latest(self, repository_id)
362    }
363
364    fn apply_code_index_snapshot(
365        &self,
366        snapshot: CodeIndexSnapshot,
367    ) -> StorageFuture<'_, CodeIndexSummary> {
368        indexing::lifecycle::apply_snapshot(self, snapshot)
369    }
370
371    fn apply_code_index_snapshot_with_fence(
372        &self,
373        snapshot: CodeIndexSnapshot,
374        fence: CodeIndexPublicationFence,
375    ) -> StorageFuture<'_, CodeIndexSummary> {
376        indexing::lifecycle::apply_snapshot_with_fence(self, snapshot, fence)
377    }
378
379    fn clear_code_workspace_state(
380        &self,
381        repository_id: String,
382        source_scope: String,
383    ) -> StorageFuture<'_, ()> {
384        indexing::lifecycle::clear_workspace(self, repository_id, source_scope)
385    }
386
387    fn code_repository_auto_workspace_state_exists(
388        &self,
389        repository_id: String,
390    ) -> StorageFuture<'_, bool> {
391        indexing::lifecycle::auto_workspace_state_exists(self, repository_id)
392    }
393
394    fn clear_code_workspace_state_with_fence(
395        &self,
396        repository_id: String,
397        source_scope: String,
398        fence: CodeIndexPublicationFence,
399    ) -> StorageFuture<'_, ()> {
400        indexing::lifecycle::clear_workspace_with_fence(self, repository_id, source_scope, fence)
401    }
402    fn begin_code_index_session(
403        &self,
404        session: CodeIndexSession,
405    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
406        indexing::lifecycle::begin_session(self, session)
407    }
408
409    fn begin_code_index_session_with_fence(
410        &self,
411        session: CodeIndexSession,
412        fence: CodeIndexPublicationFence,
413    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
414        indexing::lifecycle::begin_session_with_fence(self, session, fence)
415    }
416
417    fn begin_code_index_session_at_checkpoint(
418        &self,
419        session: CodeIndexSession,
420        expected_checkpoint: Option<CodeIndexCheckpoint>,
421    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
422        indexing::lifecycle::begin_session_at_checkpoint(self, session, expected_checkpoint)
423    }
424
425    fn begin_code_index_session_at_checkpoint_with_fence(
426        &self,
427        session: CodeIndexSession,
428        expected_checkpoint: Option<CodeIndexCheckpoint>,
429        fence: CodeIndexPublicationFence,
430    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
431        indexing::lifecycle::begin_session_at_checkpoint_with_fence(
432            self,
433            session,
434            expected_checkpoint,
435            fence,
436        )
437    }
438
439    fn apply_code_index_batch(
440        &self,
441        batch: CodeIndexBatch,
442    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
443        indexing::lifecycle::apply_batch(self, batch)
444    }
445
446    fn apply_code_index_batch_with_fence(
447        &self,
448        batch: CodeIndexBatch,
449        fence: CodeIndexPublicationFence,
450    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
451        indexing::lifecycle::apply_batch_with_fence(self, batch, fence)
452    }
453
454    fn finalize_code_index_session(
455        &self,
456        session: CodeIndexSession,
457    ) -> StorageFuture<'_, CodeIndexSummary> {
458        indexing::lifecycle::finalize_session(self, session)
459    }
460
461    fn finalize_code_index_session_with_fence(
462        &self,
463        session: CodeIndexSession,
464        fence: CodeIndexPublicationFence,
465    ) -> StorageFuture<'_, CodeIndexSummary> {
466        indexing::lifecycle::finalize_session_with_fence(self, session, fence)
467    }
468
469    fn advance_code_index_session_with_fence(
470        &self,
471        session: CodeIndexSession,
472        fence: CodeIndexPublicationFence,
473    ) -> StorageFuture<'_, crate::storage::CodeIndexFinalizationStep> {
474        indexing::lifecycle::advance_session_with_fence(self, session, fence)
475    }
476}
477
478impl CodeQueryReadStore for PartitionedSqliteKnowledgeStore {
479    fn search_code(
480        &self,
481        request: CodeRetrievalRequest,
482    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
483        let this = self.clone();
484        Box::pin(async move {
485            if let Some(shard) = routing::repository_store_for_selector(
486                &this.control,
487                &this.catalog,
488                request.repository.clone(),
489            )
490            .await?
491            {
492                return match shard.search_code(request.clone()).await {
493                    Ok(hits) => Ok(hits),
494                    Err(error) if routing::is_missing_code_scope_error(&error) => {
495                        this.control.search_code(request).await
496                    }
497                    Err(error) => Err(error),
498                };
499            }
500            this.control.search_code(request).await
501        })
502    }
503
504    fn search_code_feature_flags(
505        &self,
506        request: CodeFeatureFlagRequest,
507    ) -> StorageFuture<'_, Vec<CodeFeatureFlagGraph>> {
508        let this = self.clone();
509        Box::pin(async move {
510            if let Some(shard) = routing::repository_store_for_selector(
511                &this.control,
512                &this.catalog,
513                request.repository.clone(),
514            )
515            .await?
516            {
517                return match shard.search_code_feature_flags(request.clone()).await {
518                    Ok(flags) => Ok(flags),
519                    Err(error) if routing::is_missing_code_scope_error(&error) => {
520                        this.control.search_code_feature_flags(request).await
521                    }
522                    Err(error) => Err(error),
523                };
524            }
525            this.control.search_code_feature_flags(request).await
526        })
527    }
528
529    fn search_code_feature_flags_scope(
530        &self,
531        source_scope: String,
532        request: CodeFeatureFlagRequest,
533    ) -> StorageFuture<'_, Vec<CodeFeatureFlagGraph>> {
534        routing::search_code_feature_flags_scope(self.clone(), source_scope, request)
535    }
536
537    fn search_code_scope(
538        &self,
539        source_scope: String,
540        request: CodeRetrievalRequest,
541    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
542        routing::search_code_scope(self.clone(), source_scope, request)
543    }
544
545    fn analyze_code_impact(
546        &self,
547        request: crate::domain::CodeImpactRequest,
548        changes: CodeImpactChanges,
549    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
550        let this = self.clone();
551        Box::pin(async move {
552            if let Some(shard) = routing::repository_store_for_selector(
553                &this.control,
554                &this.catalog,
555                request.repository.clone(),
556            )
557            .await?
558            {
559                return match shard
560                    .analyze_code_impact(request.clone(), changes.clone())
561                    .await
562                {
563                    Ok(hits) => Ok(hits),
564                    Err(error) if routing::is_missing_code_scope_error(&error) => {
565                        this.control.analyze_code_impact(request, changes).await
566                    }
567                    Err(error) => Err(error),
568                };
569            }
570            this.control.analyze_code_impact(request, changes).await
571        })
572    }
573
574    fn analyze_code_impact_scope(
575        &self,
576        source_scope: String,
577        request: crate::domain::CodeImpactRequest,
578        changes: CodeImpactChanges,
579    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
580        routing::analyze_code_impact_scope(self.clone(), source_scope, request, changes)
581    }
582
583    fn codebase_view_snapshot(
584        &self,
585        source_scope: String,
586        request: crate::domain::CodebaseViewRequest,
587        row_limit: usize,
588    ) -> StorageFuture<'_, crate::domain::CodebaseViewSnapshot> {
589        routing::codebase_view_snapshot(self.clone(), source_scope, request, row_limit)
590    }
591
592    fn code_repository_totals(&self) -> StorageFuture<'_, CodeRepositoryTotals> {
593        let this = self.clone();
594        Box::pin(async move { totals::code_repository_totals(this.control, this.catalog).await })
595    }
596
597    fn code_repository_report(
598        &self,
599        repository: String,
600    ) -> StorageFuture<'_, CodeRepositoryReport> {
601        let this = self.clone();
602        Box::pin(async move {
603            if let Some(shard) =
604                repository_store_for_report(&this.control, &this.catalog, repository.clone())
605                    .await?
606            {
607                let report = shard.code_repository_report(repository.clone()).await?;
608                if report_matches_active_control(
609                    &this.control,
610                    &this.catalog,
611                    repository.clone(),
612                    &report,
613                )
614                .await?
615                {
616                    return Ok(report);
617                }
618            }
619            this.control.code_repository_report(repository).await
620        })
621    }
622
623    fn code_repository_scope_symbol_generation_counts(
624        &self,
625        source_scope: String,
626    ) -> StorageFuture<'_, CodeSymbolGenerationCounts> {
627        let this = self.clone();
628        Box::pin(async move {
629            totals::scope_symbol_generation_counts(this.control, this.catalog, source_scope).await
630        })
631    }
632}
633
634impl SoftwareProjectionStore for PartitionedSqliteKnowledgeStore {
635    fn refresh_software_global_projection(
636        &self,
637        source_scope: String,
638    ) -> StorageFuture<'_, SoftwareGlobalProjection> {
639        let this = self.clone();
640        Box::pin(async move {
641            if let Some(shard) = source_scope_store(&this.catalog, source_scope.clone()).await? {
642                return shard.refresh_software_global_projection(source_scope).await;
643            }
644            this.control
645                .refresh_software_global_projection(source_scope)
646                .await
647        })
648    }
649
650    fn refresh_software_global_projection_with_fence(
651        &self,
652        source_scope: String,
653        fence: CodeIndexPublicationFence,
654    ) -> StorageFuture<'_, SoftwareGlobalProjection> {
655        let this = self.clone();
656        Box::pin(async move {
657            let shard = this
658                .catalog
659                .checkpoint_repository_store(fence.repository_id.clone())
660                .await?
661                .ok_or_else(|| {
662                    StorageError::InvalidInput(format!(
663                        "repository shard for fenced software projection '{}' is missing",
664                        fence.repository_id
665                    ))
666                })?;
667            let projection = shard
668                .refresh_software_global_projection_with_fence(source_scope.clone(), fence.clone())
669                .await?;
670            let status = shard
671                .code_repository_status(projection.status.repository_id.clone())
672                .await?
673                .ok_or_else(|| {
674                    StorageError::InvalidInput(
675                        "sharded code repository status is missing after software publication"
676                            .to_owned(),
677                    )
678                })?;
679            this.catalog
680                .publish_scope_status_with_fence(
681                    projection.status.repository_id.clone(),
682                    source_scope,
683                    status,
684                    fence,
685                )
686                .await?;
687            Ok(projection)
688        })
689    }
690
691    fn software_global_projection(
692        &self,
693        request: SoftwareGlobalRequest,
694    ) -> StorageFuture<'_, SoftwareGlobalProjection> {
695        let this = self.clone();
696        Box::pin(async move {
697            if let Some(shard) = routing::repository_store_for_selector(
698                &this.control,
699                &this.catalog,
700                request.repository.clone(),
701            )
702            .await?
703            {
704                return match shard.software_global_projection(request.clone()).await {
705                    Ok(projection) => Ok(projection),
706                    Err(error) if routing::is_missing_code_scope_error(&error) => {
707                        this.control.software_global_projection(request).await
708                    }
709                    Err(error) => Err(error),
710                };
711            }
712            this.control.software_global_projection(request).await
713        })
714    }
715
716    fn software_global_projection_for_scope(
717        &self,
718        source_scope: String,
719        request: SoftwareGlobalRequest,
720    ) -> StorageFuture<'_, SoftwareGlobalProjection> {
721        let this = self.clone();
722        Box::pin(async move {
723            if let Some(shard) = source_scope_store(&this.catalog, source_scope.clone()).await? {
724                return shard
725                    .software_global_projection_for_scope(source_scope, request)
726                    .await;
727            }
728            this.control
729                .software_global_projection_for_scope(source_scope, request)
730                .await
731        })
732    }
733}
734
735impl BusinessKnowledgeStore for PartitionedSqliteKnowledgeStore {
736    fn replace_business_knowledge_projection(
737        &self,
738        input: crate::domain::BusinessKnowledgeProjectionInput,
739    ) -> StorageFuture<'_, crate::domain::BusinessKnowledgeStatus> {
740        let this = self.clone();
741        Box::pin(async move {
742            if let Some(shard) =
743                source_scope_store(&this.catalog, input.source_scope.clone()).await?
744            {
745                return shard.replace_business_knowledge_projection(input).await;
746            }
747            this.control
748                .replace_business_knowledge_projection(input)
749                .await
750        })
751    }
752
753    fn replace_business_knowledge_projection_with_fence(
754        &self,
755        input: crate::domain::BusinessKnowledgeProjectionInput,
756        fence: CodeIndexPublicationFence,
757    ) -> StorageFuture<'_, crate::domain::BusinessKnowledgeStatus> {
758        let this = self.clone();
759        Box::pin(async move {
760            let shard = this
761                .catalog
762                .checkpoint_repository_store(fence.repository_id.clone())
763                .await?
764                .ok_or_else(|| {
765                    StorageError::InvalidInput(format!(
766                        "repository shard for fenced business projection '{}' is missing",
767                        fence.repository_id
768                    ))
769                })?;
770            shard
771                .replace_business_knowledge_projection_with_fence(input, fence)
772                .await
773        })
774    }
775
776    fn business_knowledge_projection_for_scope(
777        &self,
778        source_scope: String,
779        request: crate::domain::BusinessKnowledgeQueryRequest,
780    ) -> StorageFuture<'_, crate::domain::BusinessKnowledgeProjection> {
781        let this = self.clone();
782        Box::pin(async move {
783            if let Some(shard) = source_scope_store(&this.catalog, source_scope.clone()).await? {
784                return shard
785                    .business_knowledge_projection_for_scope(source_scope, request)
786                    .await;
787            }
788            this.control
789                .business_knowledge_projection_for_scope(source_scope, request)
790                .await
791        })
792    }
793
794    fn business_knowledge_status(
795        &self,
796        source_scope: String,
797    ) -> StorageFuture<'_, Option<crate::domain::BusinessKnowledgeStatus>> {
798        let this = self.clone();
799        Box::pin(async move {
800            if let Some(shard) = source_scope_store(&this.catalog, source_scope.clone()).await? {
801                return shard.business_knowledge_status(source_scope).await;
802            }
803            this.control.business_knowledge_status(source_scope).await
804        })
805    }
806}
807
808#[cfg(test)]
809#[path = "mod_tests.rs"]
810mod tests;
811
812#[cfg(test)]
813#[path = "post_maintenance_tests.rs"]
814mod post_maintenance_tests;