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