1use super::*;
2
3impl RedDB {
4 pub(crate) fn write_column_block_page(
5 &self,
6 bytes: &[u8],
7 ) -> Result<crate::storage::engine::PageLocation, Box<dyn std::error::Error>> {
8 use crate::storage::engine::PageType;
9
10 let store = self.store();
11 let Some(pager) = store.pager() else {
12 return Ok(crate::storage::engine::PageLocation::new(
13 0,
14 0,
15 bytes.len() as u32,
16 ));
17 };
18 let content_size = crate::storage::engine::page::CONTENT_SIZE;
19 let page_count = bytes.len().max(1).div_ceil(content_size);
20 let mut first_page_id = None;
21 for page_index in 0..page_count {
22 let mut page = pager.allocate_page(PageType::ColumnBlock)?;
23 let page_id = page.page_id();
24 if let Some(first) = first_page_id {
25 let expected = first + page_index as u32;
26 if page_id != expected {
27 return Err(format!(
28 "column block page allocation was not contiguous: got {page_id}, expected {expected}"
29 )
30 .into());
31 }
32 } else {
33 first_page_id = Some(page_id);
34 }
35 let start = page_index * content_size;
36 let end = (start + content_size).min(bytes.len());
37 if start < end {
38 page.content_mut()[..end - start].copy_from_slice(&bytes[start..end]);
39 }
40 pager.write_page_encrypted(page_id, page)?;
41 }
42 Ok(crate::storage::engine::PageLocation::new(
43 first_page_id.expect("page_count is at least one"),
44 0,
45 bytes.len() as u32,
46 ))
47 }
48
49 pub(crate) fn read_column_block_page(
50 &self,
51 loc: crate::storage::engine::PageLocation,
52 ) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
53 let store = self.store();
54 let Some(pager) = store.pager() else {
55 return Ok(None);
56 };
57 let content_size = crate::storage::engine::page::CONTENT_SIZE;
58 let mut remaining = loc.length as usize;
59 let mut page_id = loc.page_id;
60 let mut offset = loc.offset as usize;
61 let mut out = Vec::with_capacity(remaining);
62 while remaining > 0 {
63 let page = pager.read_page_decrypted(page_id)?;
64 if page.page_type()? != crate::storage::engine::PageType::ColumnBlock {
65 return Err(format!("page {page_id} is not a ColumnBlock page").into());
66 }
67 if offset > content_size {
68 return Err(format!(
69 "column block page location offset out of bounds: {offset} > {content_size}"
70 )
71 .into());
72 }
73 let available = content_size - offset;
74 let take = remaining.min(available);
75 out.extend_from_slice(&page.content()[offset..offset + take]);
76 remaining -= take;
77 page_id = page_id.saturating_add(1);
78 offset = 0;
79 }
80 Ok(Some(out))
81 }
82
83 pub fn tree_definitions(&self) -> Vec<crate::physical::PhysicalTreeDefinition> {
84 self.physical_metadata()
85 .map(|metadata| metadata.tree_definitions)
86 .unwrap_or_default()
87 }
88
89 pub fn tree_definition(
90 &self,
91 collection: &str,
92 name: &str,
93 ) -> Option<crate::physical::PhysicalTreeDefinition> {
94 self.tree_definitions()
95 .into_iter()
96 .find(|definition| definition.collection == collection && definition.name == name)
97 }
98
99 pub fn save_tree_definition(
100 &self,
101 definition: crate::physical::PhysicalTreeDefinition,
102 ) -> Result<crate::physical::PhysicalTreeDefinition, Box<dyn std::error::Error>> {
103 self.update_physical_metadata(|metadata| {
104 if let Some(existing) = metadata.tree_definitions.iter_mut().find(|existing| {
105 existing.collection == definition.collection && existing.name == definition.name
106 }) {
107 *existing = definition.clone();
108 } else {
109 metadata.tree_definitions.push(definition.clone());
110 }
111 metadata.tree_definitions.sort_by(|left, right| {
112 left.collection
113 .cmp(&right.collection)
114 .then_with(|| left.name.cmp(&right.name))
115 });
116 definition.clone()
117 })
118 }
119
120 pub fn remove_tree_definition(
121 &self,
122 collection: &str,
123 name: &str,
124 ) -> Result<Option<crate::physical::PhysicalTreeDefinition>, Box<dyn std::error::Error>> {
125 self.update_physical_metadata(|metadata| {
126 metadata
127 .tree_definitions
128 .iter()
129 .position(|definition| {
130 definition.collection == collection && definition.name == name
131 })
132 .map(|index| metadata.tree_definitions.remove(index))
133 })
134 }
135
136 pub fn collection_default_ttl_ms(&self, collection: &str) -> Option<u64> {
137 self.collection_ttl_defaults_ms
138 .read()
139 .ok()
140 .and_then(|defaults| defaults.get(collection).copied())
141 }
142
143 pub fn set_collection_default_ttl_ms(&self, collection: impl Into<String>, ttl_ms: u64) {
144 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
145 defaults.insert(collection.into(), ttl_ms);
146 }
147 }
148
149 pub fn clear_collection_default_ttl_ms(&self, collection: &str) {
150 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
151 defaults.remove(collection);
152 }
153 }
154
155 pub fn collection_contracts(&self) -> Vec<crate::physical::CollectionContract> {
156 self.contract_cache_map()
157 .values()
158 .map(|arc| (**arc).clone())
159 .collect()
160 }
161
162 pub fn collection_contract(
163 &self,
164 collection: &str,
165 ) -> Option<crate::physical::CollectionContract> {
166 self.contract_cache_map()
167 .get(collection)
168 .map(|arc| (**arc).clone())
169 }
170
171 pub fn collection_contract_arc(
176 &self,
177 collection: &str,
178 ) -> Option<std::sync::Arc<crate::physical::CollectionContract>> {
179 self.contract_cache_map()
180 .get(collection)
181 .map(std::sync::Arc::clone)
182 }
183
184 fn contract_cache_map(
189 &self,
190 ) -> std::sync::Arc<
191 std::collections::HashMap<String, std::sync::Arc<crate::physical::CollectionContract>>,
192 > {
193 self.collection_contract_cache.get_or_load(|| {
194 self.physical_metadata()
195 .map(|metadata| metadata.collection_contracts)
196 .unwrap_or_default()
197 })
198 }
199
200 pub(crate) fn invalidate_collection_contract_cache(&self) {
201 self.collection_contract_cache.invalidate();
202 }
203
204 pub(crate) fn sync_single_file_contract_blob(&self) {
212 let contracts = self.collection_contracts();
213 let bytes = crate::physical::serialize_collection_contracts(&contracts);
214 self.store().set_aux_metadata(bytes);
215 }
216
217 pub(crate) fn seed_contract_cache_from_store_aux(&self) -> std::io::Result<()> {
222 let bytes = self.store().aux_metadata();
223 if bytes.is_empty() {
224 return Ok(());
225 }
226 let contracts = crate::physical::deserialize_collection_contracts(&bytes)?;
227 let map: std::collections::HashMap<_, _> = contracts
228 .into_iter()
229 .map(|contract| (contract.name.clone(), std::sync::Arc::new(contract)))
230 .collect();
231 let store = self.store();
235 let index = store.context_index();
236 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
237 for contract in map.values() {
238 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
239 if let Some(ttl_ms) = contract.default_ttl_ms {
240 defaults.insert(contract.name.clone(), ttl_ms);
241 }
242 }
243 }
244 self.collection_contract_cache.replace(map);
245 Ok(())
246 }
247
248 fn update_collection_contract_metadata<T, F>(
249 &self,
250 mutator: F,
251 ) -> Result<T, Box<dyn std::error::Error>>
252 where
253 F: FnOnce(&mut PhysicalMetadataFile) -> T,
254 {
255 if !self.paged_mode
256 || self.options.storage_profile.deploy_profile
257 != crate::storage::DeployProfile::Embedded
258 || self.options.storage_profile.packaging
259 != crate::storage::StoragePackaging::SingleFile
260 {
261 return self.update_physical_metadata(mutator);
262 }
263 if self.options.read_only {
264 return Err("physical metadata operations are not allowed in read-only mode".into());
265 }
266 let Some(path) = self.path() else {
267 return Err("database path is not available".into());
268 };
269
270 let _publication = self.store.vault_publication_lock.lock();
275 let mut metadata = self.load_or_bootstrap_physical_metadata(false)?;
276 let result = mutator(&mut metadata);
277 metadata.save_for_data_path(path)?;
278 Ok(result)
279 }
280
281 pub fn save_collection_contract(
282 &self,
283 contract: crate::physical::CollectionContract,
284 ) -> Result<crate::physical::CollectionContract, Box<dyn std::error::Error>> {
285 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
286 if let Some(ttl_ms) = contract.default_ttl_ms {
287 defaults.insert(contract.name.clone(), ttl_ms);
288 } else {
289 defaults.remove(&contract.name);
290 }
291 }
292
293 self.store()
294 .context_index()
295 .set_collection_enabled(&contract.name, contract.context_index_enabled);
296 if let Some(manager) = self.store().get_collection(&contract.name) {
297 let columns = contract
298 .declared_columns
299 .iter()
300 .map(|column| column.name.clone())
301 .collect();
302 manager.set_column_schema_if_empty(columns);
303 }
304
305 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
308 && self.options.storage_profile.packaging
309 == crate::storage::StoragePackaging::SingleFile
310 && !self.paged_mode
311 {
312 self.collection_contract_cache.insert(contract.clone());
313 self.sync_single_file_contract_blob();
314 return Ok(contract);
315 }
316
317 self.invalidate_collection_contract_cache();
318
319 let saved = self.update_collection_contract_metadata(|metadata| {
320 if let Some(existing) = metadata
321 .collection_contracts
322 .iter_mut()
323 .find(|existing| existing.name == contract.name)
324 {
325 *existing = contract.clone();
326 } else {
327 metadata.collection_contracts.push(contract.clone());
328 }
329 metadata
330 .collection_contracts
331 .sort_by(|left, right| left.name.cmp(&right.name));
332
333 if let Some(ttl_ms) = contract.default_ttl_ms {
334 metadata
335 .collection_ttl_defaults_ms
336 .insert(contract.name.clone(), ttl_ms);
337 } else {
338 metadata.collection_ttl_defaults_ms.remove(&contract.name);
339 }
340
341 contract.clone()
342 })?;
343 self.invalidate_collection_contract_cache();
344 Ok(saved)
345 }
346
347 pub fn remove_collection_contract(
348 &self,
349 collection: &str,
350 ) -> Result<Option<crate::physical::CollectionContract>, Box<dyn std::error::Error>> {
351 if let Ok(mut defaults) = self.collection_ttl_defaults_ms.write() {
352 defaults.remove(collection);
353 }
354
355 self.store()
356 .context_index()
357 .set_collection_enabled(collection, false);
358
359 if self.options.storage_profile.deploy_profile == crate::storage::DeployProfile::Embedded
360 && self.options.storage_profile.packaging
361 == crate::storage::StoragePackaging::SingleFile
362 && !self.paged_mode
363 {
364 let removed = self.collection_contract_cache.remove(collection);
365 self.sync_single_file_contract_blob();
366 return Ok(removed);
367 }
368
369 self.invalidate_collection_contract_cache();
370
371 let removed = self.update_collection_contract_metadata(|metadata| {
372 let removed = metadata
373 .collection_contracts
374 .iter()
375 .position(|contract| contract.name == collection)
376 .map(|index| metadata.collection_contracts.remove(index));
377 metadata.collection_ttl_defaults_ms.remove(collection);
378 metadata
379 .indexes
380 .retain(|index| index.collection.as_deref() != Some(collection));
381 removed
382 })?;
383 self.invalidate_collection_contract_cache();
384 Ok(removed)
385 }
386
387 pub(crate) fn collection_ttl_defaults_snapshot(&self) -> BTreeMap<String, u64> {
388 self.collection_ttl_defaults_ms
389 .read()
390 .map(|defaults| {
391 defaults
392 .iter()
393 .map(|(collection, ttl_ms)| (collection.clone(), *ttl_ms))
394 .collect()
395 })
396 .unwrap_or_default()
397 }
398
399 pub(crate) fn load_collection_ttl_defaults_from_metadata(&self) {
400 let metadata = self.physical_metadata();
401 let defaults = metadata
402 .as_ref()
403 .map(|m| m.collection_ttl_defaults_ms.clone())
404 .unwrap_or_default();
405
406 if let Ok(mut current) = self.collection_ttl_defaults_ms.write() {
407 current.clear();
408 current.extend(defaults);
409 }
410
411 if let Some(metadata) = metadata {
412 let store = self.store();
413 let index = store.context_index();
414 for contract in &metadata.collection_contracts {
415 index.set_collection_enabled(&contract.name, contract.context_index_enabled);
416 }
417 }
418 }
419
420 pub(crate) fn hypertable_registry_snapshot(&self) -> Vec<crate::physical::PhysicalHypertable> {
428 let registry = self.hypertables();
429 if registry.is_empty() {
430 return Vec::new();
431 }
432 use std::collections::BTreeMap;
433 let mut chunks_by_table: BTreeMap<String, Vec<crate::physical::PhysicalHypertableChunk>> =
434 BTreeMap::new();
435 for chunk in registry.snapshot_chunks() {
436 chunks_by_table
437 .entry(chunk.id.hypertable.clone())
438 .or_default()
439 .push(crate::physical::PhysicalHypertableChunk {
440 start_ns: chunk.id.start_ns,
441 end_ns_exclusive: chunk.end_ns_exclusive,
442 row_count: chunk.row_count,
443 min_ts_ns: chunk.min_ts_ns,
444 max_ts_ns: chunk.max_ts_ns,
445 sealed: chunk.sealed,
446 ttl_override_ns: chunk.ttl_override_ns,
447 columnar_page: chunk.columnar_page,
448 columnar_derived: chunk.columnar_page.is_some(),
449 });
450 }
451 registry
452 .list()
453 .into_iter()
454 .map(|spec| crate::physical::PhysicalHypertable {
455 chunks: chunks_by_table.remove(&spec.name).unwrap_or_default(),
456 name: spec.name,
457 time_column: spec.time_column,
458 chunk_interval_ns: spec.chunk_interval_ns,
459 default_ttl_ns: spec.default_ttl_ns,
460 })
461 .collect()
462 }
463
464 pub(crate) fn load_hypertables_from_metadata(&self) {
470 let Some(metadata) = self.physical_metadata() else {
471 return;
472 };
473 if metadata.hypertables.is_empty() {
474 return;
475 }
476 let registry = self.hypertables();
477 for hypertable in &metadata.hypertables {
478 let mut spec = crate::storage::timeseries::HypertableSpec::new(
479 hypertable.name.clone(),
480 hypertable.time_column.clone(),
481 hypertable.chunk_interval_ns,
482 );
483 if let Some(ttl) = hypertable.default_ttl_ns {
484 spec = spec.with_ttl_ns(ttl);
485 }
486 registry.register(spec);
487 for chunk in &hypertable.chunks {
488 let meta = crate::storage::timeseries::ChunkMeta {
489 id: crate::storage::timeseries::ChunkId {
490 hypertable: hypertable.name.clone(),
491 start_ns: chunk.start_ns,
492 },
493 end_ns_exclusive: chunk.end_ns_exclusive,
494 row_count: chunk.row_count,
495 min_ts_ns: chunk.min_ts_ns,
496 max_ts_ns: chunk.max_ts_ns,
497 sealed: chunk.sealed,
498 ttl_override_ns: chunk.ttl_override_ns,
499 columnar_page: chunk.columnar_page,
500 };
501 let id = meta.id.clone();
502 let columnar_page = meta.columnar_page;
503 registry.restore_chunk(meta);
504 if let Some(loc) = columnar_page {
505 if let Ok(Some(bytes)) = self.read_column_block_page(loc) {
506 registry.restore_columnar_block(&id, bytes);
507 }
508 }
509 }
510 }
511 }
512
513 pub fn run_maintenance(&self) -> Result<(), Box<dyn std::error::Error>> {
514 self.store.run_maintenance()?;
515 self.persist_metadata()?;
516 Ok(())
517 }
518
519 pub fn metadata_path(&self) -> Option<PathBuf> {
521 self.path
522 .as_ref()
523 .map(|path| PhysicalMetadataFile::metadata_path_for(path))
524 }
525
526 pub fn physical_metadata(&self) -> Option<PhysicalMetadataFile> {
528 self.load_or_bootstrap_physical_metadata(false).ok()
529 }
530
531 pub fn physical_indexes(&self) -> Vec<PhysicalIndexState> {
533 let indexes = self
534 .physical_metadata()
535 .map(|metadata| metadata.indexes)
536 .unwrap_or_default();
537 self.physical_indexes_from_declarations(indexes)
538 }
539
540 pub(crate) fn physical_indexes_from_declarations(
541 &self,
542 indexes: Vec<PhysicalIndexState>,
543 ) -> Vec<PhysicalIndexState> {
544 let indexes = Some(indexes)
545 .filter(|indexes| !indexes.is_empty())
546 .or_else(|| {
547 self.native_physical_state()
548 .map(|state| self.physical_index_state_from_native_state(&state, None))
549 })
550 .unwrap_or_else(|| self.physical_index_state());
551 self.reconcile_index_states_with_native_artifacts(indexes)
552 }
553
554 pub fn exports(&self) -> Vec<ExportDescriptor> {
556 self.physical_metadata()
557 .map(|metadata| metadata.exports)
558 .or_else(|| {
559 self.native_physical_state()
560 .map(|state| self.exports_from_native_state(&state))
561 })
562 .unwrap_or_default()
563 }
564
565 pub fn snapshots(&self) -> Vec<crate::physical::SnapshotDescriptor> {
567 if let Some(metadata) = self.physical_metadata() {
568 if !metadata.snapshots.is_empty() {
569 return metadata.snapshots;
570 }
571 }
572 self.native_physical_state()
573 .map(|state| self.snapshots_from_native_state(&state))
574 .unwrap_or_default()
575 }
576
577 pub fn graph_projections(&self) -> Vec<PhysicalGraphProjection> {
579 self.physical_metadata()
580 .map(|metadata| metadata.graph_projections)
581 .or_else(|| {
582 self.native_physical_state()
583 .map(|state| self.graph_projections_from_native_state(&state))
584 })
585 .unwrap_or_default()
586 }
587
588 pub fn declared_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
590 self.catalog_model_snapshot().declared_graph_projections
591 }
592
593 pub fn operational_graph_projections(&self) -> Vec<PhysicalGraphProjection> {
595 self.graph_projections()
596 .into_iter()
597 .filter(|projection| {
598 projection.last_materialized_sequence.is_some()
599 || matches!(projection.state.as_str(), "materialized" | "stale")
600 })
601 .collect()
602 }
603
604 pub fn analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
606 self.physical_metadata()
607 .map(|metadata| metadata.analytics_jobs)
608 .or_else(|| {
609 self.native_physical_state()
610 .map(|state| self.analytics_jobs_from_native_state(&state))
611 })
612 .unwrap_or_default()
613 }
614
615 pub fn declared_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
617 self.catalog_model_snapshot().declared_analytics_jobs
618 }
619
620 pub fn operational_analytics_jobs(&self) -> Vec<PhysicalAnalyticsJob> {
622 self.analytics_jobs()
623 .into_iter()
624 .filter(|job| {
625 job.last_run_sequence.is_some()
626 || matches!(
627 job.state.as_str(),
628 "running" | "completed" | "failed" | "queued" | "stale"
629 )
630 })
631 .collect()
632 }
633
634 pub fn declared_indexes(&self) -> Vec<PhysicalIndexState> {
636 self.catalog_model_snapshot().declared_indexes
637 }
638
639 pub fn operational_indexes(&self) -> Vec<PhysicalIndexState> {
641 self.catalog_model_snapshot().operational_indexes
642 }
643
644 pub fn index_statuses(&self) -> Vec<CatalogIndexStatus> {
646 self.catalog_model_snapshot().index_statuses
647 }
648
649 pub fn index_status(&self, name: &str) -> Option<CatalogIndexStatus> {
651 self.catalog_model_snapshot()
652 .index_statuses
653 .into_iter()
654 .find(|status| status.name == name)
655 }
656
657 pub fn save_graph_projection(
659 &self,
660 name: impl Into<String>,
661 node_labels: Vec<String>,
662 node_types: Vec<String>,
663 edge_labels: Vec<String>,
664 source: impl Into<String>,
665 ) -> Result<PhysicalGraphProjection, Box<dyn std::error::Error>> {
666 let name = name.into();
667 let source = source.into();
668 self.update_physical_metadata(|metadata| {
669 let now = SystemTime::now()
670 .duration_since(UNIX_EPOCH)
671 .unwrap_or_default()
672 .as_millis();
673 let projection = if let Some(existing) = metadata
674 .graph_projections
675 .iter_mut()
676 .find(|projection| projection.name == name)
677 {
678 existing.updated_at_unix_ms = now;
679 existing.state = "declared".to_string();
680 existing.source = source.clone();
681 existing.node_labels = node_labels.clone();
682 existing.node_types = node_types.clone();
683 existing.edge_labels = edge_labels.clone();
684 existing.last_materialized_sequence = None;
685 existing.clone()
686 } else {
687 let projection = PhysicalGraphProjection {
688 name: name.clone(),
689 created_at_unix_ms: now,
690 updated_at_unix_ms: now,
691 state: "declared".to_string(),
692 source: source.clone(),
693 node_labels: node_labels.clone(),
694 node_types: node_types.clone(),
695 edge_labels: edge_labels.clone(),
696 last_materialized_sequence: None,
697 };
698 metadata.graph_projections.push(projection.clone());
699 projection
700 };
701
702 Self::mark_projection_dependent_jobs_stale(metadata, &name, now);
703
704 metadata
705 .graph_projections
706 .sort_by(|left, right| left.name.cmp(&right.name));
707 projection
708 })
709 }
710
711 pub fn materialize_graph_projection(
713 &self,
714 name: &str,
715 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
716 self.update_physical_metadata(|metadata| {
717 let now = SystemTime::now()
718 .duration_since(UNIX_EPOCH)
719 .unwrap_or_default()
720 .as_millis();
721 let idx = metadata
722 .graph_projections
723 .iter()
724 .position(|projection| projection.name == name);
725 if let Some(idx) = idx {
726 metadata.graph_projections[idx].updated_at_unix_ms = now;
727 metadata.graph_projections[idx].state = "materialized".to_string();
728 metadata.graph_projections[idx].last_materialized_sequence =
729 Some(metadata.superblock.sequence);
730 let result = metadata.graph_projections[idx].clone();
731 Self::rearm_projection_dependent_jobs_declared(metadata, name, now);
732 return Some(result);
733 }
734 None
735 })
736 }
737
738 pub fn mark_graph_projection_materializing(
740 &self,
741 name: &str,
742 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
743 self.update_physical_metadata(|metadata| {
744 let now = SystemTime::now()
745 .duration_since(UNIX_EPOCH)
746 .unwrap_or_default()
747 .as_millis();
748 let idx = metadata
749 .graph_projections
750 .iter()
751 .position(|projection| projection.name == name);
752 if let Some(idx) = idx {
753 metadata.graph_projections[idx].updated_at_unix_ms = now;
754 metadata.graph_projections[idx].state = "materializing".to_string();
755 metadata.graph_projections[idx].last_materialized_sequence = None;
756 let result = metadata.graph_projections[idx].clone();
757 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
758 return Some(result);
759 }
760 None
761 })
762 }
763
764 pub fn fail_graph_projection(
766 &self,
767 name: &str,
768 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
769 self.update_physical_metadata(|metadata| {
770 let now = SystemTime::now()
771 .duration_since(UNIX_EPOCH)
772 .unwrap_or_default()
773 .as_millis();
774 let idx = metadata
775 .graph_projections
776 .iter()
777 .position(|projection| projection.name == name);
778 if let Some(idx) = idx {
779 metadata.graph_projections[idx].updated_at_unix_ms = now;
780 metadata.graph_projections[idx].state = "failed".to_string();
781 metadata.graph_projections[idx].last_materialized_sequence = None;
782 let result = metadata.graph_projections[idx].clone();
783 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
784 return Some(result);
785 }
786 None
787 })
788 }
789
790 pub fn mark_graph_projection_stale(
792 &self,
793 name: &str,
794 ) -> Result<Option<PhysicalGraphProjection>, Box<dyn std::error::Error>> {
795 self.update_physical_metadata(|metadata| {
796 let now = SystemTime::now()
797 .duration_since(UNIX_EPOCH)
798 .unwrap_or_default()
799 .as_millis();
800 let idx = metadata
801 .graph_projections
802 .iter()
803 .position(|projection| projection.name == name);
804 if let Some(idx) = idx {
805 metadata.graph_projections[idx].updated_at_unix_ms = now;
806 metadata.graph_projections[idx].state = "stale".to_string();
807 let result = metadata.graph_projections[idx].clone();
808 Self::mark_projection_dependent_jobs_stale(metadata, name, now);
809 return Some(result);
810 }
811 None
812 })
813 }
814
815 fn mark_projection_dependent_jobs_stale(
816 metadata: &mut PhysicalMetadataFile,
817 projection_name: &str,
818 now: u128,
819 ) {
820 for job in metadata.analytics_jobs.iter_mut() {
821 if job.projection.as_deref() == Some(projection_name) && job.state != "declared" {
822 job.state = "stale".to_string();
823 job.updated_at_unix_ms = now;
824 }
825 }
826 }
827
828 fn rearm_projection_dependent_jobs_declared(
829 metadata: &mut PhysicalMetadataFile,
830 projection_name: &str,
831 now: u128,
832 ) {
833 for job in metadata.analytics_jobs.iter_mut() {
834 if job.projection.as_deref() == Some(projection_name) && job.state == "stale" {
835 job.state = "declared".to_string();
836 job.last_run_sequence = None;
837 job.updated_at_unix_ms = now;
838 }
839 }
840 }
841
842 pub fn save_analytics_job(
844 &self,
845 kind: impl Into<String>,
846 projection: Option<String>,
847 metadata_entries: BTreeMap<String, String>,
848 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
849 let kind = kind.into();
850 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
851 if let Some(projection_name) = projection.as_deref() {
852 if !self.graph_projection_is_declared(projection_name) {
853 return Err(format!(
854 "graph projection '{projection_name}' is not declared in physical metadata"
855 )
856 .into());
857 }
858 }
859
860 self.update_physical_metadata(|metadata| {
861 let now = SystemTime::now()
862 .duration_since(UNIX_EPOCH)
863 .unwrap_or_default()
864 .as_millis();
865
866 let job = if let Some(existing) = metadata
867 .analytics_jobs
868 .iter_mut()
869 .find(|job| job.id == job_id)
870 {
871 existing.kind = kind.clone();
872 existing.projection = projection.clone();
873 existing.updated_at_unix_ms = now;
874 existing.metadata = metadata_entries.clone();
875 if existing.last_run_sequence.is_none() {
876 existing.state = "declared".to_string();
877 }
878 existing.clone()
879 } else {
880 let job = PhysicalAnalyticsJob {
881 id: job_id.clone(),
882 kind: kind.clone(),
883 state: "declared".to_string(),
884 projection: projection.clone(),
885 created_at_unix_ms: now,
886 updated_at_unix_ms: now,
887 last_run_sequence: None,
888 metadata: metadata_entries.clone(),
889 };
890 metadata.analytics_jobs.push(job.clone());
891 job
892 };
893
894 metadata
895 .analytics_jobs
896 .sort_by(|left, right| left.id.cmp(&right.id));
897 job
898 })
899 }
900
901 pub fn record_analytics_job(
903 &self,
904 kind: impl Into<String>,
905 projection: Option<String>,
906 metadata_entries: BTreeMap<String, String>,
907 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
908 let kind = kind.into();
909 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
910 if let Some(projection_name) = projection.as_deref() {
911 if !self.graph_projection_is_declared(projection_name) {
912 return Err(format!(
913 "graph projection '{projection_name}' is not declared in physical metadata"
914 )
915 .into());
916 }
917 if !self.graph_projection_is_operational(projection_name) {
918 return Err(format!(
919 "graph projection '{projection_name}' is declared but not operationally materialized"
920 )
921 .into());
922 }
923 }
924
925 self.update_physical_metadata(|metadata| {
926 let now = SystemTime::now()
927 .duration_since(UNIX_EPOCH)
928 .unwrap_or_default()
929 .as_millis();
930
931 let existing = metadata
932 .analytics_jobs
933 .iter_mut()
934 .find(|job| job.id == job_id)?;
935 existing.state = "completed".to_string();
936 existing.kind = kind.clone();
937 existing.projection = projection.clone();
938 existing.updated_at_unix_ms = now;
939 existing.last_run_sequence = Some(metadata.superblock.sequence);
940 existing.metadata = metadata_entries.clone();
941 let job = existing.clone();
942
943 metadata
944 .analytics_jobs
945 .sort_by(|left, right| left.id.cmp(&right.id));
946 Some(job)
947 })
948 .and_then(|job| {
949 job.ok_or_else(|| {
950 format!("analytics job '{job_id}' is not declared in physical metadata").into()
951 })
952 })
953 }
954
955 pub fn queue_analytics_job(
957 &self,
958 kind: impl Into<String>,
959 projection: Option<String>,
960 metadata_entries: BTreeMap<String, String>,
961 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
962 let kind = kind.into();
963 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
964 if let Some(projection_name) = projection.as_deref() {
965 if !self.graph_projection_is_declared(projection_name) {
966 return Err(format!(
967 "graph projection '{projection_name}' is not declared in physical metadata"
968 )
969 .into());
970 }
971 if !self.graph_projection_is_operational(projection_name) {
972 return Err(format!(
973 "graph projection '{projection_name}' is declared but not operationally materialized"
974 )
975 .into());
976 }
977 }
978
979 self.update_physical_metadata(|metadata| {
980 let now = SystemTime::now()
981 .duration_since(UNIX_EPOCH)
982 .unwrap_or_default()
983 .as_millis();
984
985 let existing = metadata
986 .analytics_jobs
987 .iter_mut()
988 .find(|job| job.id == job_id)?;
989 existing.state = "queued".to_string();
990 existing.kind = kind.clone();
991 existing.projection = projection.clone();
992 existing.updated_at_unix_ms = now;
993 existing.metadata = metadata_entries.clone();
994 let job = existing.clone();
995
996 metadata
997 .analytics_jobs
998 .sort_by(|left, right| left.id.cmp(&right.id));
999 Some(job)
1000 })
1001 .and_then(|job| {
1002 job.ok_or_else(|| {
1003 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1004 })
1005 })
1006 }
1007
1008 pub fn start_analytics_job(
1010 &self,
1011 kind: impl Into<String>,
1012 projection: Option<String>,
1013 metadata_entries: BTreeMap<String, String>,
1014 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1015 let kind = kind.into();
1016 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1017 if let Some(projection_name) = projection.as_deref() {
1018 if !self.graph_projection_is_declared(projection_name) {
1019 return Err(format!(
1020 "graph projection '{projection_name}' is not declared in physical metadata"
1021 )
1022 .into());
1023 }
1024 if !self.graph_projection_is_operational(projection_name) {
1025 return Err(format!(
1026 "graph projection '{projection_name}' is declared but not operationally materialized"
1027 )
1028 .into());
1029 }
1030 }
1031
1032 self.update_physical_metadata(|metadata| {
1033 let now = SystemTime::now()
1034 .duration_since(UNIX_EPOCH)
1035 .unwrap_or_default()
1036 .as_millis();
1037
1038 let existing = metadata
1039 .analytics_jobs
1040 .iter_mut()
1041 .find(|job| job.id == job_id)?;
1042 existing.state = "running".to_string();
1043 existing.kind = kind.clone();
1044 existing.projection = projection.clone();
1045 existing.updated_at_unix_ms = now;
1046 existing.metadata = metadata_entries.clone();
1047 let job = existing.clone();
1048
1049 metadata
1050 .analytics_jobs
1051 .sort_by(|left, right| left.id.cmp(&right.id));
1052 Some(job)
1053 })
1054 .and_then(|job| {
1055 job.ok_or_else(|| {
1056 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1057 })
1058 })
1059 }
1060
1061 pub fn fail_analytics_job(
1063 &self,
1064 kind: impl Into<String>,
1065 projection: Option<String>,
1066 metadata_entries: BTreeMap<String, String>,
1067 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1068 let kind = kind.into();
1069 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1070
1071 self.update_physical_metadata(|metadata| {
1072 let now = SystemTime::now()
1073 .duration_since(UNIX_EPOCH)
1074 .unwrap_or_default()
1075 .as_millis();
1076
1077 let existing = metadata
1078 .analytics_jobs
1079 .iter_mut()
1080 .find(|job| job.id == job_id)?;
1081 existing.state = "failed".to_string();
1082 existing.kind = kind.clone();
1083 existing.projection = projection.clone();
1084 existing.updated_at_unix_ms = now;
1085 existing.metadata = metadata_entries.clone();
1086 let job = existing.clone();
1087
1088 metadata
1089 .analytics_jobs
1090 .sort_by(|left, right| left.id.cmp(&right.id));
1091 Some(job)
1092 })
1093 .and_then(|job| {
1094 job.ok_or_else(|| {
1095 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1096 })
1097 })
1098 }
1099
1100 pub fn mark_analytics_job_stale(
1102 &self,
1103 kind: impl Into<String>,
1104 projection: Option<String>,
1105 metadata_entries: BTreeMap<String, String>,
1106 ) -> Result<PhysicalAnalyticsJob, Box<dyn std::error::Error>> {
1107 let kind = kind.into();
1108 let job_id = Self::analytics_job_id(&kind, projection.as_deref());
1109
1110 self.update_physical_metadata(|metadata| {
1111 let now = SystemTime::now()
1112 .duration_since(UNIX_EPOCH)
1113 .unwrap_or_default()
1114 .as_millis();
1115
1116 let existing = metadata
1117 .analytics_jobs
1118 .iter_mut()
1119 .find(|job| job.id == job_id)?;
1120 existing.state = "stale".to_string();
1121 existing.kind = kind.clone();
1122 existing.projection = projection.clone();
1123 existing.updated_at_unix_ms = now;
1124 existing.metadata = metadata_entries.clone();
1125 let job = existing.clone();
1126
1127 metadata
1128 .analytics_jobs
1129 .sort_by(|left, right| left.id.cmp(&right.id));
1130 Some(job)
1131 })
1132 .and_then(|job| {
1133 job.ok_or_else(|| {
1134 format!("analytics job '{job_id}' is not declared in physical metadata").into()
1135 })
1136 })
1137 }
1138
1139 pub fn create_named_export(
1141 &self,
1142 name: impl Into<String>,
1143 ) -> Result<ExportDescriptor, Box<dyn std::error::Error>> {
1144 let name = name.into();
1145 if self.options.mode != StorageMode::Persistent {
1146 return Err("exports require persistent mode".into());
1147 }
1148 let Some(path) = self.path() else {
1149 return Err("database path is not available".into());
1150 };
1151
1152 self.flush()?;
1153
1154 let _publication = self.store.vault_publication_lock.lock();
1155 let mut metadata = match self.load_or_bootstrap_physical_metadata(true) {
1156 Ok(metadata) => metadata,
1157 Err(_)
1158 if self.options.storage_profile.deploy_profile
1159 == crate::storage::DeployProfile::Embedded
1160 && self.options.storage_profile.packaging
1161 == crate::storage::StoragePackaging::SingleFile =>
1162 {
1163 crate::physical::PhysicalMetadataFile::from_state(
1164 self.options.clone(),
1165 self.catalog_snapshot(),
1166 self.physical_collection_roots(),
1167 self.physical_index_state(),
1168 None,
1169 )
1170 }
1171 Err(err) => return Err(err),
1172 };
1173 let export_data_path = reddb_file::copy_physical_export_data_file(path, &name)?;
1174 let export_metadata_path = PhysicalMetadataFile::metadata_path_for(&export_data_path);
1175 let export_metadata_binary_path =
1176 PhysicalMetadataFile::metadata_binary_path_for(&export_data_path);
1177
1178 let descriptor = ExportDescriptor {
1179 name: name.clone(),
1180 created_at_unix_ms: SystemTime::now()
1181 .duration_since(UNIX_EPOCH)
1182 .unwrap_or_default()
1183 .as_millis(),
1184 snapshot_id: metadata
1185 .snapshots
1186 .last()
1187 .map(|snapshot| snapshot.snapshot_id),
1188 superblock_sequence: metadata.superblock.sequence,
1189 data_path: export_data_path.display().to_string(),
1190 metadata_path: export_metadata_path.display().to_string(),
1191 collection_count: metadata.catalog.total_collections,
1192 total_entities: metadata.catalog.total_entities,
1193 };
1194
1195 metadata
1196 .exports
1197 .retain(|export| export.name != descriptor.name);
1198 metadata.exports.push(descriptor.clone());
1199 self.prune_export_registry(&mut metadata.exports);
1200 metadata.save_for_data_path(path)?;
1201 metadata.save_to_binary_path(&export_metadata_binary_path)?;
1202 metadata.save_to_path(&export_metadata_path)?;
1203
1204 Ok(descriptor)
1205 }
1206
1207 pub fn set_index_enabled(
1209 &self,
1210 name: &str,
1211 enabled: bool,
1212 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1213 let Some(status) = self.index_status(name) else {
1214 return Err(format!("index '{name}' is not present in catalog status").into());
1215 };
1216 if !status.declared {
1217 return Err(format!("index '{name}' is not declared in physical metadata").into());
1218 }
1219 self.update_physical_metadata(|metadata| {
1220 let now = SystemTime::now()
1221 .duration_since(UNIX_EPOCH)
1222 .unwrap_or_default()
1223 .as_millis();
1224 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1225 index.enabled = enabled;
1226 if !enabled {
1227 index.build_state = "disabled".to_string();
1228 } else if index.build_state == "disabled" {
1229 index.build_state = if index.artifact_root_page.is_some() {
1230 "ready".to_string()
1231 } else {
1232 "declared-unbuilt".to_string()
1233 };
1234 }
1235 index.last_refresh_ms = Some(now);
1236 return Some(index.clone());
1237 }
1238 None
1239 })
1240 }
1241
1242 pub fn mark_index_building(
1244 &self,
1245 name: &str,
1246 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1247 let Some(status) = self.index_status(name) else {
1248 return Err(format!("index '{name}' is not present in catalog status").into());
1249 };
1250 if !status.declared {
1251 return Err(format!("index '{name}' is not declared in physical metadata").into());
1252 }
1253 if status.lifecycle_state == "disabled" {
1254 return Err(format!("index '{name}' is disabled").into());
1255 }
1256 self.update_physical_metadata(|metadata| {
1257 let now = SystemTime::now()
1258 .duration_since(UNIX_EPOCH)
1259 .unwrap_or_default()
1260 .as_millis();
1261 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1262 index.build_state = "building".to_string();
1263 index.last_refresh_ms = Some(now);
1264 return Some(index.clone());
1265 }
1266 None
1267 })
1268 }
1269
1270 pub fn fail_index(
1272 &self,
1273 name: &str,
1274 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1275 let Some(status) = self.index_status(name) else {
1276 return Err(format!("index '{name}' is not present in catalog status").into());
1277 };
1278 if !status.declared {
1279 return Err(format!("index '{name}' is not declared in physical metadata").into());
1280 }
1281 self.update_physical_metadata(|metadata| {
1282 let now = SystemTime::now()
1283 .duration_since(UNIX_EPOCH)
1284 .unwrap_or_default()
1285 .as_millis();
1286 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1287 index.build_state = "failed".to_string();
1288 index.last_refresh_ms = Some(now);
1289 return Some(index.clone());
1290 }
1291 None
1292 })
1293 }
1294
1295 pub fn mark_index_stale(
1297 &self,
1298 name: &str,
1299 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1300 let Some(status) = self.index_status(name) else {
1301 return Err(format!("index '{name}' is not present in catalog status").into());
1302 };
1303 if !status.declared {
1304 return Err(format!("index '{name}' is not declared in physical metadata").into());
1305 }
1306 self.update_physical_metadata(|metadata| {
1307 let now = SystemTime::now()
1308 .duration_since(UNIX_EPOCH)
1309 .unwrap_or_default()
1310 .as_millis();
1311 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1312 index.build_state = if index.enabled {
1313 "stale".to_string()
1314 } else {
1315 "disabled".to_string()
1316 };
1317 index.last_refresh_ms = Some(now);
1318 return Some(index.clone());
1319 }
1320 None
1321 })
1322 }
1323
1324 pub fn mark_index_ready(
1326 &self,
1327 name: &str,
1328 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1329 self.warmup_index(name)
1330 }
1331
1332 pub fn warmup_index(
1334 &self,
1335 name: &str,
1336 ) -> Result<Option<PhysicalIndexState>, Box<dyn std::error::Error>> {
1337 let Some(status) = self.index_status(name) else {
1338 return Err(format!("index '{name}' is not present in catalog status").into());
1339 };
1340 if !status.declared {
1341 return Err(format!("index '{name}' is not declared in physical metadata").into());
1342 }
1343 if status.lifecycle_state == "disabled" {
1344 return Err(format!("index '{name}' is disabled").into());
1345 }
1346 if !status.operational {
1347 return Err(
1348 format!("index '{name}' is declared but not operationally materialized").into(),
1349 );
1350 }
1351 let warmed_artifact = self
1352 .physical_indexes()
1353 .into_iter()
1354 .find(|index| index.name == name)
1355 .map(|mut index| {
1356 self.warmup_native_vector_artifact_for_index(&index)?;
1357 self.apply_runtime_native_artifact_to_index_state(&mut index)?;
1358 Ok::<_, String>(index)
1359 })
1360 .transpose()
1361 .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1362
1363 self.update_physical_metadata(|metadata| {
1364 let now = SystemTime::now()
1365 .duration_since(UNIX_EPOCH)
1366 .unwrap_or_default()
1367 .as_millis();
1368 if let Some(index) = metadata.indexes.iter_mut().find(|index| index.name == name) {
1369 if let Some(warmed) = warmed_artifact.as_ref() {
1370 index.entries = warmed.entries;
1371 index.estimated_memory_bytes = warmed.estimated_memory_bytes;
1372 index.backend = warmed.backend.clone();
1373 index.build_state = "ready".to_string();
1374 }
1375 index.last_refresh_ms = Some(now);
1376 return Some(index.clone());
1377 }
1378 None
1379 })
1380 }
1381
1382 pub fn rebuild_index_registry(
1384 &self,
1385 collection: Option<&str>,
1386 ) -> Result<Vec<PhysicalIndexState>, Box<dyn std::error::Error>> {
1387 let fresh = self.reconcile_index_states_with_native_artifacts(self.physical_index_state());
1388 self.update_physical_metadata(|metadata| {
1389 let now = SystemTime::now()
1390 .duration_since(UNIX_EPOCH)
1391 .unwrap_or_default()
1392 .as_millis();
1393
1394 let mut affected = Vec::new();
1395 let declared = metadata.indexes.clone();
1396 for declared_index in declared {
1397 let matches_collection = collection.is_none_or(|collection_name| {
1398 declared_index.collection.as_deref() == Some(collection_name)
1399 });
1400 if !matches_collection {
1401 continue;
1402 }
1403
1404 let mut rebuilt = fresh
1405 .iter()
1406 .find(|index| index.name == declared_index.name)
1407 .cloned()
1408 .unwrap_or_else(|| {
1409 let mut index = declared_index.clone();
1410 index.build_state = "declared-unbuilt".to_string();
1411 index
1412 });
1413 rebuilt.enabled = declared_index.enabled;
1414 rebuilt.artifact_kind = rebuilt
1415 .artifact_kind
1416 .or_else(|| declared_index.artifact_kind.clone());
1417 rebuilt.artifact_root_page = rebuilt
1418 .artifact_root_page
1419 .or(declared_index.artifact_root_page);
1420 rebuilt.artifact_checksum = rebuilt
1421 .artifact_checksum
1422 .or(declared_index.artifact_checksum);
1423 rebuilt.build_state =
1424 Self::finalize_rebuilt_index_build_state(&declared_index, &rebuilt);
1425 rebuilt.last_refresh_ms = Some(now);
1426
1427 if let Some(existing) = metadata
1428 .indexes
1429 .iter_mut()
1430 .find(|index| index.name == rebuilt.name)
1431 {
1432 *existing = rebuilt.clone();
1433 } else {
1434 metadata.indexes.push(rebuilt.clone());
1435 }
1436
1437 affected.push(rebuilt);
1438 }
1439
1440 affected
1441 })
1442 }
1443
1444 fn finalize_rebuilt_index_build_state(
1445 declared: &PhysicalIndexState,
1446 rebuilt: &PhysicalIndexState,
1447 ) -> String {
1448 if !rebuilt.enabled {
1449 return "disabled".to_string();
1450 }
1451
1452 if declared.build_state == "failed" || rebuilt.build_state == "failed" {
1453 return "failed".to_string();
1454 }
1455
1456 let native_artifact_family = Self::native_artifact_kind_for_index(rebuilt.kind).is_some();
1457 if native_artifact_family {
1458 if rebuilt.artifact_root_page.is_some() && rebuilt.artifact_checksum.is_some() {
1459 return "ready".to_string();
1460 }
1461 if declared.artifact_root_page.is_some()
1462 || declared.artifact_checksum.is_some()
1463 || declared.artifact_kind.is_some()
1464 {
1465 return "stale".to_string();
1466 }
1467 return "declared-unbuilt".to_string();
1468 }
1469
1470 if rebuilt.entries > 0 {
1471 return "ready".to_string();
1472 }
1473
1474 if matches!(
1475 declared.build_state.as_str(),
1476 "stale" | "artifact-published" | "registry-loaded"
1477 ) {
1478 return "stale".to_string();
1479 }
1480
1481 "declared-unbuilt".to_string()
1482 }
1483}