1use super::*;
2
3impl RedDB {
4 pub(crate) fn write_column_block_page(
5 &self,
6 bytes: &[u8],
7 ) -> Result<crate::storage::engine::PageLocation, Box<dyn std::error::Error>> {
8 use crate::storage::engine::PageType;
9
10 let store = self.store();
11 let Some(pager) = store.pager() else {
12 return Ok(crate::storage::engine::PageLocation::new(
13 0,
14 0,
15 bytes.len() as u32,
16 ));
17 };
18 let content_size = crate::storage::engine::page::CONTENT_SIZE;
19 let page_count = bytes.len().max(1).div_ceil(content_size);
20 let mut first_page_id = None;
21 for page_index in 0..page_count {
22 let mut page = pager.allocate_page(PageType::ColumnBlock)?;
23 let page_id = page.page_id();
24 if let Some(first) = first_page_id {
25 let expected = first + page_index as u32;
26 if page_id != expected {
27 return Err(format!(
28 "column block page allocation was not contiguous: got {page_id}, expected {expected}"
29 )
30 .into());
31 }
32 } else {
33 first_page_id = Some(page_id);
34 }
35 let start = page_index * content_size;
36 let end = (start + content_size).min(bytes.len());
37 if start < end {
38 page.content_mut()[..end - start].copy_from_slice(&bytes[start..end]);
39 }
40 pager.write_page_encrypted(page_id, page)?;
41 }
42 Ok(crate::storage::engine::PageLocation::new(
43 first_page_id.expect("page_count is at least one"),
44 0,
45 bytes.len() as u32,
46 ))
47 }
48
49 pub(crate) fn read_column_block_page(
50 &self,
51 loc: crate::storage::engine::PageLocation,
52 ) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
53 let store = self.store();
54 let Some(pager) = store.pager() else {
55 return Ok(None);
56 };
57 let content_size = crate::storage::engine::page::CONTENT_SIZE;
58 let mut remaining = loc.length as usize;
59 let mut page_id = loc.page_id;
60 let mut offset = loc.offset as usize;
61 let mut out = Vec::with_capacity(remaining);
62 while remaining > 0 {
63 let page = pager.read_page_decrypted(page_id)?;
64 if page.page_type()? != crate::storage::engine::PageType::ColumnBlock {
65 return Err(format!("page {page_id} is not a ColumnBlock page").into());
66 }
67 if offset > content_size {
68 return Err(format!(
69 "column block page location offset out of bounds: {offset} > {content_size}"
70 )
71 .into());
72 }
73 let available = content_size - offset;
74 let take = remaining.min(available);
75 out.extend_from_slice(&page.content()[offset..offset + take]);
76 remaining -= take;
77 page_id = page_id.saturating_add(1);
78 offset = 0;
79 }
80 Ok(Some(out))
81 }
82
83 pub fn tree_definitions(&self) -> Vec<crate::physical::PhysicalTreeDefinition> {
84 self.physical_metadata()
85 .map(|metadata| metadata.tree_definitions)
86 .unwrap_or_default()
87 }
88
89 pub fn tree_definition(
90 &self,
91 collection: &str,
92 name: &str,
93 ) -> Option<crate::physical::PhysicalTreeDefinition> {
94 self.tree_definitions()
95 .into_iter()
96 .find(|definition| definition.collection == collection && definition.name == name)
97 }
98
99 pub fn save_tree_definition(
100 &self,
101 definition: crate::physical::PhysicalTreeDefinition,
102 ) -> Result<crate::physical::PhysicalTreeDefinition, Box<dyn std::error::Error>> {
103 self.update_physical_metadata(|metadata| {
104 if let Some(existing) = metadata.tree_definitions.iter_mut().find(|existing| {
105 existing.collection == definition.collection && existing.name == definition.name
106 }) {
107 *existing = definition.clone();
108 } else {
109 metadata.tree_definitions.push(definition.clone());
110 }
111 metadata.tree_definitions.sort_by(|left, right| {
112 left.collection
113 .cmp(&right.collection)
114 .then_with(|| left.name.cmp(&right.name))
115 });
116 definition.clone()
117 })
118 }
119
120 pub fn remove_tree_definition(
121 &self,
122 collection: &str,
123 name: &str,
124 ) -> Result<Option<crate::physical::PhysicalTreeDefinition>, Box<dyn std::error::Error>> {
125 self.update_physical_metadata(|metadata| {
126 metadata
127 .tree_definitions
128 .iter()
129 .position(|definition| {
130 definition.collection == collection && definition.name == name
131 })
132 .map(|index| metadata.tree_definitions.remove(index))
133 })
134 }
135
136 pub fn collection_default_ttl_ms(&self, collection: &str) -> Option<u64> {
137 self.collection_ttl_defaults_ms
138 .read()
139 .ok()
140 .and_then(|defaults| defaults.get(collection).copied())
141 }
142
143 pub fn set_collection_default_ttl_ms(&self, collection: impl Into<String>, ttl_ms: u64) {
144 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
145 defaults.insert(collection.into(), ttl_ms);
146 }
147 }
148
149 pub fn clear_collection_default_ttl_ms(&self, collection: &str) {
150 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
151 defaults.remove(collection);
152 }
153 }
154
155 pub fn collection_contracts(&self) -> Vec<crate::physical::CollectionContract> {
156 self.contract_cache_map()
157 .values()
158 .map(|arc| (**arc).clone())
159 .collect()
160 }
161
162 pub fn collection_contract(
163 &self,
164 collection: &str,
165 ) -> Option<crate::physical::CollectionContract> {
166 self.contract_cache_map()
167 .get(collection)
168 .map(|arc| (**arc).clone())
169 }
170
171 pub fn collection_contract_arc(
176 &self,
177 collection: &str,
178 ) -> Option<std::sync::Arc<crate::physical::CollectionContract>> {
179 self.contract_cache_map()
180 .get(collection)
181 .map(std::sync::Arc::clone)
182 }
183
184 fn contract_cache_map(
189 &self,
190 ) -> std::sync::Arc<
191 std::collections::HashMap<String, std::sync::Arc<crate::physical::CollectionContract>>,
192 > {
193 self.collection_contract_cache.get_or_load(|| {
194 self.physical_metadata()
195 .map(|metadata| metadata.collection_contracts)
196 .unwrap_or_default()
197 })
198 }
199
200 pub(crate) fn invalidate_collection_contract_cache(&self) {
201 self.collection_contract_cache.invalidate();
202 }
203
204 pub(crate) fn sync_single_file_contract_blob(&self) -> Result<(), Box<dyn std::error::Error>> {
212 let contracts = self.collection_contracts();
213 let bytes = crate::physical::serialize_collection_contracts(&contracts);
214 self.store().persist_aux_metadata(bytes)?;
215 Ok(())
216 }
217
218 pub(crate) fn seed_contract_cache_from_store_aux(&self) -> std::io::Result<()> {
223 let bytes = self.store().aux_metadata();
224 if bytes.is_empty() {
225 return Ok(());
226 }
227 let contracts = crate::physical::deserialize_collection_contracts(&bytes)?;
228 let map: std::collections::HashMap<_, _> = contracts
229 .into_iter()
230 .map(|contract| (contract.name.clone(), std::sync::Arc::new(contract)))
231 .collect();
232 let store = self.store();
236 let index = store.context_index();
237 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
238 for contract in map.values() {
239 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
240 if let Some(ttl_ms) = contract.default_ttl_ms {
241 defaults.insert(contract.name.clone(), ttl_ms);
242 }
243 }
244 }
245 self.collection_contract_cache.replace(map);
246 Ok(())
247 }
248
249 fn update_collection_contract_metadata<T, F>(
250 &self,
251 mutator: F,
252 ) -> Result<T, Box<dyn std::error::Error>>
253 where
254 F: FnOnce(&mut PhysicalMetadataFile) -> T,
255 {
256 if !self.paged_mode
257 || self.options.storage_profile.deploy_profile
258 != crate::storage::DeployProfile::Embedded
259 || self.options.storage_profile.packaging
260 != crate::storage::StoragePackaging::SingleFile
261 {
262 return self.update_physical_metadata(mutator);
263 }
264 if self.options.read_only {
265 return Err("physical metadata operations are not allowed in read-only mode".into());
266 }
267 let Some(_) = self.path() else {
268 return Err("database path is not available".into());
269 };
270
271 let _publication = self.store.vault_publication_lock.lock();
276 let mut metadata = self.load_or_bootstrap_physical_metadata(false)?;
277 let result = mutator(&mut metadata);
278 self.persist_physical_metadata_record(&metadata)?;
279 Ok(result)
280 }
281
282 pub(crate) fn cache_read_only_bootstrap_contract(
285 &self,
286 contract: crate::physical::CollectionContract,
287 ) {
288 assert!(self.options.read_only);
289 self.contract_cache_map();
290 self.collection_contract_cache.insert(contract);
291 }
292
293 pub fn save_collection_contract(
294 &self,
295 contract: crate::physical::CollectionContract,
296 ) -> Result<crate::physical::CollectionContract, Box<dyn std::error::Error>> {
297 let required_capabilities =
298 reddb_engine::storage::entity_codec::required_file_capabilities_for_contract(&contract);
299 self.store()
300 .fence_required_capabilities(required_capabilities)?;
301 let _publication = self.store.vault_publication_lock.lock();
302 self.save_collection_contract_locked(contract)
303 }
304
305 pub fn save_generated_descriptions_if_version(
306 &self,
307 collection: &str,
308 expected_version: u32,
309 expected_created_at_unix_ms: u128,
310 description: Option<String>,
311 columns: &std::collections::HashMap<String, String>,
312 ) -> Result<bool, Box<dyn std::error::Error>> {
313 let _publication = self.store.vault_publication_lock.lock();
314 let Some(mut contract) = self.collection_contract(collection) else {
315 return Ok(false);
316 };
317 if contract.version != expected_version
318 || contract.created_at_unix_ms != expected_created_at_unix_ms
319 || self.store.get_collection(collection).is_none()
320 {
321 return Ok(false);
322 }
323 let mut changed = false;
324 if (contract.description.is_none() || contract.description_generated)
325 && description.as_ref().is_some_and(|text| !text.is_empty())
326 {
327 contract.description = description;
328 contract.description_generated = true;
329 changed = true;
330 }
331 for column in &mut contract.declared_columns {
332 if let Some(text) = columns.get(&column.name) {
333 if !text.is_empty()
334 && (column.description.is_none() || column.description_generated)
335 {
336 column.description = Some(text.clone());
337 column.description_generated = true;
338 changed = true;
339 }
340 }
341 }
342 if changed {
343 contract.version = contract.version.saturating_add(1);
344 contract.updated_at_unix_ms = std::time::SystemTime::now()
345 .duration_since(std::time::UNIX_EPOCH)
346 .unwrap_or_default()
347 .as_millis();
348 self.save_collection_contract_locked(contract)?;
349 }
350 Ok(changed)
351 }
352
353 fn save_collection_contract_locked(
354 &self,
355 contract: crate::physical::CollectionContract,
356 ) -> Result<crate::physical::CollectionContract, Box<dyn std::error::Error>> {
357 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
358 if let Some(ttl_ms) = contract.default_ttl_ms {
359 defaults.insert(contract.name.clone(), ttl_ms);
360 } else {
361 defaults.remove(&contract.name);
362 }
363 }
364
365 self.store()
366 .context_index()
367 .set_collection_enabled(&contract.name, contract.context_index_enabled);
368 if let Some(manager) = self.store().get_collection(&contract.name) {
369 let columns = contract
370 .declared_columns
371 .iter()
372 .map(|column| column.name.clone())
373 .collect();
374 manager.set_column_schema_if_empty(columns);
375 }
376
377 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
380 && self.options.storage_profile.packaging
381 == crate::storage::StoragePackaging::SingleFile
382 && !self.paged_mode
383 {
384 self.collection_contract_cache.insert(contract.clone());
385 self.sync_single_file_contract_blob()?;
386 return Ok(contract);
387 }
388
389 self.invalidate_collection_contract_cache();
390
391 let saved = self.update_collection_contract_metadata(|metadata| {
392 if let Some(existing) = metadata
393 .collection_contracts
394 .iter_mut()
395 .find(|existing| existing.name == contract.name)
396 {
397 *existing = contract.clone();
398 } else {
399 metadata.collection_contracts.push(contract.clone());
400 }
401 metadata
402 .collection_contracts
403 .sort_by(|left, right| left.name.cmp(&right.name));
404
405 if let Some(ttl_ms) = contract.default_ttl_ms {
406 metadata
407 .collection_ttl_defaults_ms
408 .insert(contract.name.clone(), ttl_ms);
409 } else {
410 metadata.collection_ttl_defaults_ms.remove(&contract.name);
411 }
412
413 contract.clone()
414 })?;
415 self.invalidate_collection_contract_cache();
416 Ok(saved)
417 }
418
419 pub fn remove_collection_contract(
420 &self,
421 collection: &str,
422 ) -> Result<Option<crate::physical::CollectionContract>, Box<dyn std::error::Error>> {
423 if self.options.read_only {
424 return Err("physical metadata operations are not allowed in read-only mode".into());
425 }
426 let _publication = self.store.vault_publication_lock.lock();
427 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
428 defaults.remove(collection);
429 }
430
431 self.store()
432 .context_index()
433 .set_collection_enabled(collection, false);
434
435 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
436 && self.options.storage_profile.packaging
437 == crate::storage::StoragePackaging::SingleFile
438 && !self.paged_mode
439 {
440 let removed = self.collection_contract_cache.remove(collection);
441 self.sync_single_file_contract_blob()?;
442 return Ok(removed);
443 }
444
445 self.invalidate_collection_contract_cache();
446
447 if self.paged_mode {
448 let Some(_) = self.path() else {
449 return Err("database path is not available".into());
450 };
451 let previous = self.load_or_bootstrap_physical_metadata(false)?;
465 let mut roots = previous.superblock.collection_roots.clone();
466 roots.remove(collection);
467 let indexes = previous
468 .indexes
469 .iter()
470 .filter(|index| index.collection.as_deref() != Some(collection))
471 .cloned()
472 .collect();
473 let mut metadata = PhysicalMetadataFile::from_state(
474 self.options.clone(),
475 self.catalog_snapshot(),
476 roots,
477 indexes,
478 Some(&previous),
479 );
480 let removed = metadata
481 .collection_contracts
482 .iter()
483 .position(|contract| contract.name == collection)
484 .map(|index| metadata.collection_contracts.remove(index));
485 metadata.collection_ttl_defaults_ms.remove(collection);
486 self.persist_physical_metadata_record(&metadata)?;
487 self.persist_native_physical_header(&metadata)?;
488 self.invalidate_collection_contract_cache();
489 return Ok(removed);
490 }
491
492 let removed = self.update_collection_contract_metadata(|metadata| {
493 let removed = metadata
494 .collection_contracts
495 .iter()
496 .position(|contract| contract.name == collection)
497 .map(|index| metadata.collection_contracts.remove(index));
498 metadata.collection_ttl_defaults_ms.remove(collection);
499 metadata
500 .indexes
501 .retain(|index| index.collection.as_deref() != Some(collection));
502 removed
503 })?;
504 self.invalidate_collection_contract_cache();
505 Ok(removed)
506 }
507
508 pub(crate) fn collection_ttl_defaults_snapshot(&self) -> BTreeMap<String, u64> {
509 self.collection_ttl_defaults_ms
510 .read()
511 .map(|defaults| {
512 defaults
513 .iter()
514 .map(|(collection, ttl_ms)| (collection.clone(), *ttl_ms))
515 .collect()
516 })
517 .unwrap_or_default()
518 }
519
520 pub(crate) fn load_collection_ttl_defaults_from_metadata(&self) {
521 let metadata = self.physical_metadata();
522 let defaults = metadata
523 .as_ref()
524 .map(|m| m.collection_ttl_defaults_ms.clone())
525 .unwrap_or_default();
526
527 if let Ok(mut current) = self.collection_ttl_defaults_ms.write() {
528 current.clear();
529 current.extend(defaults);
530 }
531
532 if let Some(metadata) = metadata {
533 let store = self.store();
534 let index = store.context_index();
535 for contract in &metadata.collection_contracts {
536 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
537 }
538 }
539 }
540
541 pub(crate) fn hypertable_registry_snapshot(&self) -> Vec<crate::physical::PhysicalHypertable> {
549 let registry = self.hypertables();
550 if registry.is_empty() {
551 return Vec::new();
552 }
553 use std::collections::BTreeMap;
554 let mut chunks_by_table: BTreeMap<String, Vec<crate::physical::PhysicalHypertableChunk>> =
555 BTreeMap::new();
556 for chunk in registry.snapshot_chunks() {
557 chunks_by_table
558 .entry(chunk.id.hypertable.clone())
559 .or_default()
560 .push(crate::physical::PhysicalHypertableChunk {
561 start_ns: chunk.id.start_ns,
562 end_ns_exclusive: chunk.end_ns_exclusive,
563 row_count: chunk.row_count,
564 min_ts_ns: chunk.min_ts_ns,
565 max_ts_ns: chunk.max_ts_ns,
566 sealed: chunk.sealed,
567 ttl_override_ns: chunk.ttl_override_ns,
568 columnar_page: chunk.columnar_page,
569 columnar_derived: chunk.columnar_page.is_some(),
570 });
571 }
572 registry
573 .list()
574 .into_iter()
575 .map(|spec| crate::physical::PhysicalHypertable {
576 chunks: chunks_by_table.remove(&spec.name).unwrap_or_default(),
577 name: spec.name,
578 time_column: spec.time_column,
579 chunk_interval_ns: spec.chunk_interval_ns,
580 default_ttl_ns: spec.default_ttl_ns,
581 })
582 .collect()
583 }
584
585 pub(crate) fn load_hypertables_from_metadata(&self) {
591 let Some(metadata) = self.physical_metadata() else {
592 return;
593 };
594 if metadata.hypertables.is_empty() {
595 return;
596 }
597 let registry = self.hypertables();
598 for hypertable in &metadata.hypertables {
599 let mut spec = crate::storage::timeseries::HypertableSpec::new(
600 hypertable.name.clone(),
601 hypertable.time_column.clone(),
602 hypertable.chunk_interval_ns,
603 );
604 if let Some(ttl) = hypertable.default_ttl_ns {
605 spec = spec.with_ttl_ns(ttl);
606 }
607 registry.register(spec);
608 for chunk in &hypertable.chunks {
609 let meta = crate::storage::timeseries::ChunkMeta {
610 id: crate::storage::timeseries::ChunkId {
611 hypertable: hypertable.name.clone(),
612 start_ns: chunk.start_ns,
613 },
614 end_ns_exclusive: chunk.end_ns_exclusive,
615 row_count: chunk.row_count,
616 min_ts_ns: chunk.min_ts_ns,
617 max_ts_ns: chunk.max_ts_ns,
618 sealed: chunk.sealed,
619 ttl_override_ns: chunk.ttl_override_ns,
620 columnar_page: chunk.columnar_page,
621 };
622 let id = meta.id.clone();
623 let columnar_page = meta.columnar_page;
624 registry.restore_chunk(meta);
625 if let Some(loc) = columnar_page {
626 if let Ok(Some(bytes)) = self.read_column_block_page(loc) {
627 registry.restore_columnar_block(&id, bytes);
628 }
629 }
630 }
631 }
632 }
633
634 pub fn run_maintenance(&self) -> Result<(), Box<dyn std::error::Error>> {
635 self.store.run_maintenance()?;
636 self.persist_metadata()?;
637 if self.paged_mode {
638 if let Some(path) = self.path() {
639 self.remove_imported_metadata_sidecars(path)?;
640 }
641 }
642 Ok(())
643 }
644
645 pub fn metadata_path(&self) -> Option<PathBuf> {
647 self.path
648 .as_ref()
649 .map(|path| PhysicalMetadataFile::metadata_path_for(path))
650 }
651
652 pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
654 self.load_or_bootstrap_physical_metadata(false).ok()
655 }
656
657 pub fn physical_indexes(&self) -> Vec<PhysicalIndexState> {
659 let indexes = self
660 .physical_metadata()
661 .map(|metadata| metadata.indexes)
662 .unwrap_or_default();
663 self.physical_indexes_from_declarations(indexes)
664 }
665
666 pub(crate) fn physical_indexes_from_declarations(
667 &self,
668 indexes: Vec<PhysicalIndexState>,
669 ) -> Vec<PhysicalIndexState> {
670 let indexes = Some(indexes)
671 .filter(|indexes| !indexes.is_empty())
672 .or_else(|| {
673 self.native_physical_state()
674 .map(|state| self.physical_index_state_from_native_state(&state, None))
675 })
676 .unwrap_or_else(|| self.physical_index_state());
677 self.reconcile_index_states_with_native_artifacts(indexes)
678 }
679
680 pub fn exports(&self) -> Vec<ExportDescriptor> {
682 self.physical_metadata()
683 .map(|metadata| metadata.exports)
684 .or_else(|| {
685 self.native_physical_state()
686 .map(|state| self.exports_from_native_state(&state))
687 })
688 .unwrap_or_default()
689 }
690
691 pub fn snapshots(&self) -> Vec<crate::physical::SnapshotDescriptor> {
693 if let Some(metadata) = self.physical_metadata() {
694 if !metadata.snapshots.is_empty() {
695 return metadata.snapshots;
696 }
697 }
698 self.native_physical_state()
699 .map(|state| self.snapshots_from_native_state(&state))
700 .unwrap_or_default()
701 }
702
703 pub fn graph_projections(&self) -> Vec<PhysicalGraphProjection> {
705 self.physical_metadata()
706 .map(|metadata| metadata.graph_projections)
707 .or_else(|| {
708 self.native_physical_state()
709 .map(|state| self.graph_projections_from_native_state(&state))
710 })
711 .unwrap_or_default()
712 }
713
714 pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
716 self.catalog_model_snapshot().declared_graph_projections
717 }
718
719 pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
721 self.graph_projections()
722 .into_iter()
723 .filter(|projection| {
724 projection.last_materialized_sequence.is_some()
725 || matches!(projection.state.as_str(), "materialized" | "stale")
726 })
727 .collect()
728 }
729
730 pub fn analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
732 self.physical_metadata()
733 .map(|metadata| metadata.analytics_jobs)
734 .or_else(|| {
735 self.native_physical_state()
736 .map(|state| self.analytics_jobs_from_native_state(&state))
737 })
738 .unwrap_or_default()
739 }
740
741 pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
743 self.catalog_model_snapshot().declared_analytics_jobs
744 }
745
746 pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
748 self.analytics_jobs()
749 .into_iter()
750 .filter(|job| {
751 job.last_run_sequence.is_some()
752 || matches!(
753 job.state.as_str(),
754 "running" | "completed" | "failed" | "queued" | "stale"
755 )
756 })
757 .collect()
758 }
759
760 pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
762 self.catalog_model_snapshot().declared_indexes
763 }
764
765 pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
767 self.catalog_model_snapshot().operational_indexes
768 }
769
770 pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
772 self.catalog_model_snapshot().index_statuses
773 }
774
775 pub fn index_status(&self, name: &str) -> Option<CatalogIndexStatus> {
777 self.catalog_model_snapshot()
778 .index_statuses
779 .into_iter()
780 .find(|status| status.name == name)
781 }
782
783 pub fn save_graph_projection(
785 &self,
786 name: impl Into<String>,
787 node_labels: Vec<String>,
788 node_types: Vec<String>,
789 edge_labels: Vec<String>,
790 source: impl Into<String>,
791 ) -> Result<PhysicalGraphProjection, Box<dyn std::error::Error>> {
792 let name = name.into();
793 let source = source.into();
794 self.update_physical_metadata(|metadata| {
795 let now = SystemTime::now()
796 .duration_since(UNIX_EPOCH)
797 .unwrap_or_default()
798 .as_millis();
799 let projection = if let Some(existing) = metadata
800 .graph_projections
801 .iter_mut()
802 .find(|projection| projection.name == name)
803 {
804 existing.updated_at_unix_ms = now;
805 existing.state = "declared".to_string();
806 existing.source = source.clone();
807 existing.node_labels = node_labels.clone();
808 existing.node_types = node_types.clone();
809 existing.edge_labels = edge_labels.clone();
810 existing.last_materialized_sequence = None;
811 existing.clone()
812 } else {
813 let projection = PhysicalGraphProjection {
814 name: name.clone(),
815 created_at_unix_ms: now,
816 updated_at_unix_ms: now,
817 state: "declared".to_string(),
818 source: source.clone(),
819 node_labels: node_labels.clone(),
820 node_types: node_types.clone(),
821 edge_labels: edge_labels.clone(),
822 last_materialized_sequence: None,
823 };
824 metadata.graph_projections.push(projection.clone());
825 projection
826 };
827
828 Self::mark_projection_dependent_jobs_stale(metadata, &name, now);
829
830 metadata
831 .graph_projections
832 .sort_by(|left, right| left.name.cmp(&right.name));
833 projection
834 })
835 }
836
837 pub fn materialize_graph_projection(
839 &self,
840 name: &str,
841 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
842 self.update_physical_metadata(|metadata| {
843 let now = SystemTime::now()
844 .duration_since(UNIX_EPOCH)
845 .unwrap_or_default()
846 .as_millis();
847 let idx = metadata
848 .graph_projections
849 .iter()
850 .position(|projection| projection.name == name);
851 if let Some(idx) = idx {
852 metadata.graph_projections[idx].updated_at_unix_ms = now;
853 metadata.graph_projections[idx].state = "materialized".to_string();
854 metadata.graph_projections[idx].last_materialized_sequence =
855 Some(metadata.superblock.sequence);
856 let result = metadata.graph_projections[idx].clone();
857 Self::rearm_projection_dependent_jobs_declared(metadata, name, now);
858 return Some(result);
859 }
860 None
861 })
862 }
863
864 pub fn mark_graph_projection_materializing(
866 &self,
867 name: &str,
868 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
869 self.update_physical_metadata(|metadata| {
870 let now = SystemTime::now()
871 .duration_since(UNIX_EPOCH)
872 .unwrap_or_default()
873 .as_millis();
874 let idx = metadata
875 .graph_projections
876 .iter()
877 .position(|projection| projection.name == name);
878 if let Some(idx) = idx {
879 metadata.graph_projections[idx].updated_at_unix_ms = now;
880 metadata.graph_projections[idx].state = "materializing".to_string();
881 metadata.graph_projections[idx].last_materialized_sequence = None;
882 let result = metadata.graph_projections[idx].clone();
883 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
884 return Some(result);
885 }
886 None
887 })
888 }
889
890 pub fn fail_graph_projection(
892 &self,
893 name: &str,
894 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
895 self.update_physical_metadata(|metadata| {
896 let now = SystemTime::now()
897 .duration_since(UNIX_EPOCH)
898 .unwrap_or_default()
899 .as_millis();
900 let idx = metadata
901 .graph_projections
902 .iter()
903 .position(|projection| projection.name == name);
904 if let Some(idx) = idx {
905 metadata.graph_projections[idx].updated_at_unix_ms = now;
906 metadata.graph_projections[idx].state = "failed".to_string();
907 metadata.graph_projections[idx].last_materialized_sequence = None;
908 let result = metadata.graph_projections[idx].clone();
909 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
910 return Some(result);
911 }
912 None
913 })
914 }
915
916 pub fn mark_graph_projection_stale(
918 &self,
919 name: &str,
920 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
921 self.update_physical_metadata(|metadata| {
922 let now = SystemTime::now()
923 .duration_since(UNIX_EPOCH)
924 .unwrap_or_default()
925 .as_millis();
926 let idx = metadata
927 .graph_projections
928 .iter()
929 .position(|projection| projection.name == name);
930 if let Some(idx) = idx {
931 metadata.graph_projections[idx].updated_at_unix_ms = now;
932 metadata.graph_projections[idx].state = "stale".to_string();
933 let result = metadata.graph_projections[idx].clone();
934 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
935 return Some(result);
936 }
937 None
938 })
939 }
940
941 fn mark_projection_dependent_jobs_stale(
942 metadata: &mut PhysicalMetadataFile,
943 projection_name: &str,
944 now: u128,
945 ) {
946 for job in metadata.analytics_jobs.iter_mut() {
947 if job.projection.as_deref() == Some(projection_name) && job.state != "declared" {
948 job.state = "stale".to_string();
949 job.updated_at_unix_ms = now;
950 }
951 }
952 }
953
954 fn rearm_projection_dependent_jobs_declared(
955 metadata: &mut PhysicalMetadataFile,
956 projection_name: &str,
957 now: u128,
958 ) {
959 for job in metadata.analytics_jobs.iter_mut() {
960 if job.projection.as_deref() == Some(projection_name) && job.state == "stale" {
961 job.state = "declared".to_string();
962 job.last_run_sequence = None;
963 job.updated_at_unix_ms = now;
964 }
965 }
966 }
967
968 pub fn save_analytics_job(
970 &self,
971 kind: impl Into<String>,
972 projection: Option<String>,
973 metadata_entries: BTreeMap<String, String>,
974 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
975 let kind = kind.into();
976 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
977 if let Some(projection_name) = projection.as_deref() {
978 if !self.graph_projection_is_declared(projection_name) {
979 return Err(format!(
980 "graph projection '{projection_name}' is not declared in physical metadata"
981 )
982 .into());
983 }
984 }
985
986 self.update_physical_metadata(|metadata| {
987 let now = SystemTime::now()
988 .duration_since(UNIX_EPOCH)
989 .unwrap_or_default()
990 .as_millis();
991
992 let job = if let Some(existing) = metadata
993 .analytics_jobs
994 .iter_mut()
995 .find(|job| job.id == job_id)
996 {
997 existing.kind = kind.clone();
998 existing.projection = projection.clone();
999 existing.updated_at_unix_ms = now;
1000 existing.metadata = metadata_entries.clone();
1001 if existing.last_run_sequence.is_none() {
1002 existing.state = "declared".to_string();
1003 }
1004 existing.clone()
1005 } else {
1006 let job = PhysicalAnalyticsJob {
1007 id: job_id.clone(),
1008 kind: kind.clone(),
1009 state: "declared".to_string(),
1010 projection: projection.clone(),
1011 created_at_unix_ms: now,
1012 updated_at_unix_ms: now,
1013 last_run_sequence: None,
1014 metadata: metadata_entries.clone(),
1015 };
1016 metadata.analytics_jobs.push(job.clone());
1017 job
1018 };
1019
1020 metadata
1021 .analytics_jobs
1022 .sort_by(|left, right| left.id.cmp(&right.id));
1023 job
1024 })
1025 }
1026
1027 pub fn record_analytics_job(
1029 &self,
1030 kind: impl Into<String>,
1031 projection: Option<String>,
1032 metadata_entries: BTreeMap<String, String>,
1033 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1034 let kind = kind.into();
1035 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1036 if let Some(projection_name) = projection.as_deref() {
1037 if !self.graph_projection_is_declared(projection_name) {
1038 return Err(format!(
1039 "graph projection '{projection_name}' is not declared in physical metadata"
1040 )
1041 .into());
1042 }
1043 if !self.graph_projection_is_operational(projection_name) {
1044 return Err(format!(
1045 "graph projection '{projection_name}' is declared but not operationally materialized"
1046 )
1047 .into());
1048 }
1049 }
1050
1051 self.update_physical_metadata(|metadata| {
1052 let now = SystemTime::now()
1053 .duration_since(UNIX_EPOCH)
1054 .unwrap_or_default()
1055 .as_millis();
1056
1057 let existing = metadata
1058 .analytics_jobs
1059 .iter_mut()
1060 .find(|job| job.id == job_id)?;
1061 existing.state = "completed".to_string();
1062 existing.kind = kind.clone();
1063 existing.projection = projection.clone();
1064 existing.updated_at_unix_ms = now;
1065 existing.last_run_sequence = Some(metadata.superblock.sequence);
1066 existing.metadata = metadata_entries.clone();
1067 let job = existing.clone();
1068
1069 metadata
1070 .analytics_jobs
1071 .sort_by(|left, right| left.id.cmp(&right.id));
1072 Some(job)
1073 })
1074 .and_then(|job| {
1075 job.ok_or_else(|| {
1076 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1077 })
1078 })
1079 }
1080
1081 pub fn queue_analytics_job(
1083 &self,
1084 kind: impl Into<String>,
1085 projection: Option<String>,
1086 metadata_entries: BTreeMap<String, String>,
1087 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1088 let kind = kind.into();
1089 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1090 if let Some(projection_name) = projection.as_deref() {
1091 if !self.graph_projection_is_declared(projection_name) {
1092 return Err(format!(
1093 "graph projection '{projection_name}' is not declared in physical metadata"
1094 )
1095 .into());
1096 }
1097 if !self.graph_projection_is_operational(projection_name) {
1098 return Err(format!(
1099 "graph projection '{projection_name}' is declared but not operationally materialized"
1100 )
1101 .into());
1102 }
1103 }
1104
1105 self.update_physical_metadata(|metadata| {
1106 let now = SystemTime::now()
1107 .duration_since(UNIX_EPOCH)
1108 .unwrap_or_default()
1109 .as_millis();
1110
1111 let existing = metadata
1112 .analytics_jobs
1113 .iter_mut()
1114 .find(|job| job.id == job_id)?;
1115 existing.state = "queued".to_string();
1116 existing.kind = kind.clone();
1117 existing.projection = projection.clone();
1118 existing.updated_at_unix_ms = now;
1119 existing.metadata = metadata_entries.clone();
1120 let job = existing.clone();
1121
1122 metadata
1123 .analytics_jobs
1124 .sort_by(|left, right| left.id.cmp(&right.id));
1125 Some(job)
1126 })
1127 .and_then(|job| {
1128 job.ok_or_else(|| {
1129 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1130 })
1131 })
1132 }
1133
1134 pub fn start_analytics_job(
1136 &self,
1137 kind: impl Into<String>,
1138 projection: Option<String>,
1139 metadata_entries: BTreeMap<String, String>,
1140 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1141 let kind = kind.into();
1142 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1143 if let Some(projection_name) = projection.as_deref() {
1144 if !self.graph_projection_is_declared(projection_name) {
1145 return Err(format!(
1146 "graph projection '{projection_name}' is not declared in physical metadata"
1147 )
1148 .into());
1149 }
1150 if !self.graph_projection_is_operational(projection_name) {
1151 return Err(format!(
1152 "graph projection '{projection_name}' is declared but not operationally materialized"
1153 )
1154 .into());
1155 }
1156 }
1157
1158 self.update_physical_metadata(|metadata| {
1159 let now = SystemTime::now()
1160 .duration_since(UNIX_EPOCH)
1161 .unwrap_or_default()
1162 .as_millis();
1163
1164 let existing = metadata
1165 .analytics_jobs
1166 .iter_mut()
1167 .find(|job| job.id == job_id)?;
1168 existing.state = "running".to_string();
1169 existing.kind = kind.clone();
1170 existing.projection = projection.clone();
1171 existing.updated_at_unix_ms = now;
1172 existing.metadata = metadata_entries.clone();
1173 let job = existing.clone();
1174
1175 metadata
1176 .analytics_jobs
1177 .sort_by(|left, right| left.id.cmp(&right.id));
1178 Some(job)
1179 })
1180 .and_then(|job| {
1181 job.ok_or_else(|| {
1182 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1183 })
1184 })
1185 }
1186
1187 pub fn fail_analytics_job(
1189 &self,
1190 kind: impl Into<String>,
1191 projection: Option<String>,
1192 metadata_entries: BTreeMap<String, String>,
1193 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1194 let kind = kind.into();
1195 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1196
1197 self.update_physical_metadata(|metadata| {
1198 let now = SystemTime::now()
1199 .duration_since(UNIX_EPOCH)
1200 .unwrap_or_default()
1201 .as_millis();
1202
1203 let existing = metadata
1204 .analytics_jobs
1205 .iter_mut()
1206 .find(|job| job.id == job_id)?;
1207 existing.state = "failed".to_string();
1208 existing.kind = kind.clone();
1209 existing.projection = projection.clone();
1210 existing.updated_at_unix_ms = now;
1211 existing.metadata = metadata_entries.clone();
1212 let job = existing.clone();
1213
1214 metadata
1215 .analytics_jobs
1216 .sort_by(|left, right| left.id.cmp(&right.id));
1217 Some(job)
1218 })
1219 .and_then(|job| {
1220 job.ok_or_else(|| {
1221 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1222 })
1223 })
1224 }
1225
1226 pub fn mark_analytics_job_stale(
1228 &self,
1229 kind: impl Into<String>,
1230 projection: Option<String>,
1231 metadata_entries: BTreeMap<String, String>,
1232 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1233 let kind = kind.into();
1234 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1235
1236 self.update_physical_metadata(|metadata| {
1237 let now = SystemTime::now()
1238 .duration_since(UNIX_EPOCH)
1239 .unwrap_or_default()
1240 .as_millis();
1241
1242 let existing = metadata
1243 .analytics_jobs
1244 .iter_mut()
1245 .find(|job| job.id == job_id)?;
1246 existing.state = "stale".to_string();
1247 existing.kind = kind.clone();
1248 existing.projection = projection.clone();
1249 existing.updated_at_unix_ms = now;
1250 existing.metadata = metadata_entries.clone();
1251 let job = existing.clone();
1252
1253 metadata
1254 .analytics_jobs
1255 .sort_by(|left, right| left.id.cmp(&right.id));
1256 Some(job)
1257 })
1258 .and_then(|job| {
1259 job.ok_or_else(|| {
1260 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1261 })
1262 })
1263 }
1264
1265 pub fn create_named_export(
1267 &self,
1268 name: impl Into<String>,
1269 ) -> Result<ExportDescriptor, Box<dyn std::error::Error>> {
1270 let name = name.into();
1271 if self.options.mode != StorageMode::Persistent {
1272 return Err("exports require persistent mode".into());
1273 }
1274 let Some(path) = self.path() else {
1275 return Err("database path is not available".into());
1276 };
1277
1278 self.flush()?;
1279
1280 let _publication = self.store.vault_publication_lock.lock();
1281 let mut metadata = match self.load_or_bootstrap_physical_metadata(true) {
1282 Ok(metadata) => metadata,
1283 Err(_)
1284 if self.options.storage_profile.deploy_profile
1285 == crate::storage::DeployProfile::Embedded
1286 && self.options.storage_profile.packaging
1287 == crate::storage::StoragePackaging::SingleFile =>
1288 {
1289 crate::physical::PhysicalMetadataFile::from_state(
1290 self.options.clone(),
1291 self.catalog_snapshot(),
1292 self.physical_collection_roots(),
1293 self.physical_index_state(),
1294 None,
1295 )
1296 }
1297 Err(err) => return Err(err),
1298 };
1299 let export_data_path = reddb_file::copy_physical_export_data_file(path, &name)?;
1300
1301 let descriptor = ExportDescriptor {
1302 name: name.clone(),
1303 created_at_unix_ms: SystemTime::now()
1304 .duration_since(UNIX_EPOCH)
1305 .unwrap_or_default()
1306 .as_millis(),
1307 snapshot_id: metadata
1308 .snapshots
1309 .last()
1310 .map(|snapshot| snapshot.snapshot_id),
1311 superblock_sequence: metadata.superblock.sequence,
1312 data_path: export_data_path.display().to_string(),
1313 metadata_path: export_data_path.display().to_string(),
1314 collection_count: metadata.catalog.total_collections,
1315 total_entities: metadata.catalog.total_entities,
1316 };
1317
1318 metadata
1319 .exports
1320 .retain(|export| export.name != descriptor.name);
1321 metadata.exports.push(descriptor.clone());
1322 self.prune_export_registry(&mut metadata.exports);
1323 self.persist_physical_metadata_record(&metadata)?;
1324
1325 Ok(descriptor)
1326 }
1327
1328 pub fn set_index_enabled(
1330 &self,
1331 name: &str,
1332 enabled: bool,
1333 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1334 let Some(status) = self.index_status(name) else {
1335 return Err(format!("index '{name}' is not present in catalog status").into());
1336 };
1337 if !status.declared {
1338 return Err(format!("index '{name}' is not declared in physical metadata").into());
1339 }
1340 self.update_physical_metadata(|metadata| {
1341 let now = SystemTime::now()
1342 .duration_since(UNIX_EPOCH)
1343 .unwrap_or_default()
1344 .as_millis();
1345 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1346 index.enabled = enabled;
1347 if !enabled {
1348 index.build_state = "disabled".to_string();
1349 } else if index.build_state == "disabled" {
1350 index.build_state = if index.artifact_root_page.is_some() {
1351 "ready".to_string()
1352 } else {
1353 "declared-unbuilt".to_string()
1354 };
1355 }
1356 index.last_refresh_ms = Some(now);
1357 return Some(index.clone());
1358 }
1359 None
1360 })
1361 }
1362
1363 pub fn mark_index_building(
1365 &self,
1366 name: &str,
1367 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1368 let Some(status) = self.index_status(name) else {
1369 return Err(format!("index '{name}' is not present in catalog status").into());
1370 };
1371 if !status.declared {
1372 return Err(format!("index '{name}' is not declared in physical metadata").into());
1373 }
1374 if status.lifecycle_state == "disabled" {
1375 return Err(format!("index '{name}' is disabled").into());
1376 }
1377 self.update_physical_metadata(|metadata| {
1378 let now = SystemTime::now()
1379 .duration_since(UNIX_EPOCH)
1380 .unwrap_or_default()
1381 .as_millis();
1382 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1383 index.build_state = "building".to_string();
1384 index.last_refresh_ms = Some(now);
1385 return Some(index.clone());
1386 }
1387 None
1388 })
1389 }
1390
1391 pub fn fail_index(
1393 &self,
1394 name: &str,
1395 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1396 let Some(status) = self.index_status(name) else {
1397 return Err(format!("index '{name}' is not present in catalog status").into());
1398 };
1399 if !status.declared {
1400 return Err(format!("index '{name}' is not declared in physical metadata").into());
1401 }
1402 self.update_physical_metadata(|metadata| {
1403 let now = SystemTime::now()
1404 .duration_since(UNIX_EPOCH)
1405 .unwrap_or_default()
1406 .as_millis();
1407 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1408 index.build_state = "failed".to_string();
1409 index.last_refresh_ms = Some(now);
1410 return Some(index.clone());
1411 }
1412 None
1413 })
1414 }
1415
1416 pub fn mark_index_stale(
1418 &self,
1419 name: &str,
1420 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1421 let Some(status) = self.index_status(name) else {
1422 return Err(format!("index '{name}' is not present in catalog status").into());
1423 };
1424 if !status.declared {
1425 return Err(format!("index '{name}' is not declared in physical metadata").into());
1426 }
1427 self.update_physical_metadata(|metadata| {
1428 let now = SystemTime::now()
1429 .duration_since(UNIX_EPOCH)
1430 .unwrap_or_default()
1431 .as_millis();
1432 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1433 index.build_state = if index.enabled {
1434 "stale".to_string()
1435 } else {
1436 "disabled".to_string()
1437 };
1438 index.last_refresh_ms = Some(now);
1439 return Some(index.clone());
1440 }
1441 None
1442 })
1443 }
1444
1445 pub fn mark_index_ready(
1447 &self,
1448 name: &str,
1449 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1450 self.warmup_index(name)
1451 }
1452
1453 pub fn warmup_index(
1455 &self,
1456 name: &str,
1457 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1458 let Some(status) = self.index_status(name) else {
1459 return Err(format!("index '{name}' is not present in catalog status").into());
1460 };
1461 if !status.declared {
1462 return Err(format!("index '{name}' is not declared in physical metadata").into());
1463 }
1464 if status.lifecycle_state == "disabled" {
1465 return Err(format!("index '{name}' is disabled").into());
1466 }
1467 if !status.operational {
1468 return Err(
1469 format!("index '{name}' is declared but not operationally materialized").into(),
1470 );
1471 }
1472 let warmed_artifact = self
1473 .physical_indexes()
1474 .into_iter()
1475 .find(|index| index.name == name)
1476 .map(|mut index| {
1477 self.warmup_native_vector_artifact_for_index(&index)?;
1478 self.apply_runtime_native_artifact_to_index_state(&mut index)?;
1479 Ok::<_, String>(index)
1480 })
1481 .transpose()
1482 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1483
1484 self.update_physical_metadata(|metadata| {
1485 let now = SystemTime::now()
1486 .duration_since(UNIX_EPOCH)
1487 .unwrap_or_default()
1488 .as_millis();
1489 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1490 if let Some(warmed) = warmed_artifact.as_ref() {
1491 index.entries = warmed.entries;
1492 index.estimated_memory_bytes = warmed.estimated_memory_bytes;
1493 index.backend = warmed.backend.clone();
1494 index.build_state = "ready".to_string();
1495 }
1496 index.last_refresh_ms = Some(now);
1497 return Some(index.clone());
1498 }
1499 None
1500 })
1501 }
1502
1503 pub fn rebuild_index_registry(
1505 &self,
1506 collection: Option<&str>,
1507 ) -> Result<Vec<PhysicalIndexState>, Box<dyn std::error::Error>> {
1508 let fresh = self.reconcile_index_states_with_native_artifacts(self.physical_index_state());
1509 self.update_physical_metadata(|metadata| {
1510 let now = SystemTime::now()
1511 .duration_since(UNIX_EPOCH)
1512 .unwrap_or_default()
1513 .as_millis();
1514
1515 let mut affected = Vec::new();
1516 let declared = metadata.indexes.clone();
1517 for declared_index in declared {
1518 let matches_collection = collection.is_none_or(|collection_name| {
1519 declared_index.collection.as_deref() == Some(collection_name)
1520 });
1521 if !matches_collection {
1522 continue;
1523 }
1524
1525 let mut rebuilt = fresh
1526 .iter()
1527 .find(|index| index.name == declared_index.name)
1528 .cloned()
1529 .unwrap_or_else(|| {
1530 let mut index = declared_index.clone();
1531 index.build_state = "declared-unbuilt".to_string();
1532 index
1533 });
1534 rebuilt.enabled = declared_index.enabled;
1535 rebuilt.artifact_kind = rebuilt
1536 .artifact_kind
1537 .or_else(|| declared_index.artifact_kind.clone());
1538 rebuilt.artifact_root_page = rebuilt
1539 .artifact_root_page
1540 .or(declared_index.artifact_root_page);
1541 rebuilt.artifact_checksum = rebuilt
1542 .artifact_checksum
1543 .or(declared_index.artifact_checksum);
1544 rebuilt.build_state =
1545 Self::finalize_rebuilt_index_build_state(&declared_index, &rebuilt);
1546 rebuilt.last_refresh_ms = Some(now);
1547
1548 if let Some(existing) = metadata
1549 .indexes
1550 .iter_mut()
1551 .find(|index| index.name == rebuilt.name)
1552 {
1553 *existing = rebuilt.clone();
1554 } else {
1555 metadata.indexes.push(rebuilt.clone());
1556 }
1557
1558 affected.push(rebuilt);
1559 }
1560
1561 affected
1562 })
1563 }
1564
1565 fn finalize_rebuilt_index_build_state(
1566 declared: &PhysicalIndexState,
1567 rebuilt: &PhysicalIndexState,
1568 ) -> String {
1569 if !rebuilt.enabled {
1570 return "disabled".to_string();
1571 }
1572
1573 if declared.build_state == "failed" || rebuilt.build_state == "failed" {
1574 return "failed".to_string();
1575 }
1576
1577 let native_artifact_family = Self::native_artifact_kind_for_index(rebuilt.kind).is_some();
1578 if native_artifact_family {
1579 if rebuilt.artifact_root_page.is_some() && rebuilt.artifact_checksum.is_some() {
1580 return "ready".to_string();
1581 }
1582 if declared.artifact_root_page.is_some()
1583 || declared.artifact_checksum.is_some()
1584 || declared.artifact_kind.is_some()
1585 {
1586 return "stale".to_string();
1587 }
1588 return "declared-unbuilt".to_string();
1589 }
1590
1591 if rebuilt.entries > 0 {
1592 return "ready".to_string();
1593 }
1594
1595 if matches!(
1596 declared.build_state.as_str(),
1597 "stale" | "artifact-published" | "registry-loaded"
1598 ) {
1599 return "stale".to_string();
1600 }
1601
1602 "declared-unbuilt".to_string()
1603 }
1604}