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