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