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