1use super::*;
2use crate::storage::unified::metadata::{MetadataFilter, MetadataValue};
3
4#[cfg(test)]
5fn metadata_qualification_phase(started: &std::time::Instant, name: &str) {
6 if std::env::var_os("REDDB_QUALIFICATION_CHECKPOINT_PHASES").is_some() {
7 eprintln!(
8 "runtime qualification phase=checkpoint_metadata name={name} elapsed_us={}",
9 started.elapsed().as_micros()
10 );
11 }
12}
13
14impl RedDB {
15 pub fn is_replica_role(&self) -> bool {
16 matches!(
17 self.options.replication.role,
18 crate::replication::ReplicationRole::Replica { .. }
19 )
20 }
21
22 pub fn enforce_retention_policy(&self) -> Result<(), Box<dyn std::error::Error>> {
23 if self.options.read_only || self.is_replica_role() {
24 return Ok(());
25 }
26
27 if self.options.mode == StorageMode::Persistent {
30 let Some(path) = self.path() else {
31 return Ok(());
32 };
33
34 let _publication = self.store.vault_publication_lock.lock();
35 let Ok(mut metadata) = self.load_or_bootstrap_physical_metadata(true) else {
36 return Ok(());
37 };
38
39 self.prune_export_registry(&mut metadata.exports);
40 metadata.save_for_data_path(path)?;
41 }
42
43 let _ = self.sweep_ttl_expired_entities()?;
44
45 Ok(())
46 }
47
48 pub(crate) fn ttl_expired_entities_now(
49 &self,
50 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
51 self.ttl_expired_entities_at(current_unix_ms())
52 }
53
54 pub fn replica_allows_entity_at_read(
55 &self,
56 collection: &str,
57 entity: &crate::storage::UnifiedEntity,
58 ) -> bool {
59 if !self.is_replica_role() {
60 return true;
61 }
62 !self.entity_expired_at(collection, entity, current_unix_ms())
63 }
64
65 fn sweep_ttl_expired_entities(&self) -> Result<usize, Box<dyn std::error::Error>> {
66 let to_delete = self.ttl_expired_entities_now()?;
67
68 let mut deleted = 0usize;
69 for (collection, id) in to_delete {
70 match self.store.delete(&collection, id) {
71 Ok(true) => deleted = deleted.saturating_add(1),
72 Ok(false) => {}
73 Err(err) => {
74 return Err(format!(
75 "failed deleting expired entity {id} from collection '{collection}': {err:?}"
76 )
77 .into());
78 }
79 }
80 }
81
82 Ok(deleted)
83 }
84
85 fn ttl_expired_entities_at(
86 &self,
87 now_ms: u64,
88 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
89 let mut to_delete = Vec::<(String, EntityId)>::new();
90
91 let mut absolute_expired = self.expired_entities_by_expires_at(now_ms)?;
92 to_delete.append(&mut absolute_expired);
93
94 let mut relative_expired = self.expired_entities_by_ttl(now_ms)?;
95 to_delete.append(&mut relative_expired);
96
97 to_delete.sort_unstable();
98 to_delete.dedup();
99
100 Ok(to_delete)
101 }
102
103 fn entity_expired_at(
104 &self,
105 collection: &str,
106 entity: &crate::storage::UnifiedEntity,
107 now_ms: u64,
108 ) -> bool {
109 let Some(metadata) = self.store.get_metadata(collection, entity.id) else {
110 return false;
111 };
112
113 Self::entity_metadata_expired_at(entity, &metadata, now_ms)
114 }
115
116 pub(crate) fn entity_metadata_expired_at(
117 entity: &crate::storage::UnifiedEntity,
118 metadata: &crate::storage::unified::metadata::Metadata,
119 now_ms: u64,
120 ) -> bool {
121 if metadata
122 .get("_expires_at")
123 .and_then(Self::metadata_u64)
124 .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
125 {
126 return true;
127 }
128
129 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
130 let ttl_secs = if ttl_ms.is_none() {
131 metadata.get("_ttl").and_then(|value| {
132 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
133 })
134 } else {
135 None
136 };
137
138 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
139 return false;
140 };
141 entity
142 .created_at
143 .saturating_mul(1000)
144 .saturating_add(ttl_ms)
145 <= now_ms
146 }
147
148 fn expired_entities_by_expires_at(
149 &self,
150 now_ms: u64,
151 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
152 let mut ids = self.store.filter_metadata_all(&[(
153 "_expires_at".to_string(),
154 MetadataFilter::Le(MetadataValue::Timestamp(now_ms)),
155 )]);
156
157 if let Ok(now_ms_i64) = i64::try_from(now_ms) {
158 ids.extend(self.store.filter_metadata_all(&[(
159 "_expires_at".to_string(),
160 MetadataFilter::Le(MetadataValue::Int(now_ms_i64)),
161 )]));
162 }
163
164 let now_ms_f64 = now_ms as f64;
165 if now_ms_f64.is_finite() {
166 ids.extend(self.store.filter_metadata_all(&[(
167 "_expires_at".to_string(),
168 MetadataFilter::Le(MetadataValue::Float(now_ms_f64)),
169 )]));
170 }
171
172 Ok(ids)
173 }
174
175 fn expired_entities_by_ttl(
176 &self,
177 now_ms: u64,
178 ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
179 let mut candidates = Vec::<(String, EntityId)>::new();
180
181 let ttl_ms_candidates = self
182 .store
183 .filter_metadata_all(&[("_ttl_ms".to_string(), MetadataFilter::IsNotNull)]);
184 candidates.extend(ttl_ms_candidates);
185
186 let ttl_candidates = self
187 .store
188 .filter_metadata_all(&[("_ttl".to_string(), MetadataFilter::IsNotNull)]);
189 candidates.extend(ttl_candidates);
190
191 if candidates.is_empty() {
192 return Ok(Vec::new());
193 }
194
195 candidates.sort_unstable();
196 candidates.dedup();
197
198 let mut expired = Vec::<(String, EntityId)>::new();
199 for (collection, entity_id) in candidates {
200 let Some(entity) = self.store.get(&collection, entity_id) else {
201 continue;
202 };
203
204 let Some(metadata) = self.store.get_metadata(&collection, entity_id) else {
205 continue;
206 };
207
208 let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
209 let ttl_secs = if ttl_ms.is_none() {
210 metadata.get("_ttl").and_then(|value| {
211 Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
212 })
213 } else {
214 None
215 };
216
217 let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
218 continue;
219 };
220
221 let created_at_ms = entity.created_at.saturating_mul(1000);
222 let expiry_ms = created_at_ms.saturating_add(ttl_ms);
223 if expiry_ms <= now_ms {
224 expired.push((collection, entity_id));
225 }
226 }
227
228 Ok(expired)
229 }
230
231 fn metadata_u64(value: &MetadataValue) -> Option<u64> {
232 match value {
233 MetadataValue::Int(v) if *v >= 0 => Some(*v as u64),
234 MetadataValue::Timestamp(v) => Some(*v),
235 MetadataValue::Float(v) => {
236 if !v.is_finite() || !v.is_sign_positive() || v.fract().abs() >= f64::EPSILON {
237 return None;
238 }
239 if *v > u64::MAX as f64 {
240 return None;
241 }
242 Some(v.trunc() as u64)
243 }
244 MetadataValue::String(v) => v.parse::<u64>().ok(),
245 _ => None,
246 }
247 }
248
249 pub fn node(&self, collection: impl Into<String>, label: impl Into<String>) -> NodeBuilder {
262 NodeBuilder::new(
263 self.store.clone(),
264 self.preprocessors.clone(),
265 collection,
266 label,
267 )
268 }
269
270 pub fn edge(&self, collection: impl Into<String>, label: impl Into<String>) -> EdgeBuilder {
282 EdgeBuilder::new(
283 self.store.clone(),
284 self.preprocessors.clone(),
285 collection,
286 label,
287 )
288 }
289
290 pub fn vector(&self, collection: impl Into<String>) -> VectorBuilder {
301 VectorBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
302 }
303
304 pub fn row(&self, table: impl Into<String>, columns: Vec<(&str, Value)>) -> RowBuilder {
315 RowBuilder::new(
316 self.store.clone(),
317 self.preprocessors.clone(),
318 table,
319 columns,
320 )
321 }
322
323 pub fn doc(&self, collection: impl Into<String>) -> DocumentBuilder {
337 DocumentBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
338 }
339
340 pub fn kv(
351 &self,
352 collection: impl Into<String>,
353 key: impl Into<String>,
354 value: Value,
355 ) -> KvBuilder {
356 KvBuilder::new(
357 self.store.clone(),
358 self.preprocessors.clone(),
359 collection,
360 key,
361 value,
362 )
363 }
364
365 pub fn get_kv(&self, collection: &str, key: &str) -> Option<(Value, EntityId)> {
370 let manager = self.store.get_collection(collection)?;
371 if let Some(value) = manager.project_key_entity(key, None, false, |entity| {
372 (
373 entity
374 .data
375 .as_row()
376 .and_then(|row| row.get_field("value"))
377 .cloned()
378 .unwrap_or(Value::Null),
379 entity.id,
380 )
381 }) {
382 return Some(value);
383 }
384 let columns = manager.column_schema();
385 let key_index = columns
386 .as_ref()
387 .and_then(|names| names.iter().position(|name| name == "key"));
388 let value_index = columns
389 .as_ref()
390 .and_then(|names| names.iter().position(|name| name == "value"));
391 let mut found = None;
392 manager.for_each_entity(|entity| {
393 let EntityData::Row(row) = &entity.data else {
394 return true;
395 };
396 let legacy = row.named.is_none() && row.schema.is_none();
397 let stored_key = row.get_field("key").or_else(|| {
398 if legacy {
399 key_index.and_then(|index| row.columns.get(index))
400 } else {
401 None
402 }
403 });
404 if matches!(stored_key, Some(Value::Text(stored)) if stored.as_ref() == key) {
405 let value = row.get_field("value").or_else(|| {
406 if legacy {
407 value_index.and_then(|index| row.columns.get(index))
408 } else {
409 None
410 }
411 });
412 found = Some((value.cloned().unwrap_or(Value::Null), entity.id));
413 return false;
414 }
415 true
416 });
417 found
418 }
419
420 pub fn delete_kv(
422 &self,
423 collection: &str,
424 key: &str,
425 ) -> Result<bool, super::super::error::DevXError> {
426 let Some((_, id)) = self.get_kv(collection, key) else {
427 return Ok(false);
428 };
429 self.store
430 .delete(collection, id)
431 .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
432 Ok(true)
433 }
434
435 pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
436 if self.options.mode == StorageMode::Persistent {
437 match self.load_or_bootstrap_physical_metadata(false) {
439 Ok(metadata) => {
440 crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
441 .map_err(|err| err.to_string())?;
442 }
443 Err(error)
444 if error
445 .downcast_ref::<std::io::Error>()
446 .is_some_and(crate::physical::is_collection_contract_decode_error) =>
447 {
448 return Err(error);
449 }
450 Err(_) => {}
451 }
452 }
456 self.load_collection_ttl_defaults_from_metadata();
457 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
464 && self.options.storage_profile.packaging
465 == crate::storage::StoragePackaging::SingleFile
466 && !self.paged_mode
467 {
468 self.seed_contract_cache_from_store_aux()?;
469 }
470 self.load_hypertables_from_metadata();
474 Ok(self)
478 }
479
480 pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
481 if self.options.mode != StorageMode::Persistent || self.options.read_only {
482 return Ok(());
483 }
484 if !self.paged_mode
487 && self.options.storage_profile.deploy_profile
488 == crate::storage::DeployProfile::Embedded
489 && self.options.storage_profile.packaging
490 == crate::storage::StoragePackaging::SingleFile
491 {
492 return Ok(());
493 }
494 let Some(path) = self.path() else {
495 return Ok(());
496 };
497
498 #[cfg(test)]
499 let qualification_started = std::time::Instant::now();
500 let _publication = self.store.vault_publication_lock.lock();
501 #[cfg(test)]
502 metadata_qualification_phase(&qualification_started, "publication-lock");
503 let previous = self.load_or_bootstrap_physical_metadata(false).ok();
504 #[cfg(test)]
505 metadata_qualification_phase(&qualification_started, "previous-metadata");
506 let collection_roots = self.physical_collection_roots();
507 #[cfg(test)]
508 metadata_qualification_phase(&qualification_started, "collection-roots");
509 let native_state = self.native_physical_state();
510 #[cfg(test)]
511 metadata_qualification_phase(&qualification_started, "native-state");
512 let mut indexes = native_state
513 .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
514 .unwrap_or_else(|| self.physical_index_state());
515 if let Some(previous) = previous.as_ref() {
516 let absent = |name: &str| {
520 !collection_roots.contains_key(name)
521 && !previous
522 .collection_contracts
523 .iter()
524 .any(|contract| contract.name == name)
525 };
526 let mut removed: BTreeSet<&str> = previous
527 .superblock
528 .collection_roots
529 .keys()
530 .map(String::as_str)
531 .filter(|name| absent(name))
532 .collect();
533 let mut seen = BTreeSet::new();
534 for event in previous.manifest_events.iter().rev() {
535 if seen.insert(event.collection.as_str())
536 && matches!(&event.kind, crate::physical::ManifestEventKind::Remove)
537 && absent(&event.collection)
538 {
539 removed.insert(event.collection.as_str());
540 }
541 }
542 indexes.retain(|index| {
543 !index
544 .collection
545 .as_deref()
546 .is_some_and(|collection| removed.contains(collection))
547 });
548 }
549 #[cfg(test)]
550 metadata_qualification_phase(&qualification_started, "index-state");
551 let mut metadata = PhysicalMetadataFile::from_state(
552 self.options.clone(),
553 self.catalog_snapshot(),
554 collection_roots,
555 indexes,
556 previous.as_ref(),
557 );
558 metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
559 metadata.hypertables = self.hypertable_registry_snapshot();
563 #[cfg(test)]
564 metadata_qualification_phase(&qualification_started, "metadata-assembled");
565 metadata.save_for_data_path(path)?;
566 #[cfg(test)]
567 metadata_qualification_phase(&qualification_started, "sidecar-save");
568 self.persist_native_physical_header(&metadata)?;
569 #[cfg(test)]
570 metadata_qualification_phase(&qualification_started, "native-header");
571 Ok(())
572 }
573
574 fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
575 if self.options.mode != StorageMode::Persistent || self.options.read_only {
576 return Ok(false);
577 }
578 let Some(path) = self.path() else {
579 return Ok(false);
580 };
581 let Some(native_state) = self.native_physical_state() else {
582 return Ok(false);
583 };
584 if !Self::native_state_is_bootstrap_complete(&native_state) {
585 return Ok(false);
586 }
587
588 let _publication = self.store.vault_publication_lock.lock();
589 let previous = PhysicalMetadataFile::load_for_data_path(path).ok();
590 let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
591 metadata.save_for_data_path(path)?;
592 self.persist_native_physical_header(&metadata)?;
593 Ok(true)
594 }
595
596 pub fn rebuild_physical_metadata_from_native_state(
599 &self,
600 ) -> Result<bool, Box<dyn std::error::Error>> {
601 self.bootstrap_metadata_from_native_state()
602 }
603
604 pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
605 let registry_complete = native_state.registry.as_ref().map(|registry| {
606 registry.collections_complete
607 && registry.indexes_complete
608 && registry.graph_projections_complete
609 && registry.analytics_jobs_complete
610 && registry.vector_artifacts_complete
611 });
612 let recovery_complete = native_state
613 .recovery
614 .as_ref()
615 .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
616 let catalog_complete = native_state
617 .catalog
618 .as_ref()
619 .map(|catalog| catalog.collections_complete);
620
621 registry_complete == Some(true)
622 && recovery_complete == Some(true)
623 && catalog_complete == Some(true)
624 }
625
626 fn native_state_is_summary_free_legacy(native_state: &NativePhysicalState) -> bool {
627 let header = native_state.header;
628 header.manifest_page == 0
629 && header.manifest_checksum == 0
630 && header.registry_page == 0
631 && header.registry_checksum == 0
632 && header.recovery_page == 0
633 && header.recovery_checksum == 0
634 && header.catalog_page == 0
635 && header.catalog_checksum == 0
636 && header.metadata_state_page == 0
637 && header.metadata_state_checksum == 0
638 && header.vector_artifact_page == 0
639 && header.vector_artifact_checksum == 0
640 && native_state.manifest.is_none()
641 && native_state.registry.is_none()
642 && native_state.recovery.is_none()
643 && native_state.catalog.is_none()
644 && native_state.metadata_state.is_none()
645 && native_state.vector_artifact_pages.is_none()
646 }
647
648 pub(crate) fn load_or_bootstrap_physical_metadata(
649 &self,
650 persist_bootstrapped: bool,
651 ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
652 if self.options.mode != StorageMode::Persistent {
653 return Err("physical metadata requires persistent mode".into());
654 }
655 let Some(path) = self.path() else {
656 return Err("database path is not available".into());
657 };
658 let _publication = self.store.vault_publication_lock.lock();
659 let native_state = self.native_physical_state();
660
661 let metadata = if persist_bootstrapped && !self.options.read_only {
662 PhysicalMetadataFile::load_for_data_path(path)
663 } else {
664 PhysicalMetadataFile::inspect_for_data_path(path)
665 };
666 match metadata {
667 Ok(metadata) => {
668 if let Some(native_state) = native_state.as_ref() {
669 let inspection = Self::inspect_native_header_against_metadata(
670 native_state.header,
671 &metadata,
672 );
673 if Self::repair_policy_for_inspection(&inspection)
674 == NativeHeaderRepairPolicy::NativeAheadOfMetadata
675 {
676 let bootstrapped =
677 self.metadata_from_native_state(native_state, Some(&metadata));
678 if persist_bootstrapped && !self.options.read_only {
679 bootstrapped.save_for_data_path(path)?;
680 self.persist_native_physical_header(&bootstrapped)?;
681 }
682 return Ok(bootstrapped);
683 }
684 }
685 Ok(metadata)
686 }
687 Err(err) => {
688 if crate::physical::is_collection_contract_decode_error(&err) {
689 return Err(err.into());
690 }
691 let Some(native_state) = native_state else {
692 return Err(err.into());
693 };
694 if !Self::native_state_is_summary_free_legacy(&native_state)
701 && !Self::native_state_is_bootstrap_complete(&native_state)
702 {
703 return Err(err.into());
704 }
705 let metadata = self.metadata_from_native_state(&native_state, None);
706 if persist_bootstrapped && !self.options.read_only {
707 metadata.save_for_data_path(path)?;
708 self.persist_native_physical_header(&metadata)?;
709 }
710 Ok(metadata)
711 }
712 }
713 }
714
715 pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
716 let path = self.path()?;
717 let native_state = self.native_physical_state();
718 let metadata = PhysicalMetadataFile::inspect_for_data_path(path).ok();
719
720 match (metadata, native_state) {
721 (Some(metadata), Some(native_state)) => {
722 let inspection =
723 Self::inspect_native_header_against_metadata(native_state.header, &metadata);
724 match Self::repair_policy_for_inspection(&inspection) {
725 NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
726 NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
727 NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
728 }
729 }
730 (Some(_), None) => Some("sidecar_only"),
731 (None, Some(_)) => Some("sidecar_missing_native_available"),
732 (None, None) => Some("sidecar_missing_no_native"),
733 }
734 }
735
736 fn metadata_from_native_state(
737 &self,
738 native_state: &NativePhysicalState,
739 previous: Option<&PhysicalMetadataFile>,
740 ) -> PhysicalMetadataFile {
741 let now = SystemTime::now()
742 .duration_since(UNIX_EPOCH)
743 .unwrap_or_default()
744 .as_millis();
745 let catalog = self.catalog_snapshot();
746 let catalog_name = catalog.name.clone();
747 let catalog_total_entities = catalog.total_entities;
748 let catalog_total_collections = catalog.total_collections;
749 let indexes = self.physical_index_state_from_native_state(native_state, previous);
750
751 let mut manifest =
752 crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
753 manifest.updated_at_unix_ms = now;
754
755 let manifest_events = native_state
756 .manifest
757 .as_ref()
758 .map(|summary| {
759 summary
760 .recent_events
761 .iter()
762 .map(|event| crate::physical::ManifestEvent {
763 collection: event.collection.clone(),
764 object_key: event.object_key.clone(),
765 kind: match event.kind.as_str() {
766 "insert" => crate::physical::ManifestEventKind::Insert,
767 "update" => crate::physical::ManifestEventKind::Update,
768 "remove" => crate::physical::ManifestEventKind::Remove,
769 _ => crate::physical::ManifestEventKind::Checkpoint,
770 },
771 block: crate::physical::BlockReference {
772 index: event.block_index,
773 checksum: event.block_checksum,
774 },
775 snapshot_min: event.snapshot_min,
776 snapshot_max: event.snapshot_max,
777 })
778 .collect()
779 })
780 .unwrap_or_default();
781
782 let graph_projections = native_state
783 .registry
784 .as_ref()
785 .and_then(|registry| {
786 registry.graph_projections_complete.then(|| {
787 registry
788 .graph_projections
789 .iter()
790 .map(|projection| crate::physical::PhysicalGraphProjection {
791 name: projection.name.clone(),
792 created_at_unix_ms: projection.created_at_unix_ms,
793 updated_at_unix_ms: projection.updated_at_unix_ms,
794 state: "materialized".to_string(),
795 source: projection.source.clone(),
796 node_labels: projection.node_labels.clone(),
797 node_types: projection.node_types.clone(),
798 edge_labels: projection.edge_labels.clone(),
799 last_materialized_sequence: projection.last_materialized_sequence,
800 })
801 .collect()
802 })
803 })
804 .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
805 .unwrap_or_default();
806
807 let analytics_jobs = native_state
808 .registry
809 .as_ref()
810 .and_then(|registry| {
811 registry.analytics_jobs_complete.then(|| {
812 registry
813 .analytics_jobs
814 .iter()
815 .map(|job| crate::physical::PhysicalAnalyticsJob {
816 id: job.id.clone(),
817 kind: job.kind.clone(),
818 state: job.state.clone(),
819 projection: job.projection.clone(),
820 created_at_unix_ms: job.created_at_unix_ms,
821 updated_at_unix_ms: job.updated_at_unix_ms,
822 last_run_sequence: job.last_run_sequence,
823 metadata: job.metadata.clone(),
824 })
825 .collect()
826 })
827 })
828 .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
829 .unwrap_or_default();
830
831 let exports = native_state
832 .recovery
833 .as_ref()
834 .and_then(|recovery| {
835 recovery.exports_complete.then(|| {
836 recovery
837 .exports
838 .iter()
839 .map(|export| crate::physical::ExportDescriptor {
840 name: export.name.clone(),
841 created_at_unix_ms: export.created_at_unix_ms,
842 snapshot_id: export.snapshot_id,
843 superblock_sequence: export.superblock_sequence,
844 data_path: self
845 .path()
846 .map(|path| {
847 crate::physical::PhysicalMetadataFile::export_data_path_for(
848 path,
849 &export.name,
850 )
851 .display()
852 .to_string()
853 })
854 .unwrap_or_default(),
855 metadata_path: self
856 .path()
857 .map(|path| {
858 let export_data_path =
859 crate::physical::PhysicalMetadataFile::export_data_path_for(
860 path,
861 &export.name,
862 );
863 crate::physical::PhysicalMetadataFile::metadata_path_for(
864 &export_data_path,
865 )
866 .display()
867 .to_string()
868 })
869 .unwrap_or_default(),
870 collection_count: export.collection_count as usize,
871 total_entities: export.total_entities as usize,
872 })
873 .collect()
874 })
875 })
876 .or_else(|| previous.map(|metadata| metadata.exports.clone()))
877 .unwrap_or_default();
878
879 let snapshots = native_state
880 .recovery
881 .as_ref()
882 .and_then(|recovery| {
883 recovery.snapshots_complete.then(|| {
884 recovery
885 .snapshots
886 .iter()
887 .map(|snapshot| crate::physical::SnapshotDescriptor {
888 snapshot_id: snapshot.snapshot_id,
889 created_at_unix_ms: snapshot.created_at_unix_ms,
890 superblock_sequence: snapshot.superblock_sequence,
891 collection_count: snapshot.collection_count as usize,
892 total_entities: snapshot.total_entities as usize,
893 })
894 .collect()
895 })
896 })
897 .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
898 .unwrap_or_else(|| {
899 vec![crate::physical::SnapshotDescriptor {
900 snapshot_id: native_state.header.sequence,
901 created_at_unix_ms: now,
902 superblock_sequence: native_state.header.sequence,
903 collection_count: catalog_total_collections,
904 total_entities: catalog_total_entities,
905 }]
906 });
907
908 let catalog_stats = native_state
909 .catalog
910 .as_ref()
911 .and_then(|native_catalog| {
912 native_catalog.collections_complete.then(|| {
913 native_catalog
914 .collections
915 .iter()
916 .map(|collection| {
917 (
918 collection.name.clone(),
919 crate::api::CollectionStats {
920 entities: collection.entities as usize,
921 cross_refs: collection.cross_refs as usize,
922 segments: collection.segments as usize,
923 },
924 )
925 })
926 .collect::<BTreeMap<_, _>>()
927 })
928 })
929 .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
930 .unwrap_or_else(|| catalog.stats_by_collection.clone());
931
932 PhysicalMetadataFile {
933 protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
934 generated_at_unix_ms: now,
935 last_loaded_from: Some("native_bootstrap".to_string()),
936 last_healed_at_unix_ms: Some(now),
937 manifest,
938 catalog: crate::api::CatalogSnapshot {
939 name: catalog_name,
940 total_entities: native_state
941 .catalog
942 .as_ref()
943 .map(|summary| summary.total_entities as usize)
944 .unwrap_or(catalog_total_entities),
945 total_collections: native_state
946 .catalog
947 .as_ref()
948 .map(|summary| summary.collection_count as usize)
949 .unwrap_or(catalog_total_collections),
950 stats_by_collection: catalog_stats,
951 updated_at: SystemTime::now(),
952 },
953 manifest_events,
954 collection_ttl_defaults_ms: previous
955 .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
956 .unwrap_or_default(),
957 collection_contracts: previous
958 .map(|metadata| metadata.collection_contracts.clone())
959 .unwrap_or_default(),
960 hypertables: previous
961 .map(|metadata| metadata.hypertables.clone())
962 .unwrap_or_default(),
963 tree_definitions: previous
964 .map(|metadata| metadata.tree_definitions.clone())
965 .unwrap_or_default(),
966 indexes,
967 graph_projections,
968 analytics_jobs,
969 exports,
970 superblock: crate::physical::SuperblockHeader {
971 format_version: native_state.header.format_version,
972 sequence: native_state.header.sequence,
973 copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
974 manifest: crate::physical::ManifestPointers {
975 oldest: crate::physical::BlockReference {
976 index: native_state.header.manifest_oldest_root,
977 checksum: 0,
978 },
979 newest: crate::physical::BlockReference {
980 index: native_state.header.manifest_root,
981 checksum: 0,
982 },
983 },
984 free_set: crate::physical::BlockReference {
985 index: native_state.header.free_set_root,
986 checksum: 0,
987 },
988 collection_roots: native_state.collection_roots.clone(),
989 },
990 snapshots,
991 }
992 }
993
994 pub(crate) fn reconcile_index_states_with_native_artifacts(
995 &self,
996 mut indexes: Vec<PhysicalIndexState>,
997 ) -> Vec<PhysicalIndexState> {
998 let native_state = self.native_physical_state();
999 let native_artifacts = native_state
1000 .as_ref()
1001 .and_then(|state| state.registry.as_ref())
1002 .map(|registry| registry.vector_artifacts.as_slice())
1003 .unwrap_or_default();
1004 let artifact_pages = native_state
1005 .as_ref()
1006 .and_then(|state| state.vector_artifact_pages.as_ref());
1007 for index in &mut indexes {
1008 let Some(collection) = index.collection.as_deref() else {
1009 continue;
1010 };
1011 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1012 continue;
1013 };
1014 let Some(artifact) = native_artifacts.iter().find(|artifact| {
1015 artifact.collection == collection && artifact.artifact_kind == artifact_kind
1016 }) else {
1017 index.build_state = if index.backend == "memory-budget-deferred" {
1018 "deferred-memory-budget"
1019 } else {
1020 "metadata-only"
1021 }
1022 .to_string();
1023 continue;
1024 };
1025 index.entries = artifact.vector_count as usize;
1026 index.estimated_memory_bytes = artifact.serialized_bytes;
1027 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1028 index.artifact_kind = Some(artifact.artifact_kind.clone());
1029 index.artifact_checksum = Some(artifact.checksum);
1030 index.build_state = "artifact-published".to_string();
1031 if let Some(pages) = artifact_pages {
1032 index.artifact_root_page = pages
1033 .iter()
1034 .find(|page| {
1035 page.collection == artifact.collection
1036 && page.artifact_kind == artifact.artifact_kind
1037 })
1038 .map(|page| page.root_page);
1039 }
1040 }
1041 indexes
1042 }
1043
1044 pub(crate) fn warmup_native_vector_artifact_for_index(
1045 &self,
1046 index: &PhysicalIndexState,
1047 ) -> Result<(), String> {
1048 let Some(collection) = index.collection.as_deref() else {
1049 return Ok(());
1050 };
1051 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1052 return Ok(());
1053 };
1054 self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
1055 Ok(())
1056 }
1057
1058 pub(crate) fn apply_runtime_native_artifact_to_index_state(
1059 &self,
1060 index: &mut PhysicalIndexState,
1061 ) -> Result<(), String> {
1062 let Some(collection) = index.collection.as_deref() else {
1063 return Ok(());
1064 };
1065 let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
1066 return Ok(());
1067 };
1068 let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
1069 index.entries = artifact
1070 .graph_edge_count
1071 .or(artifact.text_posting_count)
1072 .unwrap_or(artifact.node_count) as usize;
1073 index.estimated_memory_bytes = artifact.byte_len;
1074 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1075 index.artifact_kind = Some(artifact.artifact_kind.clone());
1076 index.artifact_checksum = Some(artifact.checksum);
1077 index.build_state = "ready".to_string();
1078 index.artifact_root_page = self
1079 .native_vector_artifact_pages()
1080 .and_then(|pages| {
1081 pages.into_iter().find(|page| {
1082 page.collection == artifact.collection
1083 && page.artifact_kind == artifact.artifact_kind
1084 })
1085 })
1086 .map(|page| page.root_page);
1087 Ok(())
1088 }
1089
1090 pub(crate) fn physical_index_state_from_native_state(
1091 &self,
1092 native_state: &NativePhysicalState,
1093 previous: Option<&PhysicalMetadataFile>,
1094 ) -> Vec<PhysicalIndexState> {
1095 let mut fresh = self.physical_index_state();
1096 let Some(registry) = native_state.registry.as_ref() else {
1097 if let Some(previous) = previous {
1098 for index in &previous.indexes {
1099 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1100 fresh.push(index.clone());
1101 }
1102 }
1103 }
1104 return fresh;
1105 };
1106
1107 for index in &mut fresh {
1108 if let Some(native) = registry
1109 .indexes
1110 .iter()
1111 .find(|candidate| candidate.name == index.name)
1112 {
1113 index.enabled = native.enabled;
1114 index.last_refresh_ms = native.last_refresh_ms;
1115 index.backend = native.backend.clone();
1116 index.entries = native.entries as usize;
1117 index.estimated_memory_bytes = native.estimated_memory_bytes;
1118 if index.artifact_kind.is_none() {
1119 index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
1120 .map(|value| value.to_string());
1121 }
1122 if index.build_state == "catalog-derived" {
1123 index.build_state = "registry-loaded".to_string();
1124 }
1125 }
1126 if index.backend == "memory-budget-deferred"
1127 || previous.is_some_and(|metadata| {
1128 metadata.indexes.iter().any(|previous| {
1129 previous.name == index.name && previous.backend == "memory-budget-deferred"
1130 })
1131 })
1132 {
1133 index.backend = "memory-budget-deferred".to_string();
1134 index.build_state = "deferred-memory-budget".to_string();
1135 index.artifact_root_page = None;
1136 index.artifact_checksum = None;
1137 }
1138 }
1139
1140 for native in ®istry.indexes {
1141 if fresh.iter().any(|index| index.name == native.name) {
1142 continue;
1143 }
1144 let Some(kind) = Self::index_kind_from_str(&native.kind) else {
1145 continue;
1146 };
1147 fresh.push(PhysicalIndexState {
1148 name: native.name.clone(),
1149 kind,
1150 collection: native.collection.clone(),
1151 enabled: native.enabled,
1152 entries: native.entries as usize,
1153 estimated_memory_bytes: native.estimated_memory_bytes,
1154 last_refresh_ms: native.last_refresh_ms,
1155 backend: native.backend.clone(),
1156 artifact_kind: Self::native_artifact_kind_for_index(kind)
1157 .map(|value| value.to_string()),
1158 artifact_root_page: None,
1159 artifact_checksum: None,
1160 build_state: "registry-loaded".to_string(),
1161 });
1162 }
1163
1164 if !registry.indexes_complete {
1165 if let Some(previous) = previous {
1166 for index in &previous.indexes {
1167 if !fresh.iter().any(|candidate| candidate.name == index.name) {
1168 fresh.push(index.clone());
1169 }
1170 }
1171 }
1172 }
1173
1174 fresh
1175 }
1176
1177 pub(crate) fn graph_projections_from_native_state(
1178 &self,
1179 native_state: &NativePhysicalState,
1180 ) -> Vec<PhysicalGraphProjection> {
1181 native_state
1182 .registry
1183 .as_ref()
1184 .map(|registry| {
1185 registry
1186 .graph_projections
1187 .iter()
1188 .map(|projection| PhysicalGraphProjection {
1189 name: projection.name.clone(),
1190 created_at_unix_ms: projection.created_at_unix_ms,
1191 updated_at_unix_ms: projection.updated_at_unix_ms,
1192 state: "materialized".to_string(),
1193 source: projection.source.clone(),
1194 node_labels: projection.node_labels.clone(),
1195 node_types: projection.node_types.clone(),
1196 edge_labels: projection.edge_labels.clone(),
1197 last_materialized_sequence: projection.last_materialized_sequence,
1198 })
1199 .collect()
1200 })
1201 .unwrap_or_default()
1202 }
1203
1204 pub(crate) fn analytics_jobs_from_native_state(
1205 &self,
1206 native_state: &NativePhysicalState,
1207 ) -> Vec<PhysicalAnalyticsJob> {
1208 native_state
1209 .registry
1210 .as_ref()
1211 .map(|registry| {
1212 registry
1213 .analytics_jobs
1214 .iter()
1215 .map(|job| PhysicalAnalyticsJob {
1216 id: job.id.clone(),
1217 kind: job.kind.clone(),
1218 state: job.state.clone(),
1219 projection: job.projection.clone(),
1220 created_at_unix_ms: job.created_at_unix_ms,
1221 updated_at_unix_ms: job.updated_at_unix_ms,
1222 last_run_sequence: job.last_run_sequence,
1223 metadata: job.metadata.clone(),
1224 })
1225 .collect()
1226 })
1227 .unwrap_or_default()
1228 }
1229
1230 pub(crate) fn exports_from_native_state(
1231 &self,
1232 native_state: &NativePhysicalState,
1233 ) -> Vec<ExportDescriptor> {
1234 native_state
1235 .recovery
1236 .as_ref()
1237 .map(|recovery| {
1238 recovery
1239 .exports
1240 .iter()
1241 .map(|export| ExportDescriptor {
1242 name: export.name.clone(),
1243 created_at_unix_ms: export.created_at_unix_ms,
1244 snapshot_id: export.snapshot_id,
1245 superblock_sequence: export.superblock_sequence,
1246 data_path: self
1247 .path()
1248 .map(|path| {
1249 crate::physical::PhysicalMetadataFile::export_data_path_for(
1250 path,
1251 &export.name,
1252 )
1253 .display()
1254 .to_string()
1255 })
1256 .unwrap_or_default(),
1257 metadata_path: self
1258 .path()
1259 .map(|path| {
1260 let export_data_path =
1261 crate::physical::PhysicalMetadataFile::export_data_path_for(
1262 path,
1263 &export.name,
1264 );
1265 crate::physical::PhysicalMetadataFile::metadata_path_for(
1266 &export_data_path,
1267 )
1268 .display()
1269 .to_string()
1270 })
1271 .unwrap_or_default(),
1272 collection_count: export.collection_count as usize,
1273 total_entities: export.total_entities as usize,
1274 })
1275 .collect()
1276 })
1277 .unwrap_or_default()
1278 }
1279
1280 pub(crate) fn snapshots_from_native_state(
1281 &self,
1282 native_state: &NativePhysicalState,
1283 ) -> Vec<crate::physical::SnapshotDescriptor> {
1284 let snapshots: Vec<_> = native_state
1285 .recovery
1286 .as_ref()
1287 .map(|recovery| {
1288 recovery
1289 .snapshots
1290 .iter()
1291 .map(|snapshot| crate::physical::SnapshotDescriptor {
1292 snapshot_id: snapshot.snapshot_id,
1293 created_at_unix_ms: snapshot.created_at_unix_ms,
1294 superblock_sequence: snapshot.superblock_sequence,
1295 collection_count: snapshot.collection_count as usize,
1296 total_entities: snapshot.total_entities as usize,
1297 })
1298 .collect()
1299 })
1300 .unwrap_or_default();
1301 if !snapshots.is_empty() {
1302 return snapshots;
1303 }
1304
1305 let now = SystemTime::now()
1306 .duration_since(UNIX_EPOCH)
1307 .unwrap_or_default()
1308 .as_millis();
1309 let (collection_count, total_entities) = native_state
1310 .catalog
1311 .as_ref()
1312 .map(|catalog| {
1313 (
1314 catalog.collection_count as usize,
1315 catalog.total_entities as usize,
1316 )
1317 })
1318 .unwrap_or_else(|| {
1319 let catalog = self.catalog_snapshot();
1320 (catalog.total_collections, catalog.total_entities)
1321 });
1322
1323 vec![crate::physical::SnapshotDescriptor {
1324 snapshot_id: native_state.header.sequence,
1325 created_at_unix_ms: now,
1326 superblock_sequence: native_state.header.sequence,
1327 collection_count,
1328 total_entities,
1329 }]
1330 }
1331
1332 fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1333 match value {
1334 "btree" => Some(crate::index::IndexKind::BTree),
1335 "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1336 "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1337 "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1338 "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1339 "text.fulltext" => Some(crate::index::IndexKind::FullText),
1340 "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1341 "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1342 _ => None,
1343 }
1344 }
1345
1346 pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1347 match kind {
1348 IndexKind::VectorHnsw => Some("hnsw"),
1349 IndexKind::VectorInverted => Some("ivf"),
1350 IndexKind::VectorTurbo => Some("turboquant"),
1351 IndexKind::GraphAdjacency => Some("graph.adjacency"),
1352 IndexKind::FullText => Some("text.fulltext"),
1353 IndexKind::DocumentPathValue => Some("document.pathvalue"),
1354 _ => None,
1355 }
1356 }
1357
1358 fn index_is_declared(&self, name: &str) -> bool {
1359 self.physical_metadata()
1360 .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1361 .unwrap_or(false)
1362 }
1363
1364 pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1365 self.physical_metadata()
1366 .map(|metadata| {
1367 metadata
1368 .graph_projections
1369 .iter()
1370 .any(|projection| projection.name == name)
1371 })
1372 .unwrap_or(false)
1373 }
1374
1375 pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1376 self.operational_graph_projections()
1377 .into_iter()
1378 .any(|projection| projection.name == name && projection.state == "materialized")
1379 }
1380
1381 pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1382 match projection {
1383 Some(projection) => format!("{kind}::{projection}"),
1384 None => format!("{kind}::global"),
1385 }
1386 }
1387
1388 pub(crate) fn update_physical_metadata<T, F>(
1389 &self,
1390 mutator: F,
1391 ) -> Result<T, Box<dyn std::error::Error>>
1392 where
1393 F: FnOnce(&mut PhysicalMetadataFile) -> T,
1394 {
1395 if self.options.mode != StorageMode::Persistent {
1396 return Err("physical metadata operations require persistent mode".into());
1397 }
1398 if self.options.read_only {
1399 return Err("physical metadata operations are not allowed in read-only mode".into());
1400 }
1401 let Some(path) = self.path() else {
1402 return Err("database path is not available".into());
1403 };
1404
1405 let _publication = self.store.vault_publication_lock.lock();
1412 let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1413
1414 if metadata.indexes.is_empty() {
1415 metadata.indexes = self
1416 .native_physical_state()
1417 .map(|state| self.physical_index_state_from_native_state(&state, Some(&metadata)))
1418 .unwrap_or_else(|| self.physical_index_state());
1419 }
1420 metadata.superblock.collection_roots = self.physical_collection_roots();
1421
1422 let result = mutator(&mut metadata);
1423 metadata.save_for_data_path(path)?;
1424 self.persist_native_physical_header(&metadata)?;
1425 Ok(result)
1426 }
1427
1428 pub(crate) fn persist_native_physical_header(
1429 &self,
1430 metadata: &PhysicalMetadataFile,
1431 ) -> Result<(), Box<dyn std::error::Error>> {
1432 if !self.paged_mode {
1433 return Ok(());
1434 }
1435
1436 let _publication = self.store.vault_publication_lock.lock();
1437 let existing_page = self
1438 .store
1439 .physical_file_header()
1440 .map(|header| header.collection_roots_page)
1441 .filter(|page| *page != 0);
1442 let existing_registry_page = self
1443 .store
1444 .physical_file_header()
1445 .map(|header| header.registry_page)
1446 .filter(|page| *page != 0);
1447 let existing_recovery_page = self
1448 .store
1449 .physical_file_header()
1450 .map(|header| header.recovery_page)
1451 .filter(|page| *page != 0);
1452 let existing_catalog_page = self
1453 .store
1454 .physical_file_header()
1455 .map(|header| header.catalog_page)
1456 .filter(|page| *page != 0);
1457 let existing_metadata_state_page = self
1458 .store
1459 .physical_file_header()
1460 .map(|header| header.metadata_state_page)
1461 .filter(|page| *page != 0);
1462 let existing_vector_artifact_page = self
1463 .store
1464 .physical_file_header()
1465 .map(|header| header.vector_artifact_page)
1466 .filter(|page| *page != 0);
1467 let existing_manifest_page = self
1468 .store
1469 .physical_file_header()
1470 .map(|header| header.manifest_page)
1471 .filter(|page| *page != 0);
1472 let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1473 metadata.superblock.sequence,
1474 &metadata.manifest_events,
1475 existing_manifest_page,
1476 )?;
1477 let (collection_roots_page, collection_roots_checksum) = self
1478 .store
1479 .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1480 let build = self.native_vector_artifact_records()?;
1483 let vector_artifact_records = build.records;
1484 let mut metadata = metadata.clone();
1485 for index in &mut metadata.indexes {
1486 let Some(kind) = Self::native_artifact_kind_for_index(index.kind) else {
1487 continue;
1488 };
1489 let Some(collection) = &index.collection else {
1490 continue;
1491 };
1492 if build.deferred_collections.contains(collection) {
1493 index.backend = "memory-budget-deferred".to_string();
1494 index.build_state = "deferred-memory-budget".to_string();
1495 index.artifact_kind = Some(kind.to_string());
1496 index.artifact_root_page = None;
1497 index.artifact_checksum = None;
1498 } else if let Some((summary, _)) =
1499 vector_artifact_records.iter().find(|(summary, _)| {
1500 &summary.collection == collection && summary.artifact_kind == kind
1501 })
1502 {
1503 index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
1504 index.build_state = "artifact-published".to_string();
1505 index.artifact_kind = Some(kind.to_string());
1506 index.artifact_checksum = Some(summary.checksum);
1507 }
1508 }
1509 let metadata = &metadata;
1510 let artifact_summaries = vector_artifact_records
1511 .iter()
1512 .map(|(summary, _)| summary.clone())
1513 .collect::<Vec<_>>();
1514 let registry_summary = self.native_registry_summary_from_metadata(
1515 metadata,
1516 &artifact_summaries,
1517 artifact_summaries.len(),
1518 );
1519 let (registry_page, registry_checksum) = self
1520 .store
1521 .write_native_registry_summary(®istry_summary, existing_registry_page)?;
1522 let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1523 let (recovery_page, recovery_checksum) = self
1524 .store
1525 .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1526 let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1527 let (catalog_page, catalog_checksum) = self
1528 .store
1529 .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1530 let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1531 let (metadata_state_page, metadata_state_checksum) =
1532 self.store.write_native_metadata_state_summary(
1533 &metadata_state_summary,
1534 existing_metadata_state_page,
1535 )?;
1536 let vector_artifact_payloads = vector_artifact_records
1537 .iter()
1538 .map(|(summary, bytes)| {
1539 (
1540 summary.collection.clone(),
1541 summary.artifact_kind.clone(),
1542 bytes.clone(),
1543 )
1544 })
1545 .collect::<Vec<_>>();
1546 let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1547 self.store.write_native_vector_artifact_store(
1548 &vector_artifact_payloads,
1549 existing_vector_artifact_page,
1550 )?;
1551 let mut header = Self::native_header_from_metadata(metadata);
1552 header.manifest_page = manifest_page;
1553 header.manifest_checksum = manifest_checksum;
1554 header.collection_roots_page = collection_roots_page;
1555 header.collection_roots_checksum = collection_roots_checksum;
1556 header.registry_page = registry_page;
1557 header.registry_checksum = registry_checksum;
1558 header.recovery_page = recovery_page;
1559 header.recovery_checksum = recovery_checksum;
1560 header.catalog_page = catalog_page;
1561 header.catalog_checksum = catalog_checksum;
1562 header.metadata_state_page = metadata_state_page;
1563 header.metadata_state_checksum = metadata_state_checksum;
1564 header.vector_artifact_page = vector_artifact_page;
1565 header.vector_artifact_checksum = vector_artifact_checksum;
1566 if let Some(path) = self.path() {
1567 metadata.save_for_data_path(path)?;
1568 }
1569 self.store.update_physical_file_header(header)?;
1570 self.store.persist()?;
1571 self.store.publish_detached_native_blob_pages()?;
1572 Ok(())
1573 }
1574
1575 pub(crate) fn native_header_from_metadata(
1576 metadata: &PhysicalMetadataFile,
1577 ) -> PhysicalFileHeader {
1578 PhysicalFileHeader {
1579 format_version: metadata.superblock.format_version,
1580 sequence: metadata.superblock.sequence,
1581 manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1582 manifest_root: metadata.superblock.manifest.newest.index,
1583 free_set_root: metadata.superblock.free_set.index,
1584 manifest_page: 0,
1585 manifest_checksum: 0,
1586 collection_roots_page: 0,
1587 collection_roots_checksum: 0,
1588 collection_root_count: metadata.superblock.collection_roots.len() as u32,
1589 snapshot_count: metadata.snapshots.len() as u32,
1590 index_count: metadata.indexes.len() as u32,
1591 catalog_collection_count: metadata.catalog.total_collections as u32,
1592 catalog_total_entities: metadata.catalog.total_entities as u64,
1593 export_count: metadata.exports.len() as u32,
1594 graph_projection_count: metadata.graph_projections.len() as u32,
1595 analytics_job_count: metadata.analytics_jobs.len() as u32,
1596 manifest_event_count: metadata.manifest_events.len() as u32,
1597 registry_page: 0,
1598 registry_checksum: 0,
1599 recovery_page: 0,
1600 recovery_checksum: 0,
1601 catalog_page: 0,
1602 catalog_checksum: 0,
1603 metadata_state_page: 0,
1604 metadata_state_checksum: 0,
1605 vector_artifact_page: 0,
1606 vector_artifact_checksum: 0,
1607 }
1608 }
1609}
1610
1611fn current_unix_ms() -> u64 {
1612 SystemTime::now()
1613 .duration_since(UNIX_EPOCH)
1614 .unwrap_or_default()
1615 .as_millis()
1616 .min(u128::from(u64::MAX)) as u64
1617}
1618
1619#[cfg(test)]
1620mod catalog_observation_tests {
1621 use super::*;
1622
1623 #[test]
1624 fn summary_free_legacy_native_state_rebuilds_a_missing_sidecar_after_catalog_publication() {
1625 let directory = tempfile::tempdir().expect("temporary database");
1626 let path = directory.path().join("legacy-summary-free.rdb");
1627 let options = RedDBOptions::persistent(&path)
1628 .with_memory_budget(64 * 1024 * 1024)
1629 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1630 .expect("paged profile");
1631 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1632 runtime
1633 .execute_query("CREATE TABLE retained (value INTEGER)")
1634 .expect("retained table");
1635 runtime
1636 .execute_query("INSERT INTO retained (value) VALUES (17)")
1637 .expect("retained row");
1638 runtime.checkpoint().expect("publish retained row");
1639
1640 let db = runtime.db();
1641 let store = db.store();
1642 let mut header = store.physical_file_header().expect("physical header");
1643 assert_ne!(
1644 header.collection_roots_page, 0,
1645 "fixture retains native roots"
1646 );
1647 header.sequence = header
1648 .sequence
1649 .checked_add(1)
1650 .expect("next catalog sequence");
1651 header.manifest_page = 0;
1652 header.manifest_checksum = 0;
1653 header.registry_page = 0;
1654 header.registry_checksum = 0;
1655 header.recovery_page = 0;
1656 header.recovery_checksum = 0;
1657 header.catalog_page = 0;
1658 header.catalog_checksum = 0;
1659 header.metadata_state_page = 0;
1660 header.metadata_state_checksum = 0;
1661 header.vector_artifact_page = 0;
1662 header.vector_artifact_checksum = 0;
1663 store
1664 .update_physical_file_header(header)
1665 .expect("publish legacy catalog sequence");
1666
1667 let legacy = db.native_physical_state().expect("legacy native state");
1668 assert!(RedDB::native_state_is_summary_free_legacy(&legacy));
1669 let mut unreadable_publication = legacy.clone();
1670 unreadable_publication.header.registry_page = u32::MAX;
1671 assert!(
1672 !RedDB::native_state_is_summary_free_legacy(&unreadable_publication),
1673 "a non-zero unreadable summary pointer must still fail closed"
1674 );
1675
1676 let binary = PhysicalMetadataFile::metadata_binary_path_for(&path);
1677 let json = PhysicalMetadataFile::metadata_path_for(&path);
1678 std::fs::remove_file(binary).expect("remove binary sidecar");
1679 if json.exists() {
1680 std::fs::remove_file(json).expect("remove JSON sidecar");
1681 }
1682 for journal in PhysicalMetadataFile::journal_paths_for_data_path(&path)
1683 .expect("list metadata journals")
1684 {
1685 std::fs::remove_file(journal).expect("remove metadata journal");
1686 }
1687
1688 let metadata = db
1689 .load_or_bootstrap_physical_metadata(true)
1690 .expect("summary-free legacy state must rebuild its missing sidecar");
1691 assert_eq!(
1692 metadata.superblock.collection_roots.get("retained"),
1693 db.physical_collection_roots().get("retained")
1694 );
1695 assert!(PhysicalMetadataFile::metadata_binary_path_for(&path).exists());
1696 let rows = runtime
1697 .execute_query("SELECT value FROM retained")
1698 .expect("retained row remains queryable");
1699 assert_eq!(rows.result.records.len(), 1);
1700 }
1701
1702 #[test]
1703 fn legacy_paged_drop_does_not_resurrect_derived_index_descriptors() {
1704 let directory = tempfile::tempdir().expect("temporary database");
1705 let path = directory.path().join("drop-descriptors.rdb");
1706 let options = RedDBOptions::persistent(&path)
1707 .with_memory_budget(64 * 1024 * 1024)
1708 .with_storage_profile(crate::StorageDeployPreset::PrimaryReplicaDev.selection())
1709 .expect("paged profile");
1710 {
1711 let runtime = crate::RedDBRuntime::with_options(options).expect("paged runtime");
1712 runtime
1713 .execute_query("CREATE TABLE retained_source (value INTEGER)")
1714 .expect("retained table");
1715 runtime
1716 .execute_query("INSERT INTO retained_source (value) VALUES (17)")
1717 .expect("retained row");
1718 runtime.checkpoint().expect("initial checkpoint");
1719 }
1720 let options = RedDBOptions::persistent(&path).with_memory_budget(64 * 1024 * 1024);
1723 {
1724 let runtime =
1725 crate::RedDBRuntime::with_options(options.clone()).expect("legacy reopen");
1726 assert!(runtime.db().is_paged());
1727 assert_eq!(
1728 runtime.db().options().storage_profile.deploy_profile,
1729 crate::storage::DeployProfile::Embedded
1730 );
1731 for cycle in 0..6 {
1733 let table = format!("discarded_source_{}", cycle % 4);
1734 runtime
1735 .execute_query(&format!("CREATE TABLE {table} (value INTEGER)"))
1736 .expect("temporary table");
1737 runtime.checkpoint().expect("publish derived descriptors");
1738 let before = runtime.db().physical_metadata().expect("physical metadata");
1739 assert!(before
1740 .indexes
1741 .iter()
1742 .any(|index| index.collection.as_deref() == Some(table.as_str())));
1743 runtime
1744 .execute_query(&format!("DROP TABLE {table}"))
1745 .expect("drop temporary table");
1746 for _ in 0..2 {
1747 runtime.checkpoint().expect("checkpoint after drop");
1748 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1749 assert!(
1750 !metadata.indexes.iter().any(|index| index
1751 .collection
1752 .as_deref()
1753 .is_some_and(|name| name.starts_with("discarded_source_"))),
1754 "a dropped collection's derived descriptors were resurrected"
1755 );
1756 let native = runtime
1757 .db()
1758 .native_registry_summary()
1759 .expect("native registry");
1760 assert!(!native.indexes.iter().any(|index| {
1761 index
1762 .collection
1763 .as_deref()
1764 .is_some_and(|name| name.starts_with("discarded_source_"))
1765 }));
1766 }
1767 if cycle == 0 {
1768 let ghost = before
1771 .indexes
1772 .iter()
1773 .find(|index| index.collection.as_deref() == Some(table.as_str()))
1774 .expect("old derived descriptor")
1775 .clone();
1776 runtime
1777 .db()
1778 .update_physical_metadata(|metadata| {
1779 assert!(metadata.manifest_events.iter().any(|event| {
1780 event.collection == table
1781 && matches!(
1782 &event.kind,
1783 crate::physical::ManifestEventKind::Remove
1784 )
1785 }));
1786 metadata.indexes.push(ghost.clone());
1787 let mut orphan = ghost.clone();
1788 orphan.name = "audited_legacy_orphan".into();
1789 orphan.collection = Some("audited_missing_source".into());
1790 orphan.artifact_root_page = None;
1791 orphan.artifact_checksum = None;
1792 metadata.indexes.push(orphan.clone());
1793 orphan.name = "global_descriptor".into();
1794 orphan.collection = None;
1795 metadata.indexes.push(orphan);
1796 })
1797 .expect("simulate old metadata without deleting any rows");
1798 runtime
1799 .checkpoint()
1800 .expect("repair a confirmed old removal");
1801 }
1802 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1803 assert!(metadata
1804 .indexes
1805 .iter()
1806 .any(|index| index.name == "audited_legacy_orphan"));
1807 assert!(metadata
1808 .indexes
1809 .iter()
1810 .any(|index| index.name == "global_descriptor"));
1811 }
1812 }
1813 let runtime = crate::RedDBRuntime::with_options(options).expect("second legacy reopen");
1814 assert_eq!(
1815 runtime
1816 .db()
1817 .store()
1818 .get_collection("retained_source")
1819 .expect("retained source")
1820 .count(),
1821 1
1822 );
1823 let metadata = runtime.db().physical_metadata().expect("physical metadata");
1824 assert!(!metadata.indexes.iter().any(|index| {
1825 index
1826 .collection
1827 .as_deref()
1828 .is_some_and(|name| name.starts_with("discarded_source_"))
1829 }));
1830 assert!(metadata
1831 .indexes
1832 .iter()
1833 .any(|index| index.collection.as_deref() == Some("retained_source")));
1834 assert!(metadata
1835 .indexes
1836 .iter()
1837 .any(|index| index.name == "audited_legacy_orphan"));
1838 assert!(metadata
1839 .indexes
1840 .iter()
1841 .any(|index| index.name == "global_descriptor"));
1842 }
1843
1844 #[test]
1845 fn index_reconciliation_reads_one_native_observation() {
1846 let directory = tempfile::tempdir().expect("temporary database");
1847 let runtime = crate::RedDBRuntime::with_options(
1848 RedDBOptions::persistent(directory.path().join("catalog.rdb"))
1849 .with_storage_profile(crate::StorageDeployPreset::Serverless.selection())
1850 .expect("paged profile")
1851 .with_capability(Capability::FullText),
1852 )
1853 .expect("runtime");
1854 for number in 0..4 {
1855 runtime
1856 .execute_query(&format!("CREATE TABLE source_{number} (payload TEXT)"))
1857 .expect("table");
1858 runtime
1859 .execute_query(&format!(
1860 "INSERT INTO source_{number} (payload) VALUES ('searchable payload')"
1861 ))
1862 .expect("row");
1863 }
1864 runtime.checkpoint().expect("published artifacts");
1865 let db = runtime.db();
1866 let pager = db.store.pager().expect("paged store");
1867 let reads = || {
1868 let stats = pager.cache_stats();
1869 stats.hits + stats.misses
1870 };
1871 let before = reads();
1872 let native = db.native_physical_state().expect("native state");
1873 let observation_reads = reads() - before;
1874 let artifacts = native.registry.expect("registry").vector_artifacts;
1875 let pages = native.vector_artifact_pages.expect("artifact pages");
1876 let indexes: Vec<_> = artifacts
1877 .iter()
1878 .filter(|artifact| artifact.artifact_kind == "text.fulltext")
1879 .map(|artifact| PhysicalIndexState {
1880 name: format!("{}::fulltext", artifact.collection),
1881 kind: IndexKind::FullText,
1882 collection: Some(artifact.collection.clone()),
1883 enabled: true,
1884 entries: 0,
1885 estimated_memory_bytes: 0,
1886 last_refresh_ms: None,
1887 backend: "catalog-derived".into(),
1888 artifact_kind: None,
1889 artifact_root_page: None,
1890 artifact_checksum: None,
1891 build_state: "catalog-derived".into(),
1892 })
1893 .collect();
1894 assert!(indexes.len() >= 4, "fixture must publish all four indexes");
1895 let declarations = indexes.clone();
1896 let before = reads();
1897 let indexes = db.reconcile_index_states_with_native_artifacts(indexes);
1898 let reconciliation_reads = reads() - before;
1899 for index in indexes {
1900 let artifact = artifacts
1901 .iter()
1902 .find(|artifact| {
1903 Some(&artifact.collection) == index.collection.as_ref()
1904 && artifact.artifact_kind == "text.fulltext"
1905 })
1906 .expect("published artifact");
1907 let page = pages
1908 .iter()
1909 .find(|page| {
1910 page.collection == artifact.collection
1911 && page.artifact_kind == artifact.artifact_kind
1912 })
1913 .expect("artifact root");
1914 assert_eq!(index.artifact_root_page, Some(page.root_page));
1915 assert_eq!(index.artifact_checksum, Some(artifact.checksum));
1916 assert_eq!(index.entries, artifact.vector_count as usize);
1917 assert_eq!(index.build_state, "artifact-published");
1918 }
1919 assert_eq!(
1920 reconciliation_reads, observation_reads,
1921 "catalog reconciliation must not reread artifact pages per index"
1922 );
1923
1924 let page_id = native.header.vector_artifact_page;
1927 let mut page = pager.read_page(page_id).expect("artifact directory");
1928 page.as_bytes_mut()[crate::storage::engine::HEADER_SIZE..].fill(0);
1929 pager
1930 .write_page(page_id, page)
1931 .expect("corrupt artifact directory");
1932 assert!(db.native_vector_artifact_pages().is_none());
1933 let indexes = db.reconcile_index_states_with_native_artifacts(declarations);
1934 for index in indexes {
1935 assert_eq!(index.artifact_root_page, None);
1936 assert!(
1937 index.artifact_checksum.is_some(),
1938 "registry remains available"
1939 );
1940 assert_eq!(index.build_state, "artifact-published");
1941 }
1942 }
1943}