Skip to main content

reddb_server/storage/unified/devx/reddb/
impl_metadata.rs

1use super::*;
2use crate::storage::unified::metadata::{MetadataFilter, MetadataValue};
3
4impl RedDB {
5    pub fn is_replica_role(&self) -> bool {
6        matches!(
7            self.options.replication.role,
8            crate::replication::ReplicationRole::Replica { .. }
9        )
10    }
11
12    pub fn enforce_retention_policy(&self) -> Result<(), Box<dyn std::error::Error>> {
13        if self.options.read_only || self.is_replica_role() {
14            return Ok(());
15        }
16
17        // Export pruning is only meaningful for persistent mode where we
18        // have a metadata sidecar that tracks file-backed export artifacts.
19        if self.options.mode == StorageMode::Persistent {
20            let Some(path) = self.path() else {
21                return Ok(());
22            };
23
24            let Ok(mut metadata) = self.load_or_bootstrap_physical_metadata(true) else {
25                return Ok(());
26            };
27
28            self.prune_export_registry(&mut metadata.exports);
29            metadata.save_for_data_path(path)?;
30        }
31
32        let _ = self.sweep_ttl_expired_entities()?;
33
34        Ok(())
35    }
36
37    pub(crate) fn ttl_expired_entities_now(
38        &self,
39    ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
40        self.ttl_expired_entities_at(current_unix_ms())
41    }
42
43    pub fn replica_allows_entity_at_read(
44        &self,
45        collection: &str,
46        entity: &crate::storage::UnifiedEntity,
47    ) -> bool {
48        if !self.is_replica_role() {
49            return true;
50        }
51        !self.entity_expired_at(collection, entity, current_unix_ms())
52    }
53
54    fn sweep_ttl_expired_entities(&self) -> Result<usize, Box<dyn std::error::Error>> {
55        let to_delete = self.ttl_expired_entities_now()?;
56
57        let mut deleted = 0usize;
58        for (collection, id) in to_delete {
59            match self.store.delete(&collection, id) {
60                Ok(true) => deleted = deleted.saturating_add(1),
61                Ok(false) => {}
62                Err(err) => {
63                    return Err(format!(
64                        "failed deleting expired entity {id} from collection '{collection}': {err:?}"
65                    )
66                    .into());
67                }
68            }
69        }
70
71        Ok(deleted)
72    }
73
74    fn ttl_expired_entities_at(
75        &self,
76        now_ms: u64,
77    ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
78        let mut to_delete = Vec::<(String, EntityId)>::new();
79
80        let mut absolute_expired = self.expired_entities_by_expires_at(now_ms)?;
81        to_delete.append(&mut absolute_expired);
82
83        let mut relative_expired = self.expired_entities_by_ttl(now_ms)?;
84        to_delete.append(&mut relative_expired);
85
86        to_delete.sort_unstable();
87        to_delete.dedup();
88
89        Ok(to_delete)
90    }
91
92    fn entity_expired_at(
93        &self,
94        collection: &str,
95        entity: &crate::storage::UnifiedEntity,
96        now_ms: u64,
97    ) -> bool {
98        let Some(metadata) = self.store.get_metadata(collection, entity.id) else {
99            return false;
100        };
101
102        if metadata
103            .get("_expires_at")
104            .and_then(Self::metadata_u64)
105            .is_some_and(|expires_at_ms| expires_at_ms <= now_ms)
106        {
107            return true;
108        }
109
110        let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
111        let ttl_secs = if ttl_ms.is_none() {
112            metadata.get("_ttl").and_then(|value| {
113                Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
114            })
115        } else {
116            None
117        };
118
119        let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
120            return false;
121        };
122        entity
123            .created_at
124            .saturating_mul(1000)
125            .saturating_add(ttl_ms)
126            <= now_ms
127    }
128
129    fn expired_entities_by_expires_at(
130        &self,
131        now_ms: u64,
132    ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
133        let mut ids = self.store.filter_metadata_all(&[(
134            "_expires_at".to_string(),
135            MetadataFilter::Le(MetadataValue::Timestamp(now_ms)),
136        )]);
137
138        if let Ok(now_ms_i64) = i64::try_from(now_ms) {
139            ids.extend(self.store.filter_metadata_all(&[(
140                "_expires_at".to_string(),
141                MetadataFilter::Le(MetadataValue::Int(now_ms_i64)),
142            )]));
143        }
144
145        let now_ms_f64 = now_ms as f64;
146        if now_ms_f64.is_finite() {
147            ids.extend(self.store.filter_metadata_all(&[(
148                "_expires_at".to_string(),
149                MetadataFilter::Le(MetadataValue::Float(now_ms_f64)),
150            )]));
151        }
152
153        Ok(ids)
154    }
155
156    fn expired_entities_by_ttl(
157        &self,
158        now_ms: u64,
159    ) -> Result<Vec<(String, EntityId)>, Box<dyn std::error::Error>> {
160        let mut candidates = Vec::<(String, EntityId)>::new();
161
162        let ttl_ms_candidates = self
163            .store
164            .filter_metadata_all(&[("_ttl_ms".to_string(), MetadataFilter::IsNotNull)]);
165        candidates.extend(ttl_ms_candidates);
166
167        let ttl_candidates = self
168            .store
169            .filter_metadata_all(&[("_ttl".to_string(), MetadataFilter::IsNotNull)]);
170        candidates.extend(ttl_candidates);
171
172        if candidates.is_empty() {
173            return Ok(Vec::new());
174        }
175
176        candidates.sort_unstable();
177        candidates.dedup();
178
179        let mut expired = Vec::<(String, EntityId)>::new();
180        for (collection, entity_id) in candidates {
181            let Some(entity) = self.store.get(&collection, entity_id) else {
182                continue;
183            };
184
185            let Some(metadata) = self.store.get_metadata(&collection, entity_id) else {
186                continue;
187            };
188
189            let ttl_ms = metadata.get("_ttl_ms").and_then(Self::metadata_u64);
190            let ttl_secs = if ttl_ms.is_none() {
191                metadata.get("_ttl").and_then(|value| {
192                    Self::metadata_u64(value).and_then(|value_secs| value_secs.checked_mul(1000))
193                })
194            } else {
195                None
196            };
197
198            let Some(ttl_ms) = ttl_ms.or(ttl_secs) else {
199                continue;
200            };
201
202            let created_at_ms = entity.created_at.saturating_mul(1000);
203            let expiry_ms = created_at_ms.saturating_add(ttl_ms);
204            if expiry_ms <= now_ms {
205                expired.push((collection, entity_id));
206            }
207        }
208
209        Ok(expired)
210    }
211
212    fn metadata_u64(value: &MetadataValue) -> Option<u64> {
213        match value {
214            MetadataValue::Int(v) if *v >= 0 => Some(*v as u64),
215            MetadataValue::Timestamp(v) => Some(*v),
216            MetadataValue::Float(v) => {
217                if !v.is_finite() || !v.is_sign_positive() || v.fract().abs() >= f64::EPSILON {
218                    return None;
219                }
220                if *v > u64::MAX as f64 {
221                    return None;
222                }
223                Some(v.trunc() as u64)
224            }
225            MetadataValue::String(v) => v.parse::<u64>().ok(),
226            _ => None,
227        }
228    }
229
230    // ========================================================================
231    // Builder Methods - Create Entities
232    // ========================================================================
233
234    /// Start building a graph node
235    ///
236    /// # Example
237    /// ```ignore
238    /// let host = db.node("hosts", "Host")
239    ///     .property("ip", "192.168.1.1")
240    ///     .save()?;
241    /// ```
242    pub fn node(&self, collection: impl Into<String>, label: impl Into<String>) -> NodeBuilder {
243        NodeBuilder::new(
244            self.store.clone(),
245            self.preprocessors.clone(),
246            collection,
247            label,
248        )
249    }
250
251    /// Start building a graph edge
252    ///
253    /// # Example
254    /// ```ignore
255    /// let edge = db.edge("connections", "CONNECTS_TO")
256    ///     .from(host_a)
257    ///     .to(host_b)
258    ///     .weight(0.95)
259    ///     .property("protocol", "TCP")
260    ///     .save()?;
261    /// ```
262    pub fn edge(&self, collection: impl Into<String>, label: impl Into<String>) -> EdgeBuilder {
263        EdgeBuilder::new(
264            self.store.clone(),
265            self.preprocessors.clone(),
266            collection,
267            label,
268        )
269    }
270
271    /// Start building a vector entry
272    ///
273    /// # Example
274    /// ```ignore
275    /// let vec = db.vector("embeddings")
276    ///     .dense(embedding)
277    ///     .content("Original text content")
278    ///     .metadata("source", "document.pdf")
279    ///     .save()?;
280    /// ```
281    pub fn vector(&self, collection: impl Into<String>) -> VectorBuilder {
282        VectorBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
283    }
284
285    /// Start building a table row
286    ///
287    /// # Example
288    /// ```ignore
289    /// let row = db.row("scans", vec![
290    ///     ("timestamp", Value::Timestamp(now)),
291    ///     ("target", Value::text("192.168.1.0/24")),
292    ///     ("findings", Value::Integer(42)),
293    /// ]).save()?;
294    /// ```
295    pub fn row(&self, table: impl Into<String>, columns: Vec<(&str, Value)>) -> RowBuilder {
296        RowBuilder::new(
297            self.store.clone(),
298            self.preprocessors.clone(),
299            table,
300            columns,
301        )
302    }
303
304    /// Start building a document
305    ///
306    /// Documents are stored as enriched table rows with a full JSON body
307    /// field and flattened top-level keys for filtering.
308    ///
309    /// # Example
310    /// ```ignore
311    /// let doc = db.doc("articles")
312    ///     .field("title", "Hello World")
313    ///     .field("views", 42)
314    ///     .metadata("source", "web")
315    ///     .save()?;
316    /// ```
317    pub fn doc(&self, collection: impl Into<String>) -> DocumentBuilder {
318        DocumentBuilder::new(self.store.clone(), self.preprocessors.clone(), collection)
319    }
320
321    /// Start building a key-value pair
322    ///
323    /// KV pairs are stored as table rows with named fields `key` and `value`.
324    ///
325    /// # Example
326    /// ```ignore
327    /// let id = db.kv("config", "theme", Value::text("dark"))
328    ///     .metadata("updated_by", "admin")
329    ///     .save()?;
330    /// ```
331    pub fn kv(
332        &self,
333        collection: impl Into<String>,
334        key: impl Into<String>,
335        value: Value,
336    ) -> KvBuilder {
337        KvBuilder::new(
338            self.store.clone(),
339            self.preprocessors.clone(),
340            collection,
341            key,
342            value,
343        )
344    }
345
346    /// Get a key-value pair by key, returning the value and entity id
347    ///
348    /// Scans the collection for an entity whose named field `key` matches.
349    pub fn get_kv(&self, collection: &str, key: &str) -> Option<(Value, EntityId)> {
350        let manager = self.store.get_collection(collection)?;
351        let entities = manager.query_all(|_| true);
352        for entity in entities {
353            if let EntityData::Row(ref row) = entity.data {
354                if let Some(ref named) = row.named {
355                    if let Some(Value::Text(ref k)) = named.get("key") {
356                        if &**k == key {
357                            let value = named.get("value").cloned().unwrap_or(Value::Null);
358                            return Some((value, entity.id));
359                        }
360                    }
361                }
362            }
363        }
364        None
365    }
366
367    /// Delete a key-value pair by key, returning whether it was found and removed
368    pub fn delete_kv(
369        &self,
370        collection: &str,
371        key: &str,
372    ) -> Result<bool, super::super::error::DevXError> {
373        let Some((_, id)) = self.get_kv(collection, key) else {
374            return Ok(false);
375        };
376        self.store
377            .delete(collection, id)
378            .map_err(|err| super::super::error::DevXError::Storage(format!("{err:?}")))?;
379        Ok(true)
380    }
381
382    pub(crate) fn with_initialized_metadata(self) -> Result<Self, Box<dyn std::error::Error>> {
383        if self.options.mode == StorageMode::Persistent {
384            // Load metadata without persisting (avoids blocking catalog snapshot on boot)
385            match self.load_or_bootstrap_physical_metadata(false) {
386                Ok(metadata) => {
387                    crate::reserved_fields::validate_physical_metadata_contracts(&metadata)
388                        .map_err(|err| err.to_string())?;
389                }
390                Err(error)
391                    if error
392                        .downcast_ref::<std::io::Error>()
393                        .is_some_and(crate::physical::is_collection_contract_decode_error) =>
394                {
395                    return Err(error)
396                }
397                Err(_) => {}
398            }
399            // Skip repair on boot — deferred to first explicit persist_metadata() call.
400            // This avoids the recursive catalog_model_snapshot → physical_metadata loop
401            // that caused stack overflow / 12-second hang on startup.
402        }
403        self.load_collection_ttl_defaults_from_metadata();
404        // Single-file artifact: collection contracts ride the store's binary
405        // dump, not the metadata sidecar (which is never written in this
406        // profile). Seed the contract cache from that blob so each
407        // collection's declared model survives a restart — otherwise recovery
408        // re-infers the model from the stored entities and a KV collection
409        // comes back as a table.
410        if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
411            && self.options.storage_profile.packaging
412                == crate::storage::StoragePackaging::SingleFile
413        {
414            self.seed_contract_cache_from_store_aux()?;
415        }
416        // Issue #866 — rehydrate the hypertable chunk spine before the
417        // API opens so chunk routing / pruning / TTL work immediately
418        // after a restart.
419        self.load_hypertables_from_metadata();
420        self.recover_queue_pending_state();
421        Ok(self)
422    }
423
424    pub(crate) fn persist_metadata(&self) -> Result<(), Box<dyn std::error::Error>> {
425        if self.options.mode != StorageMode::Persistent || self.options.read_only {
426            return Ok(());
427        }
428        if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
429            && self.options.storage_profile.packaging
430                == crate::storage::StoragePackaging::SingleFile
431        {
432            return Ok(());
433        }
434        let Some(path) = self.path() else {
435            return Ok(());
436        };
437
438        let previous = self.load_or_bootstrap_physical_metadata(false).ok();
439        let collection_roots = self.physical_collection_roots();
440        let indexes = self
441            .native_physical_state()
442            .map(|state| self.physical_index_state_from_native_state(&state, previous.as_ref()))
443            .unwrap_or_else(|| self.physical_index_state());
444        let mut metadata = PhysicalMetadataFile::from_state(
445            self.options.clone(),
446            self.catalog_snapshot(),
447            collection_roots,
448            indexes,
449            previous.as_ref(),
450        );
451        metadata.collection_ttl_defaults_ms = self.collection_ttl_defaults_snapshot();
452        // Issue #866 — persist the hypertable chunk spine (specs +
453        // chunk bounds / routing / TTL) onto the same metadata path as
454        // collection contracts so a restart recovers it identically.
455        metadata.hypertables = self.hypertable_registry_snapshot();
456        metadata.save_for_data_path(path)?;
457        self.persist_native_physical_header(&metadata)?;
458        Ok(())
459    }
460
461    fn bootstrap_metadata_from_native_state(&self) -> Result<bool, Box<dyn std::error::Error>> {
462        if self.options.mode != StorageMode::Persistent || self.options.read_only {
463            return Ok(false);
464        }
465        let Some(path) = self.path() else {
466            return Ok(false);
467        };
468        let Some(native_state) = self.native_physical_state() else {
469            return Ok(false);
470        };
471        if !Self::native_state_is_bootstrap_complete(&native_state) {
472            return Ok(false);
473        }
474
475        let previous = PhysicalMetadataFile::load_for_data_path(path).ok();
476        let metadata = self.metadata_from_native_state(&native_state, previous.as_ref());
477        metadata.save_for_data_path(path)?;
478        self.persist_native_physical_header(&metadata)?;
479        Ok(true)
480    }
481
482    /// Rebuild the external physical metadata view from the native state published in the
483    /// paged database file.
484    pub fn rebuild_physical_metadata_from_native_state(
485        &self,
486    ) -> Result<bool, Box<dyn std::error::Error>> {
487        self.bootstrap_metadata_from_native_state()
488    }
489
490    pub(crate) fn native_state_is_bootstrap_complete(native_state: &NativePhysicalState) -> bool {
491        let registry_complete = native_state.registry.as_ref().map(|registry| {
492            registry.collections_complete
493                && registry.indexes_complete
494                && registry.graph_projections_complete
495                && registry.analytics_jobs_complete
496                && registry.vector_artifacts_complete
497        });
498        let recovery_complete = native_state
499            .recovery
500            .as_ref()
501            .map(|recovery| recovery.snapshots_complete && recovery.exports_complete);
502        let catalog_complete = native_state
503            .catalog
504            .as_ref()
505            .map(|catalog| catalog.collections_complete);
506
507        registry_complete == Some(true)
508            && recovery_complete == Some(true)
509            && catalog_complete == Some(true)
510    }
511
512    pub(crate) fn load_or_bootstrap_physical_metadata(
513        &self,
514        persist_bootstrapped: bool,
515    ) -> Result<PhysicalMetadataFile, Box<dyn std::error::Error>> {
516        if self.options.mode != StorageMode::Persistent {
517            return Err("physical metadata requires persistent mode".into());
518        }
519        let Some(path) = self.path() else {
520            return Err("database path is not available".into());
521        };
522        let native_state = self.native_physical_state();
523
524        match PhysicalMetadataFile::load_for_data_path(path) {
525            Ok(metadata) => {
526                if let Some(native_state) = native_state.as_ref() {
527                    let inspection = Self::inspect_native_header_against_metadata(
528                        native_state.header,
529                        &metadata,
530                    );
531                    if Self::repair_policy_for_inspection(&inspection)
532                        == NativeHeaderRepairPolicy::NativeAheadOfMetadata
533                    {
534                        let bootstrapped =
535                            self.metadata_from_native_state(native_state, Some(&metadata));
536                        if persist_bootstrapped && !self.options.read_only {
537                            bootstrapped.save_for_data_path(path)?;
538                            self.persist_native_physical_header(&bootstrapped)?;
539                        }
540                        return Ok(bootstrapped);
541                    }
542                }
543                Ok(metadata)
544            }
545            Err(err) => {
546                if crate::physical::is_collection_contract_decode_error(&err) {
547                    return Err(err.into());
548                }
549                let Some(native_state) = native_state else {
550                    return Err(err.into());
551                };
552                // Accept the bootstrap when the native state is either
553                // (a) fully populated and consistent (the original
554                // contract), or (b) trivially empty — a freshly created
555                // database with no collections written yet. Without (b)
556                // a brand-new data file can never reach
557                // `readiness_for_query = true`, because the bootstrap
558                // refuses to run until the registry/catalog/recovery
559                // structures are "complete", which they never become
560                // until the bootstrap has already run once.
561                //
562                // The emptiness check is conservative: header.sequence
563                // must still be at its initial value AND all three
564                // physical state summaries must be absent. Anything
565                // else falls through to the original error so we never
566                // paper over partially corrupted files.
567                let is_fresh_empty = native_state.header.sequence == 0
568                    && native_state.registry.is_none()
569                    && native_state.catalog.is_none()
570                    && native_state.recovery.is_none();
571                if !is_fresh_empty && !Self::native_state_is_bootstrap_complete(&native_state) {
572                    return Err(err.into());
573                }
574                let metadata = self.metadata_from_native_state(&native_state, None);
575                if persist_bootstrapped && !self.options.read_only {
576                    metadata.save_for_data_path(path)?;
577                    self.persist_native_physical_header(&metadata)?;
578                }
579                Ok(metadata)
580            }
581        }
582    }
583
584    pub(crate) fn physical_metadata_preference(&self) -> Option<&'static str> {
585        let path = self.path()?;
586        let native_state = self.native_physical_state();
587        let metadata = PhysicalMetadataFile::load_for_data_path(path).ok();
588
589        match (metadata, native_state) {
590            (Some(metadata), Some(native_state)) => {
591                let inspection =
592                    Self::inspect_native_header_against_metadata(native_state.header, &metadata);
593                match Self::repair_policy_for_inspection(&inspection) {
594                    NativeHeaderRepairPolicy::InSync => Some("sidecar_current"),
595                    NativeHeaderRepairPolicy::RepairNativeFromMetadata => Some("sidecar_current"),
596                    NativeHeaderRepairPolicy::NativeAheadOfMetadata => Some("native_ahead"),
597                }
598            }
599            (Some(_), None) => Some("sidecar_only"),
600            (None, Some(_)) => Some("sidecar_missing_native_available"),
601            (None, None) => Some("sidecar_missing_no_native"),
602        }
603    }
604
605    fn metadata_from_native_state(
606        &self,
607        native_state: &NativePhysicalState,
608        previous: Option<&PhysicalMetadataFile>,
609    ) -> PhysicalMetadataFile {
610        let now = SystemTime::now()
611            .duration_since(UNIX_EPOCH)
612            .unwrap_or_default()
613            .as_millis();
614        let catalog = self.catalog_snapshot();
615        let catalog_name = catalog.name.clone();
616        let catalog_total_entities = catalog.total_entities;
617        let catalog_total_collections = catalog.total_collections;
618        let indexes = self.physical_index_state();
619
620        let mut manifest =
621            crate::api::SchemaManifest::now(self.options.clone(), catalog.total_collections);
622        manifest.updated_at_unix_ms = now;
623
624        let manifest_events = native_state
625            .manifest
626            .as_ref()
627            .map(|summary| {
628                summary
629                    .recent_events
630                    .iter()
631                    .map(|event| crate::physical::ManifestEvent {
632                        collection: event.collection.clone(),
633                        object_key: event.object_key.clone(),
634                        kind: match event.kind.as_str() {
635                            "insert" => crate::physical::ManifestEventKind::Insert,
636                            "update" => crate::physical::ManifestEventKind::Update,
637                            "remove" => crate::physical::ManifestEventKind::Remove,
638                            _ => crate::physical::ManifestEventKind::Checkpoint,
639                        },
640                        block: crate::physical::BlockReference {
641                            index: event.block_index,
642                            checksum: event.block_checksum,
643                        },
644                        snapshot_min: event.snapshot_min,
645                        snapshot_max: event.snapshot_max,
646                    })
647                    .collect()
648            })
649            .unwrap_or_default();
650
651        let graph_projections = native_state
652            .registry
653            .as_ref()
654            .and_then(|registry| {
655                registry.graph_projections_complete.then(|| {
656                    registry
657                        .graph_projections
658                        .iter()
659                        .map(|projection| crate::physical::PhysicalGraphProjection {
660                            name: projection.name.clone(),
661                            created_at_unix_ms: projection.created_at_unix_ms,
662                            updated_at_unix_ms: projection.updated_at_unix_ms,
663                            state: "materialized".to_string(),
664                            source: projection.source.clone(),
665                            node_labels: projection.node_labels.clone(),
666                            node_types: projection.node_types.clone(),
667                            edge_labels: projection.edge_labels.clone(),
668                            last_materialized_sequence: projection.last_materialized_sequence,
669                        })
670                        .collect()
671                })
672            })
673            .or_else(|| previous.map(|metadata| metadata.graph_projections.clone()))
674            .unwrap_or_default();
675
676        let analytics_jobs = native_state
677            .registry
678            .as_ref()
679            .and_then(|registry| {
680                registry.analytics_jobs_complete.then(|| {
681                    registry
682                        .analytics_jobs
683                        .iter()
684                        .map(|job| crate::physical::PhysicalAnalyticsJob {
685                            id: job.id.clone(),
686                            kind: job.kind.clone(),
687                            state: job.state.clone(),
688                            projection: job.projection.clone(),
689                            created_at_unix_ms: job.created_at_unix_ms,
690                            updated_at_unix_ms: job.updated_at_unix_ms,
691                            last_run_sequence: job.last_run_sequence,
692                            metadata: job.metadata.clone(),
693                        })
694                        .collect()
695                })
696            })
697            .or_else(|| previous.map(|metadata| metadata.analytics_jobs.clone()))
698            .unwrap_or_default();
699
700        let exports = native_state
701            .recovery
702            .as_ref()
703            .and_then(|recovery| {
704                recovery.exports_complete.then(|| {
705                    recovery
706                        .exports
707                        .iter()
708                        .map(|export| crate::physical::ExportDescriptor {
709                            name: export.name.clone(),
710                            created_at_unix_ms: export.created_at_unix_ms,
711                            snapshot_id: export.snapshot_id,
712                            superblock_sequence: export.superblock_sequence,
713                            data_path: self
714                                .path()
715                                .map(|path| {
716                                    crate::physical::PhysicalMetadataFile::export_data_path_for(
717                                        path,
718                                        &export.name,
719                                    )
720                                    .display()
721                                    .to_string()
722                                })
723                                .unwrap_or_default(),
724                            metadata_path: self
725                                .path()
726                                .map(|path| {
727                                    let export_data_path =
728                                        crate::physical::PhysicalMetadataFile::export_data_path_for(
729                                            path,
730                                            &export.name,
731                                        );
732                                    crate::physical::PhysicalMetadataFile::metadata_path_for(
733                                        &export_data_path,
734                                    )
735                                    .display()
736                                    .to_string()
737                                })
738                                .unwrap_or_default(),
739                            collection_count: export.collection_count as usize,
740                            total_entities: export.total_entities as usize,
741                        })
742                        .collect()
743                })
744            })
745            .or_else(|| previous.map(|metadata| metadata.exports.clone()))
746            .unwrap_or_default();
747
748        let snapshots = native_state
749            .recovery
750            .as_ref()
751            .and_then(|recovery| {
752                recovery.snapshots_complete.then(|| {
753                    recovery
754                        .snapshots
755                        .iter()
756                        .map(|snapshot| crate::physical::SnapshotDescriptor {
757                            snapshot_id: snapshot.snapshot_id,
758                            created_at_unix_ms: snapshot.created_at_unix_ms,
759                            superblock_sequence: snapshot.superblock_sequence,
760                            collection_count: snapshot.collection_count as usize,
761                            total_entities: snapshot.total_entities as usize,
762                        })
763                        .collect()
764                })
765            })
766            .or_else(|| previous.map(|metadata| metadata.snapshots.clone()))
767            .unwrap_or_else(|| {
768                vec![crate::physical::SnapshotDescriptor {
769                    snapshot_id: native_state.header.sequence,
770                    created_at_unix_ms: now,
771                    superblock_sequence: native_state.header.sequence,
772                    collection_count: catalog_total_collections,
773                    total_entities: catalog_total_entities,
774                }]
775            });
776
777        let catalog_stats = native_state
778            .catalog
779            .as_ref()
780            .and_then(|native_catalog| {
781                native_catalog.collections_complete.then(|| {
782                    native_catalog
783                        .collections
784                        .iter()
785                        .map(|collection| {
786                            (
787                                collection.name.clone(),
788                                crate::api::CollectionStats {
789                                    entities: collection.entities as usize,
790                                    cross_refs: collection.cross_refs as usize,
791                                    segments: collection.segments as usize,
792                                },
793                            )
794                        })
795                        .collect::<BTreeMap<_, _>>()
796                })
797            })
798            .or_else(|| previous.map(|metadata| metadata.catalog.stats_by_collection.clone()))
799            .unwrap_or_else(|| catalog.stats_by_collection.clone());
800
801        PhysicalMetadataFile {
802            protocol_version: crate::physical::PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
803            generated_at_unix_ms: now,
804            last_loaded_from: Some("native_bootstrap".to_string()),
805            last_healed_at_unix_ms: Some(now),
806            manifest,
807            catalog: crate::api::CatalogSnapshot {
808                name: catalog_name,
809                total_entities: native_state
810                    .catalog
811                    .as_ref()
812                    .map(|summary| summary.total_entities as usize)
813                    .unwrap_or(catalog_total_entities),
814                total_collections: native_state
815                    .catalog
816                    .as_ref()
817                    .map(|summary| summary.collection_count as usize)
818                    .unwrap_or(catalog_total_collections),
819                stats_by_collection: catalog_stats,
820                updated_at: SystemTime::now(),
821            },
822            manifest_events,
823            collection_ttl_defaults_ms: previous
824                .map(|metadata| metadata.collection_ttl_defaults_ms.clone())
825                .unwrap_or_default(),
826            collection_contracts: previous
827                .map(|metadata| metadata.collection_contracts.clone())
828                .unwrap_or_default(),
829            hypertables: previous
830                .map(|metadata| metadata.hypertables.clone())
831                .unwrap_or_default(),
832            tree_definitions: previous
833                .map(|metadata| metadata.tree_definitions.clone())
834                .unwrap_or_default(),
835            indexes,
836            graph_projections,
837            analytics_jobs,
838            exports,
839            superblock: crate::physical::SuperblockHeader {
840                format_version: native_state.header.format_version,
841                sequence: native_state.header.sequence,
842                copies: crate::physical::DEFAULT_SUPERBLOCK_COPIES,
843                manifest: crate::physical::ManifestPointers {
844                    oldest: crate::physical::BlockReference {
845                        index: native_state.header.manifest_oldest_root,
846                        checksum: 0,
847                    },
848                    newest: crate::physical::BlockReference {
849                        index: native_state.header.manifest_root,
850                        checksum: 0,
851                    },
852                },
853                free_set: crate::physical::BlockReference {
854                    index: native_state.header.free_set_root,
855                    checksum: 0,
856                },
857                collection_roots: native_state.collection_roots.clone(),
858            },
859            snapshots,
860        }
861    }
862
863    pub(crate) fn reconcile_index_states_with_native_artifacts(
864        &self,
865        mut indexes: Vec<PhysicalIndexState>,
866    ) -> Vec<PhysicalIndexState> {
867        let native_artifacts = self
868            .native_physical_state()
869            .and_then(|state| state.registry)
870            .map(|registry| registry.vector_artifacts)
871            .unwrap_or_default();
872        for index in &mut indexes {
873            let Some(collection) = index.collection.as_deref() else {
874                continue;
875            };
876            let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
877                continue;
878            };
879            let Some(artifact) = native_artifacts.iter().find(|artifact| {
880                artifact.collection == collection && artifact.artifact_kind == artifact_kind
881            }) else {
882                index.build_state = "metadata-only".to_string();
883                continue;
884            };
885            index.entries = artifact.vector_count as usize;
886            index.estimated_memory_bytes = artifact.serialized_bytes;
887            index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
888            index.artifact_kind = Some(artifact.artifact_kind.clone());
889            index.artifact_checksum = Some(artifact.checksum);
890            index.build_state = "artifact-published".to_string();
891            if let Some(pages) = self.native_vector_artifact_pages() {
892                index.artifact_root_page = pages
893                    .into_iter()
894                    .find(|page| {
895                        page.collection == artifact.collection
896                            && page.artifact_kind == artifact.artifact_kind
897                    })
898                    .map(|page| page.root_page);
899            }
900        }
901        indexes
902    }
903
904    pub(crate) fn warmup_native_vector_artifact_for_index(
905        &self,
906        index: &PhysicalIndexState,
907    ) -> Result<(), String> {
908        let Some(collection) = index.collection.as_deref() else {
909            return Ok(());
910        };
911        let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
912            return Ok(());
913        };
914        self.warmup_native_vector_artifact(collection, Some(artifact_kind))?;
915        Ok(())
916    }
917
918    pub(crate) fn apply_runtime_native_artifact_to_index_state(
919        &self,
920        index: &mut PhysicalIndexState,
921    ) -> Result<(), String> {
922        let Some(collection) = index.collection.as_deref() else {
923            return Ok(());
924        };
925        let Some(artifact_kind) = Self::native_artifact_kind_for_index(index.kind) else {
926            return Ok(());
927        };
928        let artifact = self.inspect_native_vector_artifact(collection, Some(artifact_kind))?;
929        index.entries = artifact
930            .graph_edge_count
931            .or(artifact.text_posting_count)
932            .unwrap_or(artifact.node_count) as usize;
933        index.estimated_memory_bytes = artifact.byte_len;
934        index.backend = format!("{}+native-artifact", index_backend_name(index.kind));
935        index.artifact_kind = Some(artifact.artifact_kind.clone());
936        index.artifact_checksum = Some(artifact.checksum);
937        index.build_state = "ready".to_string();
938        index.artifact_root_page = self
939            .native_vector_artifact_pages()
940            .and_then(|pages| {
941                pages.into_iter().find(|page| {
942                    page.collection == artifact.collection
943                        && page.artifact_kind == artifact.artifact_kind
944                })
945            })
946            .map(|page| page.root_page);
947        Ok(())
948    }
949
950    pub(crate) fn physical_index_state_from_native_state(
951        &self,
952        native_state: &NativePhysicalState,
953        previous: Option<&PhysicalMetadataFile>,
954    ) -> Vec<PhysicalIndexState> {
955        let mut fresh = self.physical_index_state();
956        let Some(registry) = native_state.registry.as_ref() else {
957            if let Some(previous) = previous {
958                for index in &previous.indexes {
959                    if !fresh.iter().any(|candidate| candidate.name == index.name) {
960                        fresh.push(index.clone());
961                    }
962                }
963            }
964            return fresh;
965        };
966
967        for index in &mut fresh {
968            if let Some(native) = registry
969                .indexes
970                .iter()
971                .find(|candidate| candidate.name == index.name)
972            {
973                index.enabled = native.enabled;
974                index.last_refresh_ms = native.last_refresh_ms;
975                index.backend = native.backend.clone();
976                index.entries = native.entries as usize;
977                index.estimated_memory_bytes = native.estimated_memory_bytes;
978                if index.artifact_kind.is_none() {
979                    index.artifact_kind = Self::native_artifact_kind_for_index(index.kind)
980                        .map(|value| value.to_string());
981                }
982                if index.build_state == "catalog-derived" {
983                    index.build_state = "registry-loaded".to_string();
984                }
985            }
986        }
987
988        for native in &registry.indexes {
989            if fresh.iter().any(|index| index.name == native.name) {
990                continue;
991            }
992            let Some(kind) = Self::index_kind_from_str(&native.kind) else {
993                continue;
994            };
995            fresh.push(PhysicalIndexState {
996                name: native.name.clone(),
997                kind,
998                collection: native.collection.clone(),
999                enabled: native.enabled,
1000                entries: native.entries as usize,
1001                estimated_memory_bytes: native.estimated_memory_bytes,
1002                last_refresh_ms: native.last_refresh_ms,
1003                backend: native.backend.clone(),
1004                artifact_kind: Self::native_artifact_kind_for_index(kind)
1005                    .map(|value| value.to_string()),
1006                artifact_root_page: None,
1007                artifact_checksum: None,
1008                build_state: "registry-loaded".to_string(),
1009            });
1010        }
1011
1012        if !registry.indexes_complete {
1013            if let Some(previous) = previous {
1014                for index in &previous.indexes {
1015                    if !fresh.iter().any(|candidate| candidate.name == index.name) {
1016                        fresh.push(index.clone());
1017                    }
1018                }
1019            }
1020        }
1021
1022        fresh
1023    }
1024
1025    pub(crate) fn graph_projections_from_native_state(
1026        &self,
1027        native_state: &NativePhysicalState,
1028    ) -> Vec<PhysicalGraphProjection> {
1029        native_state
1030            .registry
1031            .as_ref()
1032            .map(|registry| {
1033                registry
1034                    .graph_projections
1035                    .iter()
1036                    .map(|projection| PhysicalGraphProjection {
1037                        name: projection.name.clone(),
1038                        created_at_unix_ms: projection.created_at_unix_ms,
1039                        updated_at_unix_ms: projection.updated_at_unix_ms,
1040                        state: "materialized".to_string(),
1041                        source: projection.source.clone(),
1042                        node_labels: projection.node_labels.clone(),
1043                        node_types: projection.node_types.clone(),
1044                        edge_labels: projection.edge_labels.clone(),
1045                        last_materialized_sequence: projection.last_materialized_sequence,
1046                    })
1047                    .collect()
1048            })
1049            .unwrap_or_default()
1050    }
1051
1052    pub(crate) fn analytics_jobs_from_native_state(
1053        &self,
1054        native_state: &NativePhysicalState,
1055    ) -> Vec<PhysicalAnalyticsJob> {
1056        native_state
1057            .registry
1058            .as_ref()
1059            .map(|registry| {
1060                registry
1061                    .analytics_jobs
1062                    .iter()
1063                    .map(|job| PhysicalAnalyticsJob {
1064                        id: job.id.clone(),
1065                        kind: job.kind.clone(),
1066                        state: job.state.clone(),
1067                        projection: job.projection.clone(),
1068                        created_at_unix_ms: job.created_at_unix_ms,
1069                        updated_at_unix_ms: job.updated_at_unix_ms,
1070                        last_run_sequence: job.last_run_sequence,
1071                        metadata: job.metadata.clone(),
1072                    })
1073                    .collect()
1074            })
1075            .unwrap_or_default()
1076    }
1077
1078    pub(crate) fn exports_from_native_state(
1079        &self,
1080        native_state: &NativePhysicalState,
1081    ) -> Vec<ExportDescriptor> {
1082        native_state
1083            .recovery
1084            .as_ref()
1085            .map(|recovery| {
1086                recovery
1087                    .exports
1088                    .iter()
1089                    .map(|export| ExportDescriptor {
1090                        name: export.name.clone(),
1091                        created_at_unix_ms: export.created_at_unix_ms,
1092                        snapshot_id: export.snapshot_id,
1093                        superblock_sequence: export.superblock_sequence,
1094                        data_path: self
1095                            .path()
1096                            .map(|path| {
1097                                crate::physical::PhysicalMetadataFile::export_data_path_for(
1098                                    path,
1099                                    &export.name,
1100                                )
1101                                .display()
1102                                .to_string()
1103                            })
1104                            .unwrap_or_default(),
1105                        metadata_path: self
1106                            .path()
1107                            .map(|path| {
1108                                let export_data_path =
1109                                    crate::physical::PhysicalMetadataFile::export_data_path_for(
1110                                        path,
1111                                        &export.name,
1112                                    );
1113                                crate::physical::PhysicalMetadataFile::metadata_path_for(
1114                                    &export_data_path,
1115                                )
1116                                .display()
1117                                .to_string()
1118                            })
1119                            .unwrap_or_default(),
1120                        collection_count: export.collection_count as usize,
1121                        total_entities: export.total_entities as usize,
1122                    })
1123                    .collect()
1124            })
1125            .unwrap_or_default()
1126    }
1127
1128    pub(crate) fn snapshots_from_native_state(
1129        &self,
1130        native_state: &NativePhysicalState,
1131    ) -> Vec<crate::physical::SnapshotDescriptor> {
1132        let snapshots: Vec<_> = native_state
1133            .recovery
1134            .as_ref()
1135            .map(|recovery| {
1136                recovery
1137                    .snapshots
1138                    .iter()
1139                    .map(|snapshot| crate::physical::SnapshotDescriptor {
1140                        snapshot_id: snapshot.snapshot_id,
1141                        created_at_unix_ms: snapshot.created_at_unix_ms,
1142                        superblock_sequence: snapshot.superblock_sequence,
1143                        collection_count: snapshot.collection_count as usize,
1144                        total_entities: snapshot.total_entities as usize,
1145                    })
1146                    .collect()
1147            })
1148            .unwrap_or_default();
1149        if !snapshots.is_empty() {
1150            return snapshots;
1151        }
1152
1153        let now = SystemTime::now()
1154            .duration_since(UNIX_EPOCH)
1155            .unwrap_or_default()
1156            .as_millis();
1157        let (collection_count, total_entities) = native_state
1158            .catalog
1159            .as_ref()
1160            .map(|catalog| {
1161                (
1162                    catalog.collection_count as usize,
1163                    catalog.total_entities as usize,
1164                )
1165            })
1166            .unwrap_or_else(|| {
1167                let catalog = self.catalog_snapshot();
1168                (catalog.total_collections, catalog.total_entities)
1169            });
1170
1171        vec![crate::physical::SnapshotDescriptor {
1172            snapshot_id: native_state.header.sequence,
1173            created_at_unix_ms: now,
1174            superblock_sequence: native_state.header.sequence,
1175            collection_count,
1176            total_entities,
1177        }]
1178    }
1179
1180    fn index_kind_from_str(value: &str) -> Option<crate::index::IndexKind> {
1181        match value {
1182            "btree" => Some(crate::index::IndexKind::BTree),
1183            "vector.hnsw" => Some(crate::index::IndexKind::VectorHnsw),
1184            "vector.inverted" => Some(crate::index::IndexKind::VectorInverted),
1185            "vector.turbo" => Some(crate::index::IndexKind::VectorTurbo),
1186            "graph.adjacency" => Some(crate::index::IndexKind::GraphAdjacency),
1187            "text.fulltext" => Some(crate::index::IndexKind::FullText),
1188            "document.pathvalue" => Some(crate::index::IndexKind::DocumentPathValue),
1189            "search.hybrid" => Some(crate::index::IndexKind::HybridSearch),
1190            _ => None,
1191        }
1192    }
1193
1194    pub(crate) fn native_artifact_kind_for_index(kind: IndexKind) -> Option<&'static str> {
1195        match kind {
1196            IndexKind::VectorHnsw => Some("hnsw"),
1197            IndexKind::VectorInverted => Some("ivf"),
1198            IndexKind::VectorTurbo => Some("turboquant"),
1199            IndexKind::GraphAdjacency => Some("graph.adjacency"),
1200            IndexKind::FullText => Some("text.fulltext"),
1201            IndexKind::DocumentPathValue => Some("document.pathvalue"),
1202            _ => None,
1203        }
1204    }
1205
1206    fn index_is_declared(&self, name: &str) -> bool {
1207        self.physical_metadata()
1208            .map(|metadata| metadata.indexes.iter().any(|index| index.name == name))
1209            .unwrap_or(false)
1210    }
1211
1212    pub(crate) fn graph_projection_is_declared(&self, name: &str) -> bool {
1213        self.physical_metadata()
1214            .map(|metadata| {
1215                metadata
1216                    .graph_projections
1217                    .iter()
1218                    .any(|projection| projection.name == name)
1219            })
1220            .unwrap_or(false)
1221    }
1222
1223    pub(crate) fn graph_projection_is_operational(&self, name: &str) -> bool {
1224        self.operational_graph_projections()
1225            .into_iter()
1226            .any(|projection| projection.name == name && projection.state == "materialized")
1227    }
1228
1229    pub(crate) fn analytics_job_id(kind: &str, projection: Option<&str>) -> String {
1230        match projection {
1231            Some(projection) => format!("{kind}::{projection}"),
1232            None => format!("{kind}::global"),
1233        }
1234    }
1235
1236    pub(crate) fn update_physical_metadata<T, F>(
1237        &self,
1238        mutator: F,
1239    ) -> Result<T, Box<dyn std::error::Error>>
1240    where
1241        F: FnOnce(&mut PhysicalMetadataFile) -> T,
1242    {
1243        if self.options.mode != StorageMode::Persistent {
1244            return Err("physical metadata operations require persistent mode".into());
1245        }
1246        if self.options.read_only {
1247            return Err("physical metadata operations are not allowed in read-only mode".into());
1248        }
1249        let Some(path) = self.path() else {
1250            return Err("database path is not available".into());
1251        };
1252
1253        let mut metadata = self.load_or_bootstrap_physical_metadata(true)?;
1254
1255        if metadata.indexes.is_empty() {
1256            metadata.indexes = self.physical_index_state();
1257        }
1258        metadata.superblock.collection_roots = self.physical_collection_roots();
1259
1260        let result = mutator(&mut metadata);
1261        metadata.save_for_data_path(path)?;
1262        self.persist_native_physical_header(&metadata)?;
1263        Ok(result)
1264    }
1265
1266    pub(crate) fn persist_native_physical_header(
1267        &self,
1268        metadata: &PhysicalMetadataFile,
1269    ) -> Result<(), Box<dyn std::error::Error>> {
1270        if !self.paged_mode {
1271            return Ok(());
1272        }
1273
1274        let existing_page = self
1275            .store
1276            .physical_file_header()
1277            .map(|header| header.collection_roots_page)
1278            .filter(|page| *page != 0);
1279        let existing_registry_page = self
1280            .store
1281            .physical_file_header()
1282            .map(|header| header.registry_page)
1283            .filter(|page| *page != 0);
1284        let existing_recovery_page = self
1285            .store
1286            .physical_file_header()
1287            .map(|header| header.recovery_page)
1288            .filter(|page| *page != 0);
1289        let existing_catalog_page = self
1290            .store
1291            .physical_file_header()
1292            .map(|header| header.catalog_page)
1293            .filter(|page| *page != 0);
1294        let existing_metadata_state_page = self
1295            .store
1296            .physical_file_header()
1297            .map(|header| header.metadata_state_page)
1298            .filter(|page| *page != 0);
1299        let existing_vector_artifact_page = self
1300            .store
1301            .physical_file_header()
1302            .map(|header| header.vector_artifact_page)
1303            .filter(|page| *page != 0);
1304        let existing_manifest_page = self
1305            .store
1306            .physical_file_header()
1307            .map(|header| header.manifest_page)
1308            .filter(|page| *page != 0);
1309        let (manifest_page, manifest_checksum) = self.store.write_native_manifest_summary(
1310            metadata.superblock.sequence,
1311            &metadata.manifest_events,
1312            existing_manifest_page,
1313        )?;
1314        let (collection_roots_page, collection_roots_checksum) = self
1315            .store
1316            .write_native_collection_roots(&metadata.superblock.collection_roots, existing_page)?;
1317        let registry_summary = self.native_registry_summary_from_metadata(metadata);
1318        let (registry_page, registry_checksum) = self
1319            .store
1320            .write_native_registry_summary(&registry_summary, existing_registry_page)?;
1321        let recovery_summary = Self::native_recovery_summary_from_metadata(metadata);
1322        let (recovery_page, recovery_checksum) = self
1323            .store
1324            .write_native_recovery_summary(&recovery_summary, existing_recovery_page)?;
1325        let catalog_summary = Self::native_catalog_summary_from_metadata(metadata);
1326        let (catalog_page, catalog_checksum) = self
1327            .store
1328            .write_native_catalog_summary(&catalog_summary, existing_catalog_page)?;
1329        let metadata_state_summary = Self::native_metadata_state_summary_from_metadata(metadata);
1330        let (metadata_state_page, metadata_state_checksum) =
1331            self.store.write_native_metadata_state_summary(
1332                &metadata_state_summary,
1333                existing_metadata_state_page,
1334            )?;
1335        let vector_artifact_records = self.native_vector_artifact_records();
1336        let vector_artifact_payloads = vector_artifact_records
1337            .iter()
1338            .map(|(summary, bytes)| {
1339                (
1340                    summary.collection.clone(),
1341                    summary.artifact_kind.clone(),
1342                    bytes.clone(),
1343                )
1344            })
1345            .collect::<Vec<_>>();
1346        let (vector_artifact_page, vector_artifact_checksum, _vector_artifact_pages) =
1347            self.store.write_native_vector_artifact_store(
1348                &vector_artifact_payloads,
1349                existing_vector_artifact_page,
1350            )?;
1351        let mut header = Self::native_header_from_metadata(metadata);
1352        header.manifest_page = manifest_page;
1353        header.manifest_checksum = manifest_checksum;
1354        header.collection_roots_page = collection_roots_page;
1355        header.collection_roots_checksum = collection_roots_checksum;
1356        header.registry_page = registry_page;
1357        header.registry_checksum = registry_checksum;
1358        header.recovery_page = recovery_page;
1359        header.recovery_checksum = recovery_checksum;
1360        header.catalog_page = catalog_page;
1361        header.catalog_checksum = catalog_checksum;
1362        header.metadata_state_page = metadata_state_page;
1363        header.metadata_state_checksum = metadata_state_checksum;
1364        header.vector_artifact_page = vector_artifact_page;
1365        header.vector_artifact_checksum = vector_artifact_checksum;
1366        self.store.update_physical_file_header(header)?;
1367        self.store.persist()?;
1368        Ok(())
1369    }
1370
1371    pub(crate) fn native_header_from_metadata(
1372        metadata: &PhysicalMetadataFile,
1373    ) -> PhysicalFileHeader {
1374        PhysicalFileHeader {
1375            format_version: metadata.superblock.format_version,
1376            sequence: metadata.superblock.sequence,
1377            manifest_oldest_root: metadata.superblock.manifest.oldest.index,
1378            manifest_root: metadata.superblock.manifest.newest.index,
1379            free_set_root: metadata.superblock.free_set.index,
1380            manifest_page: 0,
1381            manifest_checksum: 0,
1382            collection_roots_page: 0,
1383            collection_roots_checksum: 0,
1384            collection_root_count: metadata.superblock.collection_roots.len() as u32,
1385            snapshot_count: metadata.snapshots.len() as u32,
1386            index_count: metadata.indexes.len() as u32,
1387            catalog_collection_count: metadata.catalog.total_collections as u32,
1388            catalog_total_entities: metadata.catalog.total_entities as u64,
1389            export_count: metadata.exports.len() as u32,
1390            graph_projection_count: metadata.graph_projections.len() as u32,
1391            analytics_job_count: metadata.analytics_jobs.len() as u32,
1392            manifest_event_count: metadata.manifest_events.len() as u32,
1393            registry_page: 0,
1394            registry_checksum: 0,
1395            recovery_page: 0,
1396            recovery_checksum: 0,
1397            catalog_page: 0,
1398            catalog_checksum: 0,
1399            metadata_state_page: 0,
1400            metadata_state_checksum: 0,
1401            vector_artifact_page: 0,
1402            vector_artifact_checksum: 0,
1403        }
1404    }
1405
1406    fn recover_queue_pending_state(&self) {
1407        const QUEUE_META_COLLECTION: &str = "red_queue_meta";
1408
1409        let Some(manager) = self.store.get_collection(QUEUE_META_COLLECTION) else {
1410            return;
1411        };
1412
1413        let pending_rows = manager.query_all(|entity| {
1414            entity.data.as_row().is_some_and(|row| {
1415                matches!(
1416                    row.get_field("kind"),
1417                    Some(reddb_types::Value::Text(kind))
1418                        if matches!(&**kind, "queue_pending" | "queue_pending_lc")
1419                )
1420            })
1421        });
1422
1423        for row in pending_rows {
1424            let _ = self.store.delete(QUEUE_META_COLLECTION, row.id);
1425        }
1426    }
1427}
1428
1429fn current_unix_ms() -> u64 {
1430    SystemTime::now()
1431        .duration_since(UNIX_EPOCH)
1432        .unwrap_or_default()
1433        .as_millis()
1434        .min(u128::from(u64::MAX)) as u64
1435}