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