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 let mut contracts = self.collection_contracts();
385 contracts.retain(|existing| existing.name != contract.name);
386 contracts.push(contract.clone());
387 let bytes = crate::physical::serialize_collection_contracts(&contracts);
388 self.store.persist_aux_metadata(bytes)?;
389 self.collection_contract_cache.insert(contract.clone());
390 return Ok(contract);
391 }
392
393 self.invalidate_collection_contract_cache();
394
395 let saved = self.update_collection_contract_metadata(|metadata| {
396 if let Some(existing) = metadata
397 .collection_contracts
398 .iter_mut()
399 .find(|existing| existing.name == contract.name)
400 {
401 *existing = contract.clone();
402 } else {
403 metadata.collection_contracts.push(contract.clone());
404 }
405 metadata
406 .collection_contracts
407 .sort_by(|left, right| left.name.cmp(&right.name));
408
409 if let Some(ttl_ms) = contract.default_ttl_ms {
410 metadata
411 .collection_ttl_defaults_ms
412 .insert(contract.name.clone(), ttl_ms);
413 } else {
414 metadata.collection_ttl_defaults_ms.remove(&contract.name);
415 }
416
417 contract.clone()
418 })?;
419 self.invalidate_collection_contract_cache();
420 Ok(saved)
421 }
422
423 pub fn remove_collection_contract(
424 &self,
425 collection: &str,
426 ) -> Result<Option<crate::physical::CollectionContract>, Box<dyn std::error::Error>> {
427 if self.options.read_only {
428 return Err("physical metadata operations are not allowed in read-only mode".into());
429 }
430 let _publication = self.store.vault_publication_lock.lock();
431 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
432 defaults.remove(collection);
433 }
434
435 self.store()
436 .context_index()
437 .set_collection_enabled(collection, false);
438
439 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
440 && self.options.storage_profile.packaging
441 == crate::storage::StoragePackaging::SingleFile
442 && !self.paged_mode
443 {
444 let removed = self.collection_contract_cache.remove(collection);
445 self.sync_single_file_contract_blob()?;
446 return Ok(removed);
447 }
448
449 self.invalidate_collection_contract_cache();
450
451 if self.paged_mode {
452 let Some(_) = self.path() else {
453 return Err("database path is not available".into());
454 };
455 let previous = self.load_or_bootstrap_physical_metadata(false)?;
469 let mut roots = previous.superblock.collection_roots.clone();
470 roots.remove(collection);
471 let indexes = previous
472 .indexes
473 .iter()
474 .filter(|index| index.collection.as_deref() != Some(collection))
475 .cloned()
476 .collect();
477 let mut metadata = PhysicalMetadataFile::from_state(
478 self.options.clone(),
479 self.catalog_snapshot(),
480 roots,
481 indexes,
482 Some(&previous),
483 );
484 let removed = metadata
485 .collection_contracts
486 .iter()
487 .position(|contract| contract.name == collection)
488 .map(|index| metadata.collection_contracts.remove(index));
489 metadata.collection_ttl_defaults_ms.remove(collection);
490 self.persist_physical_metadata_record(&metadata)?;
491 self.persist_native_physical_header(&metadata)?;
492 self.invalidate_collection_contract_cache();
493 return Ok(removed);
494 }
495
496 let removed = self.update_collection_contract_metadata(|metadata| {
497 let removed = metadata
498 .collection_contracts
499 .iter()
500 .position(|contract| contract.name == collection)
501 .map(|index| metadata.collection_contracts.remove(index));
502 metadata.collection_ttl_defaults_ms.remove(collection);
503 metadata
504 .indexes
505 .retain(|index| index.collection.as_deref() != Some(collection));
506 removed
507 })?;
508 self.invalidate_collection_contract_cache();
509 Ok(removed)
510 }
511
512 pub(crate) fn collection_ttl_defaults_snapshot(&self) -> BTreeMap<String, u64> {
513 self.collection_ttl_defaults_ms
514 .read()
515 .map(|defaults| {
516 defaults
517 .iter()
518 .map(|(collection, ttl_ms)| (collection.clone(), *ttl_ms))
519 .collect()
520 })
521 .unwrap_or_default()
522 }
523
524 pub(crate) fn load_collection_ttl_defaults_from_metadata(&self) {
525 let metadata = self.physical_metadata();
526 let defaults = metadata
527 .as_ref()
528 .map(|m| m.collection_ttl_defaults_ms.clone())
529 .unwrap_or_default();
530
531 if let Ok(mut current) = self.collection_ttl_defaults_ms.write() {
532 current.clear();
533 current.extend(defaults);
534 }
535
536 if let Some(metadata) = metadata {
537 let store = self.store();
538 let index = store.context_index();
539 for contract in &metadata.collection_contracts {
540 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
541 }
542 }
543 }
544
545 pub(crate) fn hypertable_registry_snapshot(&self) -> Vec<crate::physical::PhysicalHypertable> {
553 let registry = self.hypertables();
554 if registry.is_empty() {
555 return Vec::new();
556 }
557 use std::collections::BTreeMap;
558 let mut chunks_by_table: BTreeMap<String, Vec<crate::physical::PhysicalHypertableChunk>> =
559 BTreeMap::new();
560 for chunk in registry.snapshot_chunks() {
561 chunks_by_table
562 .entry(chunk.id.hypertable.clone())
563 .or_default()
564 .push(crate::physical::PhysicalHypertableChunk {
565 start_ns: chunk.id.start_ns,
566 end_ns_exclusive: chunk.end_ns_exclusive,
567 row_count: chunk.row_count,
568 min_ts_ns: chunk.min_ts_ns,
569 max_ts_ns: chunk.max_ts_ns,
570 sealed: chunk.sealed,
571 ttl_override_ns: chunk.ttl_override_ns,
572 columnar_page: chunk.columnar_page,
573 columnar_derived: chunk.columnar_page.is_some(),
574 });
575 }
576 registry
577 .list()
578 .into_iter()
579 .map(|spec| crate::physical::PhysicalHypertable {
580 chunks: chunks_by_table.remove(&spec.name).unwrap_or_default(),
581 name: spec.name,
582 time_column: spec.time_column,
583 chunk_interval_ns: spec.chunk_interval_ns,
584 default_ttl_ns: spec.default_ttl_ns,
585 })
586 .collect()
587 }
588
589 pub(crate) fn load_hypertables_from_metadata(&self) {
595 let Some(metadata) = self.physical_metadata() else {
596 return;
597 };
598 if metadata.hypertables.is_empty() {
599 return;
600 }
601 let registry = self.hypertables();
602 for hypertable in &metadata.hypertables {
603 let mut spec = crate::storage::timeseries::HypertableSpec::new(
604 hypertable.name.clone(),
605 hypertable.time_column.clone(),
606 hypertable.chunk_interval_ns,
607 );
608 if let Some(ttl) = hypertable.default_ttl_ns {
609 spec = spec.with_ttl_ns(ttl);
610 }
611 registry.register(spec);
612 for chunk in &hypertable.chunks {
613 let meta = crate::storage::timeseries::ChunkMeta {
614 id: crate::storage::timeseries::ChunkId {
615 hypertable: hypertable.name.clone(),
616 start_ns: chunk.start_ns,
617 },
618 end_ns_exclusive: chunk.end_ns_exclusive,
619 row_count: chunk.row_count,
620 min_ts_ns: chunk.min_ts_ns,
621 max_ts_ns: chunk.max_ts_ns,
622 sealed: chunk.sealed,
623 ttl_override_ns: chunk.ttl_override_ns,
624 columnar_page: chunk.columnar_page,
625 };
626 let id = meta.id.clone();
627 let columnar_page = meta.columnar_page;
628 registry.restore_chunk(meta);
629 if let Some(loc) = columnar_page {
630 if let Ok(Some(bytes)) = self.read_column_block_page(loc) {
631 registry.restore_columnar_block(&id, bytes);
632 }
633 }
634 }
635 }
636 }
637
638 pub fn run_maintenance(&self) -> Result<(), Box<dyn std::error::Error>> {
639 self.store.run_maintenance()?;
640 self.persist_metadata()?;
641 if self.paged_mode {
642 if let Some(path) = self.path() {
643 self.remove_imported_metadata_sidecars(path)?;
644 }
645 }
646 Ok(())
647 }
648
649 pub fn metadata_path(&self) -> Option<PathBuf> {
651 self.path
652 .as_ref()
653 .map(|path| PhysicalMetadataFile::metadata_path_for(path))
654 }
655
656 pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
658 self.load_or_bootstrap_physical_metadata(false).ok()
659 }
660
661 pub fn physical_indexes(&self) -> Vec<PhysicalIndexState> {
663 let indexes = self
664 .physical_metadata()
665 .map(|metadata| metadata.indexes)
666 .unwrap_or_default();
667 self.physical_indexes_from_declarations(indexes)
668 }
669
670 pub(crate) fn physical_indexes_from_declarations(
671 &self,
672 indexes: Vec<PhysicalIndexState>,
673 ) -> Vec<PhysicalIndexState> {
674 let indexes = Some(indexes)
675 .filter(|indexes| !indexes.is_empty())
676 .or_else(|| {
677 self.native_physical_state()
678 .map(|state| self.physical_index_state_from_native_state(&state, None))
679 })
680 .unwrap_or_else(|| self.physical_index_state());
681 self.reconcile_index_states_with_native_artifacts(indexes)
682 }
683
684 pub fn exports(&self) -> Vec<ExportDescriptor> {
686 self.physical_metadata()
687 .map(|metadata| metadata.exports)
688 .or_else(|| {
689 self.native_physical_state()
690 .map(|state| self.exports_from_native_state(&state))
691 })
692 .unwrap_or_default()
693 }
694
695 pub fn snapshots(&self) -> Vec<crate::physical::SnapshotDescriptor> {
697 if let Some(metadata) = self.physical_metadata() {
698 if !metadata.snapshots.is_empty() {
699 return metadata.snapshots;
700 }
701 }
702 self.native_physical_state()
703 .map(|state| self.snapshots_from_native_state(&state))
704 .unwrap_or_default()
705 }
706
707 pub fn graph_projections(&self) -> Vec<PhysicalGraphProjection> {
709 self.physical_metadata()
710 .map(|metadata| metadata.graph_projections)
711 .or_else(|| {
712 self.native_physical_state()
713 .map(|state| self.graph_projections_from_native_state(&state))
714 })
715 .unwrap_or_default()
716 }
717
718 pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
720 self.catalog_model_snapshot().declared_graph_projections
721 }
722
723 pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
725 self.graph_projections()
726 .into_iter()
727 .filter(|projection| {
728 projection.last_materialized_sequence.is_some()
729 || matches!(projection.state.as_str(), "materialized" | "stale")
730 })
731 .collect()
732 }
733
734 pub fn analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
736 self.physical_metadata()
737 .map(|metadata| metadata.analytics_jobs)
738 .or_else(|| {
739 self.native_physical_state()
740 .map(|state| self.analytics_jobs_from_native_state(&state))
741 })
742 .unwrap_or_default()
743 }
744
745 pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
747 self.catalog_model_snapshot().declared_analytics_jobs
748 }
749
750 pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
752 self.analytics_jobs()
753 .into_iter()
754 .filter(|job| {
755 job.last_run_sequence.is_some()
756 || matches!(
757 job.state.as_str(),
758 "running" | "completed" | "failed" | "queued" | "stale"
759 )
760 })
761 .collect()
762 }
763
764 pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
766 self.catalog_model_snapshot().declared_indexes
767 }
768
769 pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
771 self.catalog_model_snapshot().operational_indexes
772 }
773
774 pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
776 self.catalog_model_snapshot().index_statuses
777 }
778
779 pub fn index_status(&self, name: &str) -> Option<CatalogIndexStatus> {
781 self.catalog_model_snapshot()
782 .index_statuses
783 .into_iter()
784 .find(|status| status.name == name)
785 }
786
787 pub fn save_graph_projection(
789 &self,
790 name: impl Into<String>,
791 node_labels: Vec<String>,
792 node_types: Vec<String>,
793 edge_labels: Vec<String>,
794 source: impl Into<String>,
795 ) -> Result<PhysicalGraphProjection, Box<dyn std::error::Error>> {
796 let name = name.into();
797 let source = source.into();
798 self.update_physical_metadata(|metadata| {
799 let now = SystemTime::now()
800 .duration_since(UNIX_EPOCH)
801 .unwrap_or_default()
802 .as_millis();
803 let projection = if let Some(existing) = metadata
804 .graph_projections
805 .iter_mut()
806 .find(|projection| projection.name == name)
807 {
808 existing.updated_at_unix_ms = now;
809 existing.state = "declared".to_string();
810 existing.source = source.clone();
811 existing.node_labels = node_labels.clone();
812 existing.node_types = node_types.clone();
813 existing.edge_labels = edge_labels.clone();
814 existing.last_materialized_sequence = None;
815 existing.clone()
816 } else {
817 let projection = PhysicalGraphProjection {
818 name: name.clone(),
819 created_at_unix_ms: now,
820 updated_at_unix_ms: now,
821 state: "declared".to_string(),
822 source: source.clone(),
823 node_labels: node_labels.clone(),
824 node_types: node_types.clone(),
825 edge_labels: edge_labels.clone(),
826 last_materialized_sequence: None,
827 };
828 metadata.graph_projections.push(projection.clone());
829 projection
830 };
831
832 Self::mark_projection_dependent_jobs_stale(metadata, &name, now);
833
834 metadata
835 .graph_projections
836 .sort_by(|left, right| left.name.cmp(&right.name));
837 projection
838 })
839 }
840
841 pub fn materialize_graph_projection(
843 &self,
844 name: &str,
845 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
846 self.update_physical_metadata(|metadata| {
847 let now = SystemTime::now()
848 .duration_since(UNIX_EPOCH)
849 .unwrap_or_default()
850 .as_millis();
851 let idx = metadata
852 .graph_projections
853 .iter()
854 .position(|projection| projection.name == name);
855 if let Some(idx) = idx {
856 metadata.graph_projections[idx].updated_at_unix_ms = now;
857 metadata.graph_projections[idx].state = "materialized".to_string();
858 metadata.graph_projections[idx].last_materialized_sequence =
859 Some(metadata.superblock.sequence);
860 let result = metadata.graph_projections[idx].clone();
861 Self::rearm_projection_dependent_jobs_declared(metadata, name, now);
862 return Some(result);
863 }
864 None
865 })
866 }
867
868 pub fn mark_graph_projection_materializing(
870 &self,
871 name: &str,
872 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
873 self.update_physical_metadata(|metadata| {
874 let now = SystemTime::now()
875 .duration_since(UNIX_EPOCH)
876 .unwrap_or_default()
877 .as_millis();
878 let idx = metadata
879 .graph_projections
880 .iter()
881 .position(|projection| projection.name == name);
882 if let Some(idx) = idx {
883 metadata.graph_projections[idx].updated_at_unix_ms = now;
884 metadata.graph_projections[idx].state = "materializing".to_string();
885 metadata.graph_projections[idx].last_materialized_sequence = None;
886 let result = metadata.graph_projections[idx].clone();
887 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
888 return Some(result);
889 }
890 None
891 })
892 }
893
894 pub fn fail_graph_projection(
896 &self,
897 name: &str,
898 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
899 self.update_physical_metadata(|metadata| {
900 let now = SystemTime::now()
901 .duration_since(UNIX_EPOCH)
902 .unwrap_or_default()
903 .as_millis();
904 let idx = metadata
905 .graph_projections
906 .iter()
907 .position(|projection| projection.name == name);
908 if let Some(idx) = idx {
909 metadata.graph_projections[idx].updated_at_unix_ms = now;
910 metadata.graph_projections[idx].state = "failed".to_string();
911 metadata.graph_projections[idx].last_materialized_sequence = None;
912 let result = metadata.graph_projections[idx].clone();
913 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
914 return Some(result);
915 }
916 None
917 })
918 }
919
920 pub fn mark_graph_projection_stale(
922 &self,
923 name: &str,
924 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
925 self.update_physical_metadata(|metadata| {
926 let now = SystemTime::now()
927 .duration_since(UNIX_EPOCH)
928 .unwrap_or_default()
929 .as_millis();
930 let idx = metadata
931 .graph_projections
932 .iter()
933 .position(|projection| projection.name == name);
934 if let Some(idx) = idx {
935 metadata.graph_projections[idx].updated_at_unix_ms = now;
936 metadata.graph_projections[idx].state = "stale".to_string();
937 let result = metadata.graph_projections[idx].clone();
938 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
939 return Some(result);
940 }
941 None
942 })
943 }
944
945 fn mark_projection_dependent_jobs_stale(
946 metadata: &mut PhysicalMetadataFile,
947 projection_name: &str,
948 now: u128,
949 ) {
950 for job in metadata.analytics_jobs.iter_mut() {
951 if job.projection.as_deref() == Some(projection_name) && job.state != "declared" {
952 job.state = "stale".to_string();
953 job.updated_at_unix_ms = now;
954 }
955 }
956 }
957
958 fn rearm_projection_dependent_jobs_declared(
959 metadata: &mut PhysicalMetadataFile,
960 projection_name: &str,
961 now: u128,
962 ) {
963 for job in metadata.analytics_jobs.iter_mut() {
964 if job.projection.as_deref() == Some(projection_name) && job.state == "stale" {
965 job.state = "declared".to_string();
966 job.last_run_sequence = None;
967 job.updated_at_unix_ms = now;
968 }
969 }
970 }
971
972 pub fn save_analytics_job(
974 &self,
975 kind: impl Into<String>,
976 projection: Option<String>,
977 metadata_entries: BTreeMap<String, String>,
978 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
979 let kind = kind.into();
980 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
981 if let Some(projection_name) = projection.as_deref() {
982 if !self.graph_projection_is_declared(projection_name) {
983 return Err(format!(
984 "graph projection '{projection_name}' is not declared in physical metadata"
985 )
986 .into());
987 }
988 }
989
990 self.update_physical_metadata(|metadata| {
991 let now = SystemTime::now()
992 .duration_since(UNIX_EPOCH)
993 .unwrap_or_default()
994 .as_millis();
995
996 let job = if let Some(existing) = metadata
997 .analytics_jobs
998 .iter_mut()
999 .find(|job| job.id == job_id)
1000 {
1001 existing.kind = kind.clone();
1002 existing.projection = projection.clone();
1003 existing.updated_at_unix_ms = now;
1004 existing.metadata = metadata_entries.clone();
1005 if existing.last_run_sequence.is_none() {
1006 existing.state = "declared".to_string();
1007 }
1008 existing.clone()
1009 } else {
1010 let job = PhysicalAnalyticsJob {
1011 id: job_id.clone(),
1012 kind: kind.clone(),
1013 state: "declared".to_string(),
1014 projection: projection.clone(),
1015 created_at_unix_ms: now,
1016 updated_at_unix_ms: now,
1017 last_run_sequence: None,
1018 metadata: metadata_entries.clone(),
1019 };
1020 metadata.analytics_jobs.push(job.clone());
1021 job
1022 };
1023
1024 metadata
1025 .analytics_jobs
1026 .sort_by(|left, right| left.id.cmp(&right.id));
1027 job
1028 })
1029 }
1030
1031 pub fn record_analytics_job(
1033 &self,
1034 kind: impl Into<String>,
1035 projection: Option<String>,
1036 metadata_entries: BTreeMap<String, String>,
1037 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1038 let kind = kind.into();
1039 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1040 if let Some(projection_name) = projection.as_deref() {
1041 if !self.graph_projection_is_declared(projection_name) {
1042 return Err(format!(
1043 "graph projection '{projection_name}' is not declared in physical metadata"
1044 )
1045 .into());
1046 }
1047 if !self.graph_projection_is_operational(projection_name) {
1048 return Err(format!(
1049 "graph projection '{projection_name}' is declared but not operationally materialized"
1050 )
1051 .into());
1052 }
1053 }
1054
1055 self.update_physical_metadata(|metadata| {
1056 let now = SystemTime::now()
1057 .duration_since(UNIX_EPOCH)
1058 .unwrap_or_default()
1059 .as_millis();
1060
1061 let existing = metadata
1062 .analytics_jobs
1063 .iter_mut()
1064 .find(|job| job.id == job_id)?;
1065 existing.state = "completed".to_string();
1066 existing.kind = kind.clone();
1067 existing.projection = projection.clone();
1068 existing.updated_at_unix_ms = now;
1069 existing.last_run_sequence = Some(metadata.superblock.sequence);
1070 existing.metadata = metadata_entries.clone();
1071 let job = existing.clone();
1072
1073 metadata
1074 .analytics_jobs
1075 .sort_by(|left, right| left.id.cmp(&right.id));
1076 Some(job)
1077 })
1078 .and_then(|job| {
1079 job.ok_or_else(|| {
1080 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1081 })
1082 })
1083 }
1084
1085 pub fn queue_analytics_job(
1087 &self,
1088 kind: impl Into<String>,
1089 projection: Option<String>,
1090 metadata_entries: BTreeMap<String, String>,
1091 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1092 let kind = kind.into();
1093 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1094 if let Some(projection_name) = projection.as_deref() {
1095 if !self.graph_projection_is_declared(projection_name) {
1096 return Err(format!(
1097 "graph projection '{projection_name}' is not declared in physical metadata"
1098 )
1099 .into());
1100 }
1101 if !self.graph_projection_is_operational(projection_name) {
1102 return Err(format!(
1103 "graph projection '{projection_name}' is declared but not operationally materialized"
1104 )
1105 .into());
1106 }
1107 }
1108
1109 self.update_physical_metadata(|metadata| {
1110 let now = SystemTime::now()
1111 .duration_since(UNIX_EPOCH)
1112 .unwrap_or_default()
1113 .as_millis();
1114
1115 let existing = metadata
1116 .analytics_jobs
1117 .iter_mut()
1118 .find(|job| job.id == job_id)?;
1119 existing.state = "queued".to_string();
1120 existing.kind = kind.clone();
1121 existing.projection = projection.clone();
1122 existing.updated_at_unix_ms = now;
1123 existing.metadata = metadata_entries.clone();
1124 let job = existing.clone();
1125
1126 metadata
1127 .analytics_jobs
1128 .sort_by(|left, right| left.id.cmp(&right.id));
1129 Some(job)
1130 })
1131 .and_then(|job| {
1132 job.ok_or_else(|| {
1133 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1134 })
1135 })
1136 }
1137
1138 pub fn start_analytics_job(
1140 &self,
1141 kind: impl Into<String>,
1142 projection: Option<String>,
1143 metadata_entries: BTreeMap<String, String>,
1144 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1145 let kind = kind.into();
1146 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1147 if let Some(projection_name) = projection.as_deref() {
1148 if !self.graph_projection_is_declared(projection_name) {
1149 return Err(format!(
1150 "graph projection '{projection_name}' is not declared in physical metadata"
1151 )
1152 .into());
1153 }
1154 if !self.graph_projection_is_operational(projection_name) {
1155 return Err(format!(
1156 "graph projection '{projection_name}' is declared but not operationally materialized"
1157 )
1158 .into());
1159 }
1160 }
1161
1162 self.update_physical_metadata(|metadata| {
1163 let now = SystemTime::now()
1164 .duration_since(UNIX_EPOCH)
1165 .unwrap_or_default()
1166 .as_millis();
1167
1168 let existing = metadata
1169 .analytics_jobs
1170 .iter_mut()
1171 .find(|job| job.id == job_id)?;
1172 existing.state = "running".to_string();
1173 existing.kind = kind.clone();
1174 existing.projection = projection.clone();
1175 existing.updated_at_unix_ms = now;
1176 existing.metadata = metadata_entries.clone();
1177 let job = existing.clone();
1178
1179 metadata
1180 .analytics_jobs
1181 .sort_by(|left, right| left.id.cmp(&right.id));
1182 Some(job)
1183 })
1184 .and_then(|job| {
1185 job.ok_or_else(|| {
1186 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1187 })
1188 })
1189 }
1190
1191 pub fn fail_analytics_job(
1193 &self,
1194 kind: impl Into<String>,
1195 projection: Option<String>,
1196 metadata_entries: BTreeMap<String, String>,
1197 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1198 let kind = kind.into();
1199 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1200
1201 self.update_physical_metadata(|metadata| {
1202 let now = SystemTime::now()
1203 .duration_since(UNIX_EPOCH)
1204 .unwrap_or_default()
1205 .as_millis();
1206
1207 let existing = metadata
1208 .analytics_jobs
1209 .iter_mut()
1210 .find(|job| job.id == job_id)?;
1211 existing.state = "failed".to_string();
1212 existing.kind = kind.clone();
1213 existing.projection = projection.clone();
1214 existing.updated_at_unix_ms = now;
1215 existing.metadata = metadata_entries.clone();
1216 let job = existing.clone();
1217
1218 metadata
1219 .analytics_jobs
1220 .sort_by(|left, right| left.id.cmp(&right.id));
1221 Some(job)
1222 })
1223 .and_then(|job| {
1224 job.ok_or_else(|| {
1225 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1226 })
1227 })
1228 }
1229
1230 pub fn mark_analytics_job_stale(
1232 &self,
1233 kind: impl Into<String>,
1234 projection: Option<String>,
1235 metadata_entries: BTreeMap<String, String>,
1236 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1237 let kind = kind.into();
1238 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1239
1240 self.update_physical_metadata(|metadata| {
1241 let now = SystemTime::now()
1242 .duration_since(UNIX_EPOCH)
1243 .unwrap_or_default()
1244 .as_millis();
1245
1246 let existing = metadata
1247 .analytics_jobs
1248 .iter_mut()
1249 .find(|job| job.id == job_id)?;
1250 existing.state = "stale".to_string();
1251 existing.kind = kind.clone();
1252 existing.projection = projection.clone();
1253 existing.updated_at_unix_ms = now;
1254 existing.metadata = metadata_entries.clone();
1255 let job = existing.clone();
1256
1257 metadata
1258 .analytics_jobs
1259 .sort_by(|left, right| left.id.cmp(&right.id));
1260 Some(job)
1261 })
1262 .and_then(|job| {
1263 job.ok_or_else(|| {
1264 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1265 })
1266 })
1267 }
1268
1269 pub fn create_named_export(
1271 &self,
1272 name: impl Into<String>,
1273 ) -> Result<ExportDescriptor, Box<dyn std::error::Error>> {
1274 let name = name.into();
1275 if self.options.mode != StorageMode::Persistent {
1276 return Err("exports require persistent mode".into());
1277 }
1278 let Some(path) = self.path() else {
1279 return Err("database path is not available".into());
1280 };
1281
1282 self.flush()?;
1283
1284 let _publication = self.store.vault_publication_lock.lock();
1285 let mut metadata = match self.load_or_bootstrap_physical_metadata(true) {
1286 Ok(metadata) => metadata,
1287 Err(_)
1288 if self.options.storage_profile.deploy_profile
1289 == crate::storage::DeployProfile::Embedded
1290 && self.options.storage_profile.packaging
1291 == crate::storage::StoragePackaging::SingleFile =>
1292 {
1293 crate::physical::PhysicalMetadataFile::from_state(
1294 self.options.clone(),
1295 self.catalog_snapshot(),
1296 self.physical_collection_roots(),
1297 self.physical_index_state(),
1298 None,
1299 )
1300 }
1301 Err(err) => return Err(err),
1302 };
1303 let export_data_path = reddb_file::copy_physical_export_data_file(path, &name)?;
1304
1305 let descriptor = ExportDescriptor {
1306 name: name.clone(),
1307 created_at_unix_ms: SystemTime::now()
1308 .duration_since(UNIX_EPOCH)
1309 .unwrap_or_default()
1310 .as_millis(),
1311 snapshot_id: metadata
1312 .snapshots
1313 .last()
1314 .map(|snapshot| snapshot.snapshot_id),
1315 superblock_sequence: metadata.superblock.sequence,
1316 data_path: export_data_path.display().to_string(),
1317 metadata_path: export_data_path.display().to_string(),
1318 collection_count: metadata.catalog.total_collections,
1319 total_entities: metadata.catalog.total_entities,
1320 };
1321
1322 metadata
1323 .exports
1324 .retain(|export| export.name != descriptor.name);
1325 metadata.exports.push(descriptor.clone());
1326 self.prune_export_registry(&mut metadata.exports);
1327 self.persist_physical_metadata_record(&metadata)?;
1328
1329 Ok(descriptor)
1330 }
1331
1332 pub fn set_index_enabled(
1334 &self,
1335 name: &str,
1336 enabled: bool,
1337 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1338 let Some(status) = self.index_status(name) else {
1339 return Err(format!("index '{name}' is not present in catalog status").into());
1340 };
1341 if !status.declared {
1342 return Err(format!("index '{name}' is not declared in physical metadata").into());
1343 }
1344 self.update_physical_metadata(|metadata| {
1345 let now = SystemTime::now()
1346 .duration_since(UNIX_EPOCH)
1347 .unwrap_or_default()
1348 .as_millis();
1349 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1350 index.enabled = enabled;
1351 if !enabled {
1352 index.build_state = "disabled".to_string();
1353 } else if index.build_state == "disabled" {
1354 index.build_state = if index.artifact_root_page.is_some() {
1355 "ready".to_string()
1356 } else {
1357 "declared-unbuilt".to_string()
1358 };
1359 }
1360 index.last_refresh_ms = Some(now);
1361 return Some(index.clone());
1362 }
1363 None
1364 })
1365 }
1366
1367 pub fn mark_index_building(
1369 &self,
1370 name: &str,
1371 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1372 let Some(status) = self.index_status(name) else {
1373 return Err(format!("index '{name}' is not present in catalog status").into());
1374 };
1375 if !status.declared {
1376 return Err(format!("index '{name}' is not declared in physical metadata").into());
1377 }
1378 if status.lifecycle_state == "disabled" {
1379 return Err(format!("index '{name}' is disabled").into());
1380 }
1381 self.update_physical_metadata(|metadata| {
1382 let now = SystemTime::now()
1383 .duration_since(UNIX_EPOCH)
1384 .unwrap_or_default()
1385 .as_millis();
1386 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1387 index.build_state = "building".to_string();
1388 index.last_refresh_ms = Some(now);
1389 return Some(index.clone());
1390 }
1391 None
1392 })
1393 }
1394
1395 pub fn fail_index(
1397 &self,
1398 name: &str,
1399 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1400 let Some(status) = self.index_status(name) else {
1401 return Err(format!("index '{name}' is not present in catalog status").into());
1402 };
1403 if !status.declared {
1404 return Err(format!("index '{name}' is not declared in physical metadata").into());
1405 }
1406 self.update_physical_metadata(|metadata| {
1407 let now = SystemTime::now()
1408 .duration_since(UNIX_EPOCH)
1409 .unwrap_or_default()
1410 .as_millis();
1411 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1412 index.build_state = "failed".to_string();
1413 index.last_refresh_ms = Some(now);
1414 return Some(index.clone());
1415 }
1416 None
1417 })
1418 }
1419
1420 pub fn mark_index_stale(
1422 &self,
1423 name: &str,
1424 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1425 let Some(status) = self.index_status(name) else {
1426 return Err(format!("index '{name}' is not present in catalog status").into());
1427 };
1428 if !status.declared {
1429 return Err(format!("index '{name}' is not declared in physical metadata").into());
1430 }
1431 self.update_physical_metadata(|metadata| {
1432 let now = SystemTime::now()
1433 .duration_since(UNIX_EPOCH)
1434 .unwrap_or_default()
1435 .as_millis();
1436 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1437 index.build_state = if index.enabled {
1438 "stale".to_string()
1439 } else {
1440 "disabled".to_string()
1441 };
1442 index.last_refresh_ms = Some(now);
1443 return Some(index.clone());
1444 }
1445 None
1446 })
1447 }
1448
1449 pub fn mark_index_ready(
1451 &self,
1452 name: &str,
1453 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1454 self.warmup_index(name)
1455 }
1456
1457 pub fn warmup_index(
1459 &self,
1460 name: &str,
1461 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1462 let Some(status) = self.index_status(name) else {
1463 return Err(format!("index '{name}' is not present in catalog status").into());
1464 };
1465 if !status.declared {
1466 return Err(format!("index '{name}' is not declared in physical metadata").into());
1467 }
1468 if status.lifecycle_state == "disabled" {
1469 return Err(format!("index '{name}' is disabled").into());
1470 }
1471 if !status.operational {
1472 return Err(
1473 format!("index '{name}' is declared but not operationally materialized").into(),
1474 );
1475 }
1476 let warmed_artifact = self
1477 .physical_indexes()
1478 .into_iter()
1479 .find(|index| index.name == name)
1480 .map(|mut index| {
1481 self.warmup_native_vector_artifact_for_index(&index)?;
1482 self.apply_runtime_native_artifact_to_index_state(&mut index)?;
1483 Ok::<_, String>(index)
1484 })
1485 .transpose()
1486 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1487
1488 self.update_physical_metadata(|metadata| {
1489 let now = SystemTime::now()
1490 .duration_since(UNIX_EPOCH)
1491 .unwrap_or_default()
1492 .as_millis();
1493 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1494 if let Some(warmed) = warmed_artifact.as_ref() {
1495 index.entries = warmed.entries;
1496 index.estimated_memory_bytes = warmed.estimated_memory_bytes;
1497 index.backend = warmed.backend.clone();
1498 index.build_state = "ready".to_string();
1499 }
1500 index.last_refresh_ms = Some(now);
1501 return Some(index.clone());
1502 }
1503 None
1504 })
1505 }
1506
1507 pub fn rebuild_index_registry(
1509 &self,
1510 collection: Option<&str>,
1511 ) -> Result<Vec<PhysicalIndexState>, Box<dyn std::error::Error>> {
1512 let fresh = self.reconcile_index_states_with_native_artifacts(self.physical_index_state());
1513 self.update_physical_metadata(|metadata| {
1514 let now = SystemTime::now()
1515 .duration_since(UNIX_EPOCH)
1516 .unwrap_or_default()
1517 .as_millis();
1518
1519 let mut affected = Vec::new();
1520 let declared = metadata.indexes.clone();
1521 for declared_index in declared {
1522 let matches_collection = collection.is_none_or(|collection_name| {
1523 declared_index.collection.as_deref() == Some(collection_name)
1524 });
1525 if !matches_collection {
1526 continue;
1527 }
1528
1529 let mut rebuilt = fresh
1530 .iter()
1531 .find(|index| index.name == declared_index.name)
1532 .cloned()
1533 .unwrap_or_else(|| {
1534 let mut index = declared_index.clone();
1535 index.build_state = "declared-unbuilt".to_string();
1536 index
1537 });
1538 rebuilt.enabled = declared_index.enabled;
1539 rebuilt.artifact_kind = rebuilt
1540 .artifact_kind
1541 .or_else(|| declared_index.artifact_kind.clone());
1542 rebuilt.artifact_root_page = rebuilt
1543 .artifact_root_page
1544 .or(declared_index.artifact_root_page);
1545 rebuilt.artifact_checksum = rebuilt
1546 .artifact_checksum
1547 .or(declared_index.artifact_checksum);
1548 rebuilt.build_state =
1549 Self::finalize_rebuilt_index_build_state(&declared_index, &rebuilt);
1550 rebuilt.last_refresh_ms = Some(now);
1551
1552 if let Some(existing) = metadata
1553 .indexes
1554 .iter_mut()
1555 .find(|index| index.name == rebuilt.name)
1556 {
1557 *existing = rebuilt.clone();
1558 } else {
1559 metadata.indexes.push(rebuilt.clone());
1560 }
1561
1562 affected.push(rebuilt);
1563 }
1564
1565 affected
1566 })
1567 }
1568
1569 fn finalize_rebuilt_index_build_state(
1570 declared: &PhysicalIndexState,
1571 rebuilt: &PhysicalIndexState,
1572 ) -> String {
1573 if !rebuilt.enabled {
1574 return "disabled".to_string();
1575 }
1576
1577 if declared.build_state == "failed" || rebuilt.build_state == "failed" {
1578 return "failed".to_string();
1579 }
1580
1581 let native_artifact_family = Self::native_artifact_kind_for_index(rebuilt.kind).is_some();
1582 if native_artifact_family {
1583 if rebuilt.artifact_root_page.is_some() && rebuilt.artifact_checksum.is_some() {
1584 return "ready".to_string();
1585 }
1586 if declared.artifact_root_page.is_some()
1587 || declared.artifact_checksum.is_some()
1588 || declared.artifact_kind.is_some()
1589 {
1590 return "stale".to_string();
1591 }
1592 return "declared-unbuilt".to_string();
1593 }
1594
1595 if rebuilt.entries > 0 {
1596 return "ready".to_string();
1597 }
1598
1599 if matches!(
1600 declared.build_state.as_str(),
1601 "stale" | "artifact-published" | "registry-loaded"
1602 ) {
1603 return "stale".to_string();
1604 }
1605
1606 "declared-unbuilt".to_string()
1607 }
1608}