1use super::*;
2
3impl RedDB {
4 pub fn quorum_coordinator(
11 &self,
12 ) -> Option<&Arc<crate::replication::quorum::QuorumCoordinator>> {
13 self.quorum.as_ref()
14 }
15
16 pub fn wait_for_replication_quorum(
27 &self,
28 target_lsn: u64,
29 ) -> Result<(), crate::replication::QuorumError> {
30 match &self.quorum {
31 Some(q) => q.wait_for_quorum(target_lsn),
32 None => Ok(()),
33 }
34 }
35}
36
37impl RedDB {
38 pub(crate) fn native_registry_summary_from_metadata(
39 &self,
40 metadata: &PhysicalMetadataFile,
41 artifact_summaries: &[NativeVectorArtifactSummary],
42 artifact_count: usize,
43 ) -> NativeRegistrySummary {
44 const SAMPLE_LIMIT: usize = 32;
45
46 let collection_names: Vec<_> = metadata
47 .catalog
48 .stats_by_collection
49 .keys()
50 .take(SAMPLE_LIMIT)
51 .cloned()
52 .collect();
53 let indexes: Vec<_> = metadata
54 .indexes
55 .iter()
56 .take(SAMPLE_LIMIT)
57 .map(|index| NativeRegistryIndexSummary {
58 name: index.name.clone(),
59 kind: index.kind.as_str().to_string(),
60 collection: index.collection.clone(),
61 enabled: index.enabled,
62 entries: index.entries as u64,
63 estimated_memory_bytes: index.estimated_memory_bytes,
64 last_refresh_ms: index.last_refresh_ms,
65 backend: index.backend.clone(),
66 })
67 .collect();
68 let graph_projections: Vec<_> = metadata
69 .graph_projections
70 .iter()
71 .take(SAMPLE_LIMIT)
72 .map(|projection| NativeRegistryProjectionSummary {
73 name: projection.name.clone(),
74 source: projection.source.clone(),
75 created_at_unix_ms: projection.created_at_unix_ms,
76 updated_at_unix_ms: projection.updated_at_unix_ms,
77 node_labels: projection.node_labels.clone(),
78 node_types: projection.node_types.clone(),
79 edge_labels: projection.edge_labels.clone(),
80 last_materialized_sequence: projection.last_materialized_sequence,
81 })
82 .collect();
83 let analytics_jobs = metadata
84 .analytics_jobs
85 .iter()
86 .take(SAMPLE_LIMIT)
87 .map(|job| NativeRegistryJobSummary {
88 id: job.id.clone(),
89 kind: job.kind.clone(),
90 projection: job.projection.clone(),
91 state: job.state.clone(),
92 created_at_unix_ms: job.created_at_unix_ms,
93 updated_at_unix_ms: job.updated_at_unix_ms,
94 last_run_sequence: job.last_run_sequence,
95 metadata: job.metadata.clone(),
96 })
97 .collect::<Vec<_>>();
98 let vector_artifacts = artifact_summaries
99 .iter()
100 .take(SAMPLE_LIMIT)
101 .cloned()
102 .collect::<Vec<_>>();
103 let vector_artifact_count = artifact_count as u32;
104
105 NativeRegistrySummary {
106 collection_count: metadata.catalog.total_collections as u32,
107 index_count: metadata.indexes.len() as u32,
108 graph_projection_count: metadata.graph_projections.len() as u32,
109 analytics_job_count: metadata.analytics_jobs.len() as u32,
110 vector_artifact_count,
111 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
112 indexes_complete: metadata.indexes.len() <= SAMPLE_LIMIT,
113 graph_projections_complete: metadata.graph_projections.len() <= SAMPLE_LIMIT,
114 analytics_jobs_complete: metadata.analytics_jobs.len() <= SAMPLE_LIMIT,
115 vector_artifacts_complete: vector_artifact_count as usize <= SAMPLE_LIMIT,
116 omitted_collection_count: metadata
117 .catalog
118 .stats_by_collection
119 .len()
120 .saturating_sub(collection_names.len())
121 as u32,
122 omitted_index_count: metadata.indexes.len().saturating_sub(indexes.len()) as u32,
123 omitted_graph_projection_count: metadata
124 .graph_projections
125 .len()
126 .saturating_sub(graph_projections.len())
127 as u32,
128 omitted_analytics_job_count: metadata
129 .analytics_jobs
130 .len()
131 .saturating_sub(analytics_jobs.len())
132 as u32,
133 omitted_vector_artifact_count: vector_artifact_count
134 .saturating_sub(vector_artifacts.len() as u32),
135 collection_names,
136 indexes,
137 graph_projections,
138 analytics_jobs,
139 vector_artifacts,
140 }
141 }
142
143 pub(crate) fn native_vector_artifact_records(
144 &self,
145 ) -> Vec<(NativeVectorArtifactSummary, Vec<u8>)> {
146 let mut artifacts = Vec::new();
147 for collection in self.store.list_collections() {
148 let Some(manager) = self.store.get_collection(&collection) else {
149 continue;
150 };
151 let entities = manager.query_all(|_| true);
152 let mut vectors = Vec::new();
153 let mut graph_edges = Vec::new();
154 let mut fulltext_documents = Vec::new();
155 let mut document_records = Vec::new();
156 for entity in entities {
157 match entity.data {
158 EntityData::Vector(vector) => {
159 if !vector.dense.is_empty() {
160 vectors.push((entity.id, vector.dense));
161 }
162 }
163 EntityData::Edge(edge) => {
164 if let EntityKind::GraphEdge(edge_kind) = entity.kind {
165 graph_edges.push((
166 entity.id,
167 edge_kind.from_node,
168 edge_kind.to_node,
169 edge_kind.label,
170 edge.weight,
171 ));
172 }
173 }
174 data => {
175 let text = Self::native_fulltext_text_for_entity(&data);
176 if !text.trim().is_empty() {
177 fulltext_documents.push((entity.id, text));
178 }
179 if let Some(document) =
180 Self::native_document_pathvalue_for_entity(entity.id, &data)
181 {
182 document_records.push(document);
183 }
184 }
185 }
186 }
187 if !vectors.is_empty() {
188 let dimension = vectors[0].1.len();
189 let mut hnsw = HnswIndex::with_dimension(dimension);
190 for (id, vector) in vectors
191 .into_iter()
192 .filter(|(_, vector)| vector.len() == dimension)
193 {
194 hnsw.insert_with_id(id.raw(), vector);
195 }
196 let stats = hnsw.stats();
197 let bytes = hnsw.to_bytes();
198 let summary = NativeVectorArtifactSummary {
199 collection: collection.clone(),
200 artifact_kind: "hnsw".to_string(),
201 vector_count: stats.node_count as u64,
202 dimension: stats.dimension as u32,
203 max_layer: stats.max_layer as u32,
204 serialized_bytes: bytes.len() as u64,
205 checksum: crate::storage::engine::crc32(&bytes) as u64,
206 };
207 artifacts.push((summary, bytes));
208
209 let n_lists = ((stats.node_count as f64).sqrt().ceil() as usize).max(1);
210 let mut ivf = IvfIndex::new(IvfConfig::new(dimension, n_lists));
211 let training = manager
212 .query_all(|_| true)
213 .into_iter()
214 .filter_map(|entity| match entity.data {
215 EntityData::Vector(vector) if vector.dense.len() == dimension => {
216 Some(vector.dense)
217 }
218 _ => None,
219 })
220 .collect::<Vec<_>>();
221 ivf.train(&training);
222 let items = manager
223 .query_all(|_| true)
224 .into_iter()
225 .filter_map(|entity| match entity.data {
226 EntityData::Vector(vector) if vector.dense.len() == dimension => {
227 Some((entity.id.raw(), vector.dense))
228 }
229 _ => None,
230 })
231 .collect::<Vec<_>>();
232 ivf.add_batch_with_ids(items);
233 let ivf_stats = ivf.stats();
234 let ivf_bytes = ivf.to_bytes();
235 let ivf_summary = NativeVectorArtifactSummary {
236 collection: collection.clone(),
237 artifact_kind: "ivf".to_string(),
238 vector_count: ivf_stats.total_vectors as u64,
239 dimension: ivf_stats.dimension as u32,
240 max_layer: ivf_stats.n_lists as u32,
241 serialized_bytes: ivf_bytes.len() as u64,
242 checksum: crate::storage::engine::crc32(&ivf_bytes) as u64,
243 };
244 artifacts.push((ivf_summary, ivf_bytes));
245 }
246
247 if !graph_edges.is_empty() {
248 let bytes = Self::serialize_native_graph_adjacency_artifact(&graph_edges);
249 let (edge_count, node_count, label_count) =
250 Self::inspect_native_graph_adjacency_artifact(&bytes).unwrap_or((0, 0, 0));
251 let summary = NativeVectorArtifactSummary {
252 collection: collection.clone(),
253 artifact_kind: "graph.adjacency".to_string(),
254 vector_count: edge_count,
255 dimension: node_count as u32,
256 max_layer: label_count,
257 serialized_bytes: bytes.len() as u64,
258 checksum: crate::storage::engine::crc32(&bytes) as u64,
259 };
260 artifacts.push((summary, bytes));
261 }
262
263 if !fulltext_documents.is_empty() {
264 let bytes =
265 Self::serialize_native_fulltext_artifact(&collection, &fulltext_documents);
266 let (doc_count, term_count, posting_count) =
267 Self::inspect_native_fulltext_artifact(&bytes).unwrap_or((0, 0, 0));
268 let summary = NativeVectorArtifactSummary {
269 collection: collection.clone(),
270 artifact_kind: "text.fulltext".to_string(),
271 vector_count: posting_count,
272 dimension: doc_count as u32,
273 max_layer: term_count as u32,
274 serialized_bytes: bytes.len() as u64,
275 checksum: crate::storage::engine::crc32(&bytes) as u64,
276 };
277 artifacts.push((summary, bytes));
278 }
279
280 if !document_records.is_empty() {
281 let bytes = Self::serialize_native_document_pathvalue_artifact(
282 &collection,
283 &document_records,
284 );
285 let (doc_count, path_count, value_count, unique_value_count) =
286 Self::inspect_native_document_pathvalue_artifact(&bytes)
287 .unwrap_or((0, 0, 0, 0));
288 let _ = unique_value_count;
289 let summary = NativeVectorArtifactSummary {
290 collection: collection.clone(),
291 artifact_kind: "document.pathvalue".to_string(),
292 vector_count: value_count,
293 dimension: doc_count as u32,
294 max_layer: path_count as u32,
295 serialized_bytes: bytes.len() as u64,
296 checksum: crate::storage::engine::crc32(&bytes) as u64,
297 };
298 artifacts.push((summary, bytes));
299 }
300 }
301 artifacts
302 }
303
304 fn serialize_native_graph_adjacency_artifact(
305 edges: &[(EntityId, String, String, String, f32)],
306 ) -> Vec<u8> {
307 let edges: Vec<reddb_file::NativeGraphEdge> = edges
308 .iter()
309 .map(
310 |(edge_id, from_node, to_node, label, weight)| reddb_file::NativeGraphEdge {
311 edge_id: edge_id.raw(),
312 from_node: from_node.clone(),
313 to_node: to_node.clone(),
314 label: label.clone(),
315 weight: *weight,
316 },
317 )
318 .collect();
319 reddb_file::encode_native_graph_adjacency_frame(&reddb_file::NativeGraphAdjacencyFrame {
320 edges,
321 })
322 }
323
324 pub(crate) fn inspect_native_graph_adjacency_artifact(
325 bytes: &[u8],
326 ) -> Result<(u64, u64, u32), String> {
327 let frame =
328 reddb_file::decode_native_graph_adjacency_frame(bytes).map_err(|e| e.to_string())?;
329 let edge_count = frame.edges.len() as u64;
330 let mut nodes = BTreeSet::new();
331 let mut labels = BTreeSet::new();
332 for edge in frame.edges {
333 nodes.insert(edge.from_node);
334 nodes.insert(edge.to_node);
335 labels.insert(edge.label);
336 }
337 Ok((edge_count, nodes.len() as u64, labels.len() as u32))
338 }
339
340 fn serialize_native_fulltext_artifact(
341 collection: &str,
342 documents: &[(EntityId, String)],
343 ) -> Vec<u8> {
344 let mut postings: BTreeMap<String, Vec<(u64, u32)>> = BTreeMap::new();
345 for (entity_id, text) in documents {
346 let mut frequencies: BTreeMap<String, u32> = BTreeMap::new();
347 for token in Self::native_fulltext_tokenize(text) {
348 *frequencies.entry(token).or_insert(0) += 1;
349 }
350 for (token, count) in frequencies {
351 postings
352 .entry(token)
353 .or_default()
354 .push((entity_id.raw(), count));
355 }
356 }
357
358 let terms = postings
359 .into_iter()
360 .map(|(term, postings)| reddb_file::NativeFulltextTerm {
361 term,
362 postings: postings
363 .into_iter()
364 .map(
365 |(entity_id, term_count)| reddb_file::NativeFulltextPosting {
366 entity_id,
367 term_count,
368 },
369 )
370 .collect(),
371 })
372 .collect();
373 reddb_file::encode_native_fulltext_frame(&reddb_file::NativeFulltextFrame {
374 collection: collection.to_string(),
375 doc_count: documents.len() as u32,
376 terms,
377 })
378 }
379
380 pub(crate) fn inspect_native_fulltext_artifact(
381 bytes: &[u8],
382 ) -> Result<(u64, u64, u64), String> {
383 let frame = reddb_file::decode_native_fulltext_frame(bytes).map_err(|e| e.to_string())?;
384 let posting_count: u64 = frame
385 .terms
386 .iter()
387 .map(|term| term.postings.len() as u64)
388 .sum();
389 Ok((
390 frame.doc_count as u64,
391 frame.terms.len() as u64,
392 posting_count,
393 ))
394 }
395
396 fn serialize_native_document_pathvalue_artifact(
397 collection: &str,
398 documents: &[(EntityId, Vec<(String, String)>)],
399 ) -> Vec<u8> {
400 let docs: Vec<reddb_file::NativeDocPathValue> = documents
401 .iter()
402 .map(|(entity_id, entries)| reddb_file::NativeDocPathValue {
403 entity_id: entity_id.raw(),
404 entries: entries
405 .iter()
406 .map(|(path, value)| reddb_file::NativeDocPathValueEntry {
407 path: path.clone(),
408 value: value.clone(),
409 })
410 .collect(),
411 })
412 .collect();
413 reddb_file::encode_native_doc_pathvalue_frame(&reddb_file::NativeDocPathValueFrame {
414 collection: collection.to_string(),
415 docs,
416 })
417 }
418
419 pub(crate) fn inspect_native_document_pathvalue_artifact(
420 bytes: &[u8],
421 ) -> Result<(u64, u64, u64, u64), String> {
422 let frame =
423 reddb_file::decode_native_doc_pathvalue_frame(bytes).map_err(|e| e.to_string())?;
424 let doc_count = frame.docs.len() as u64;
425 let mut paths = BTreeSet::new();
426 let mut values = BTreeSet::new();
427 let mut total_entries = 0u64;
428 for doc in frame.docs {
429 for entry in doc.entries {
430 paths.insert(entry.path);
431 values.insert(entry.value);
432 total_entries += 1;
433 }
434 }
435 Ok((
436 doc_count,
437 paths.len() as u64,
438 total_entries,
439 values.len() as u64,
440 ))
441 }
442
443 fn native_document_pathvalue_for_entity(
444 entity_id: EntityId,
445 data: &EntityData,
446 ) -> Option<(EntityId, Vec<(String, String)>)> {
447 let mut entries = Vec::new();
448 match data {
449 EntityData::Row(row) => {
450 if let Some(named) = &row.named {
451 for (key, value) in named {
452 Self::collect_native_document_entries_from_value(key, value, &mut entries);
453 }
454 }
455 for (idx, value) in row.columns.iter().enumerate() {
456 let path = format!("columns[{idx}]");
457 Self::collect_native_document_entries_from_value(&path, value, &mut entries);
458 }
459 }
460 EntityData::Node(node) => {
461 for (key, value) in &node.properties {
462 Self::collect_native_document_entries_from_value(key, value, &mut entries);
463 }
464 }
465 EntityData::Edge(edge) => {
466 for (key, value) in &edge.properties {
467 Self::collect_native_document_entries_from_value(key, value, &mut entries);
468 }
469 }
470 EntityData::Vector(_) => {}
471 EntityData::TimeSeries(_) => {}
472 EntityData::QueueMessage(_) => {}
473 }
474 if entries.is_empty() {
475 None
476 } else {
477 Some((entity_id, entries))
478 }
479 }
480
481 fn collect_native_document_entries_from_value(
482 path: &str,
483 value: &Value,
484 out: &mut Vec<(String, String)>,
485 ) {
486 match value {
487 Value::Json(bytes) | Value::Blob(bytes) => {
488 if let Ok(json) = crate::json::from_slice::<JsonValue>(bytes) {
489 Self::collect_native_document_entries_from_json(path, &json, out);
490 }
491 }
492 _ => {}
493 }
494 }
495
496 fn collect_native_document_entries_from_json(
497 path: &str,
498 value: &JsonValue,
499 out: &mut Vec<(String, String)>,
500 ) {
501 match value {
502 JsonValue::Object(entries) => {
503 for (key, value) in entries {
504 let next = if path.is_empty() {
505 key.clone()
506 } else {
507 format!("{path}.{key}")
508 };
509 Self::collect_native_document_entries_from_json(&next, value, out);
510 }
511 }
512 JsonValue::Array(items) => {
513 for (idx, value) in items.iter().enumerate() {
514 let next = format!("{path}[{idx}]");
515 Self::collect_native_document_entries_from_json(&next, value, out);
516 }
517 }
518 _ => {
519 if let Some(text) = Self::native_json_scalar_text(value) {
520 out.push((path.to_string(), text));
521 }
522 }
523 }
524 }
525
526 fn native_json_scalar_text(value: &JsonValue) -> Option<String> {
527 match value {
528 JsonValue::Null => None,
529 JsonValue::Bool(value) => Some(value.to_string()),
530 JsonValue::Integer(value) => Some(value.to_string()),
531 JsonValue::Number(value) => Some(value.to_string()),
532 JsonValue::Decimal(value) => Some(value.clone()),
533 JsonValue::String(value) => Some(value.clone()),
534 JsonValue::Array(_) | JsonValue::Object(_) => None,
535 }
536 }
537
538 fn native_fulltext_text_for_entity(data: &EntityData) -> String {
539 match data {
540 EntityData::Row(row) => {
541 let mut parts = Vec::new();
542 if let Some(named) = &row.named {
543 for value in named.values() {
544 if let Some(text) = Self::native_value_text(value) {
545 parts.push(text);
546 }
547 }
548 }
549 for value in &row.columns {
550 if let Some(text) = Self::native_value_text(value) {
551 parts.push(text);
552 }
553 }
554 parts.join(" ")
555 }
556 EntityData::Node(node) => node
557 .properties
558 .values()
559 .filter_map(Self::native_value_text)
560 .collect::<Vec<_>>()
561 .join(" "),
562 EntityData::Edge(edge) => edge
563 .properties
564 .values()
565 .filter_map(Self::native_value_text)
566 .collect::<Vec<_>>()
567 .join(" "),
568 EntityData::Vector(vector) => vector.content.clone().unwrap_or_default(),
569 EntityData::TimeSeries(ts) => ts.metric.clone(),
570 EntityData::QueueMessage(_) => String::new(),
571 }
572 }
573
574 fn native_value_text(value: &Value) -> Option<String> {
575 match value {
576 Value::Text(value) => Some(value.to_string()),
577 Value::Json(value) => String::from_utf8(value.clone()).ok(),
578 Value::Blob(value) => String::from_utf8(value.clone()).ok(),
579 Value::Integer(value) => Some(value.to_string()),
580 Value::UnsignedInteger(value) => Some(value.to_string()),
581 Value::Float(value) => Some(value.to_string()),
582 Value::Boolean(value) => Some(value.to_string()),
583 Value::IpAddr(value) => Some(value.to_string()),
584 Value::NodeRef(value) => Some(value.clone()),
585 Value::EdgeRef(value) => Some(value.clone()),
586 Value::RowRef(table, row_id) => Some(format!("{table}:{row_id}")),
587 Value::VectorRef(collection, vector_id) => Some(format!("{collection}:{vector_id}")),
588 Value::Timestamp(value) => Some(value.to_string()),
589 Value::Duration(value) => Some(value.to_string()),
590 Value::Uuid(_) | Value::MacAddr(_) | Value::Vector(_) | Value::Null => None,
591 Value::Color([r, g, b]) => Some(format!("#{:02X}{:02X}{:02X}", r, g, b)),
592 Value::Email(s) => Some(s.clone()),
593 Value::Url(s) => Some(s.clone()),
594 Value::Phone(n) => Some(format!("+{}", n)),
595 Value::Semver(packed) => Some(format!(
596 "{}.{}.{}",
597 packed / 1_000_000,
598 (packed / 1_000) % 1_000,
599 packed % 1_000
600 )),
601 Value::Cidr(ip, prefix) => Some(format!(
602 "{}.{}.{}.{}/{}",
603 (ip >> 24) & 0xFF,
604 (ip >> 16) & 0xFF,
605 (ip >> 8) & 0xFF,
606 ip & 0xFF,
607 prefix
608 )),
609 Value::Date(days) => Some(days.to_string()),
610 Value::Time(ms) => {
611 let total_secs = ms / 1000;
612 Some(format!(
613 "{:02}:{:02}:{:02}",
614 total_secs / 3600,
615 (total_secs / 60) % 60,
616 total_secs % 60
617 ))
618 }
619 Value::Decimal(v) => Some(Value::Decimal(*v).display_string()),
620 Value::DecimalText(v) => Some(v.clone()),
621 Value::EnumValue(i) => Some(format!("enum({})", i)),
622 Value::Array(_) => None,
623 Value::TimestampMs(ms) => Some(ms.to_string()),
624 Value::Ipv4(ip) => Some(format!(
625 "{}.{}.{}.{}",
626 (ip >> 24) & 0xFF,
627 (ip >> 16) & 0xFF,
628 (ip >> 8) & 0xFF,
629 ip & 0xFF
630 )),
631 Value::Ipv6(bytes) => Some(format!("{}", std::net::Ipv6Addr::from(*bytes))),
632 Value::Subnet(ip, mask) => {
633 let prefix = mask.leading_ones();
634 Some(format!(
635 "{}.{}.{}.{}/{}",
636 (ip >> 24) & 0xFF,
637 (ip >> 16) & 0xFF,
638 (ip >> 8) & 0xFF,
639 ip & 0xFF,
640 prefix
641 ))
642 }
643 Value::Port(p) => Some(p.to_string()),
644 Value::Latitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
645 Value::Longitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
646 Value::GeoPoint(lat, lon) => Some(format!(
647 "{:.6},{:.6}",
648 *lat as f64 / 1_000_000.0,
649 *lon as f64 / 1_000_000.0
650 )),
651 Value::Country2(c) => Some(String::from_utf8_lossy(c).to_string()),
652 Value::Country3(c) => Some(String::from_utf8_lossy(c).to_string()),
653 Value::Lang2(c) => Some(String::from_utf8_lossy(c).to_string()),
654 Value::Lang5(c) => Some(String::from_utf8_lossy(c).to_string()),
655 Value::Currency(c) => Some(String::from_utf8_lossy(c).to_string()),
656 Value::AssetCode(code) => Some(code.clone()),
657 Value::Money { .. } => Some(value.display_string()),
658 Value::ColorAlpha([r, g, b, a]) => {
659 Some(format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a))
660 }
661 Value::BigInt(v) => Some(v.to_string()),
662 Value::KeyRef(col, key) => Some(format!("{}:{}", col, key)),
663 Value::DocRef(col, id) => Some(format!("{}#{}", col, id)),
664 Value::TableRef(name) => Some(name.clone()),
665 Value::PageRef(page_id) => Some(format!("page:{}", page_id)),
666 Value::Secret(_) | Value::Password(_) => None,
667 }
668 }
669
670 fn native_fulltext_tokenize(text: &str) -> Vec<String> {
671 text.to_lowercase()
672 .split(|c: char| !c.is_alphanumeric())
673 .filter(|s| s.len() >= 2)
674 .map(|s| s.to_string())
675 .collect()
676 }
677
678 pub(crate) fn native_recovery_summary_from_metadata(
679 metadata: &PhysicalMetadataFile,
680 ) -> NativeRecoverySummary {
681 const SAMPLE_LIMIT: usize = 16;
682
683 let snapshots: Vec<_> = metadata
684 .snapshots
685 .iter()
686 .rev()
687 .take(SAMPLE_LIMIT)
688 .map(|snapshot| NativeSnapshotSummary {
689 snapshot_id: snapshot.snapshot_id,
690 created_at_unix_ms: snapshot.created_at_unix_ms,
691 superblock_sequence: snapshot.superblock_sequence,
692 collection_count: snapshot.collection_count as u32,
693 total_entities: snapshot.total_entities as u64,
694 })
695 .collect();
696 let exports: Vec<_> = metadata
697 .exports
698 .iter()
699 .rev()
700 .take(SAMPLE_LIMIT)
701 .map(|export| NativeExportSummary {
702 name: export.name.clone(),
703 created_at_unix_ms: export.created_at_unix_ms,
704 snapshot_id: export.snapshot_id,
705 superblock_sequence: export.superblock_sequence,
706 collection_count: export.collection_count as u32,
707 total_entities: export.total_entities as u64,
708 })
709 .collect();
710
711 NativeRecoverySummary {
712 snapshot_count: metadata.snapshots.len() as u32,
713 export_count: metadata.exports.len() as u32,
714 snapshots_complete: metadata.snapshots.len() <= SAMPLE_LIMIT,
715 exports_complete: metadata.exports.len() <= SAMPLE_LIMIT,
716 omitted_snapshot_count: metadata.snapshots.len().saturating_sub(snapshots.len()) as u32,
717 omitted_export_count: metadata.exports.len().saturating_sub(exports.len()) as u32,
718 snapshots,
719 exports,
720 }
721 }
722
723 pub(crate) fn native_catalog_summary_from_metadata(
724 metadata: &PhysicalMetadataFile,
725 ) -> NativeCatalogSummary {
726 const SAMPLE_LIMIT: usize = 32;
727
728 let collections: Vec<_> = metadata
729 .catalog
730 .stats_by_collection
731 .iter()
732 .take(SAMPLE_LIMIT)
733 .map(|(name, stats)| NativeCatalogCollectionSummary {
734 name: name.clone(),
735 entities: stats.entities as u64,
736 cross_refs: stats.cross_refs as u64,
737 segments: stats.segments as u32,
738 })
739 .collect();
740
741 NativeCatalogSummary {
742 collection_count: metadata.catalog.total_collections as u32,
743 total_entities: metadata.catalog.total_entities as u64,
744 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
745 omitted_collection_count: metadata
746 .catalog
747 .stats_by_collection
748 .len()
749 .saturating_sub(collections.len()) as u32,
750 collections,
751 }
752 }
753
754 pub(crate) fn native_metadata_state_summary_from_metadata(
755 metadata: &PhysicalMetadataFile,
756 ) -> NativeMetadataStateSummary {
757 NativeMetadataStateSummary {
758 protocol_version: metadata.protocol_version.clone(),
759 generated_at_unix_ms: metadata.generated_at_unix_ms,
760 last_loaded_from: metadata.last_loaded_from.clone(),
761 last_healed_at_unix_ms: metadata.last_healed_at_unix_ms,
762 }
763 }
764
765 pub(crate) fn inspect_native_header_against_metadata(
766 native: PhysicalFileHeader,
767 metadata: &PhysicalMetadataFile,
768 ) -> NativeHeaderInspection {
769 let expected = Self::native_header_from_metadata(metadata);
770 let mut mismatches = Vec::new();
771
772 if native.format_version != expected.format_version {
773 mismatches.push(NativeHeaderMismatch {
774 field: "format_version",
775 native: native.format_version.to_string(),
776 expected: expected.format_version.to_string(),
777 });
778 }
779 if native.sequence != expected.sequence {
780 mismatches.push(NativeHeaderMismatch {
781 field: "sequence",
782 native: native.sequence.to_string(),
783 expected: expected.sequence.to_string(),
784 });
785 }
786 if native.manifest_oldest_root != expected.manifest_oldest_root {
787 mismatches.push(NativeHeaderMismatch {
788 field: "manifest_oldest_root",
789 native: native.manifest_oldest_root.to_string(),
790 expected: expected.manifest_oldest_root.to_string(),
791 });
792 }
793 if native.manifest_root != expected.manifest_root {
794 mismatches.push(NativeHeaderMismatch {
795 field: "manifest_root",
796 native: native.manifest_root.to_string(),
797 expected: expected.manifest_root.to_string(),
798 });
799 }
800 if native.free_set_root != expected.free_set_root {
801 mismatches.push(NativeHeaderMismatch {
802 field: "free_set_root",
803 native: native.free_set_root.to_string(),
804 expected: expected.free_set_root.to_string(),
805 });
806 }
807 if native.collection_root_count != expected.collection_root_count {
808 mismatches.push(NativeHeaderMismatch {
809 field: "collection_root_count",
810 native: native.collection_root_count.to_string(),
811 expected: expected.collection_root_count.to_string(),
812 });
813 }
814 if native.snapshot_count != expected.snapshot_count {
815 mismatches.push(NativeHeaderMismatch {
816 field: "snapshot_count",
817 native: native.snapshot_count.to_string(),
818 expected: expected.snapshot_count.to_string(),
819 });
820 }
821 if native.index_count != expected.index_count {
822 mismatches.push(NativeHeaderMismatch {
823 field: "index_count",
824 native: native.index_count.to_string(),
825 expected: expected.index_count.to_string(),
826 });
827 }
828 if native.catalog_collection_count != expected.catalog_collection_count {
829 mismatches.push(NativeHeaderMismatch {
830 field: "catalog_collection_count",
831 native: native.catalog_collection_count.to_string(),
832 expected: expected.catalog_collection_count.to_string(),
833 });
834 }
835 if native.catalog_total_entities != expected.catalog_total_entities {
836 mismatches.push(NativeHeaderMismatch {
837 field: "catalog_total_entities",
838 native: native.catalog_total_entities.to_string(),
839 expected: expected.catalog_total_entities.to_string(),
840 });
841 }
842 if native.export_count != expected.export_count {
843 mismatches.push(NativeHeaderMismatch {
844 field: "export_count",
845 native: native.export_count.to_string(),
846 expected: expected.export_count.to_string(),
847 });
848 }
849 if native.graph_projection_count != expected.graph_projection_count {
850 mismatches.push(NativeHeaderMismatch {
851 field: "graph_projection_count",
852 native: native.graph_projection_count.to_string(),
853 expected: expected.graph_projection_count.to_string(),
854 });
855 }
856 if native.analytics_job_count != expected.analytics_job_count {
857 mismatches.push(NativeHeaderMismatch {
858 field: "analytics_job_count",
859 native: native.analytics_job_count.to_string(),
860 expected: expected.analytics_job_count.to_string(),
861 });
862 }
863 if native.manifest_event_count != expected.manifest_event_count {
864 mismatches.push(NativeHeaderMismatch {
865 field: "manifest_event_count",
866 native: native.manifest_event_count.to_string(),
867 expected: expected.manifest_event_count.to_string(),
868 });
869 }
870
871 NativeHeaderInspection {
872 native,
873 expected,
874 consistent: mismatches.is_empty(),
875 mismatches,
876 }
877 }
878
879 pub(crate) fn repair_policy_for_inspection(
880 inspection: &NativeHeaderInspection,
881 ) -> NativeHeaderRepairPolicy {
882 if inspection.consistent {
883 return NativeHeaderRepairPolicy::InSync;
884 }
885
886 if inspection.expected.sequence >= inspection.native.sequence {
887 NativeHeaderRepairPolicy::RepairNativeFromMetadata
888 } else {
889 NativeHeaderRepairPolicy::NativeAheadOfMetadata
890 }
891 }
892
893 pub(crate) fn prune_export_registry(&self, exports: &mut Vec<ExportDescriptor>) {
894 let retention = self.options.export_retention.max(1);
895 if exports.len() <= retention {
896 return;
897 }
898
899 exports.sort_by_key(|export| export.created_at_unix_ms);
900 let removed: Vec<ExportDescriptor> =
901 exports.drain(0..(exports.len() - retention)).collect();
902
903 for export in removed {
904 let _ = fs::remove_file(&export.data_path);
905 let _ = fs::remove_file(&export.metadata_path);
906 let binary_path = PhysicalMetadataFile::metadata_binary_path_for(std::path::Path::new(
907 &export.data_path,
908 ));
909 let _ = fs::remove_file(binary_path);
910 }
911 }
912
913 pub(crate) fn runtime_index_catalog(&self) -> IndexCatalog {
914 let mut catalog = IndexCatalog::register_default_vector_graph(
915 self.options.has_capability(Capability::Table),
916 self.options.has_capability(Capability::Graph),
917 );
918 if self.options.has_capability(Capability::FullText) {
919 catalog.register(RuntimeIndexConfig::new(
920 "text-fulltext",
921 IndexKind::FullText,
922 ));
923 catalog.register(RuntimeIndexConfig::new(
924 "document-pathvalue",
925 IndexKind::DocumentPathValue,
926 ));
927 }
928 catalog.register(RuntimeIndexConfig::new(
929 "search-hybrid",
930 IndexKind::HybridSearch,
931 ));
932 catalog
933 }
934
935 pub(crate) fn physical_index_state(&self) -> Vec<PhysicalIndexState> {
936 let catalog = self.runtime_index_catalog();
940 let snapshot = crate::catalog::snapshot_store_with_declarations(
941 "reddb",
942 self.store.as_ref(),
943 Some(&catalog),
944 None, None, );
947 let mut metrics_by_name = std::collections::BTreeMap::new();
948 for metric in &snapshot.indices {
949 metrics_by_name.insert(metric.name.clone(), metric.clone());
950 }
951
952 let mut states = Vec::new();
953 for collection in snapshot.collections {
954 for index_name in &collection.indices {
955 let metric = metrics_by_name.get(index_name);
956 let kind = metric
957 .map(|metric| metric.kind)
958 .unwrap_or_else(|| infer_collection_index_kind(collection.model, index_name));
959 let entries = estimate_index_entries(&collection, kind);
960 states.push(PhysicalIndexState {
961 name: format!("{}::{}", collection.name, index_name),
962 kind,
963 collection: Some(collection.name.clone()),
964 enabled: metric.map(|metric| metric.enabled).unwrap_or(true),
965 entries,
966 estimated_memory_bytes: estimate_index_memory(entries, kind),
967 last_refresh_ms: metric.and_then(|metric| metric.last_refresh_ms),
968 backend: index_backend_name(kind).to_string(),
969 artifact_kind: None,
970 artifact_root_page: None,
971 artifact_checksum: None,
972 build_state: "catalog-derived".to_string(),
973 });
974 }
975 }
976
977 states
978 }
979
980 pub(crate) fn physical_collection_roots(&self) -> BTreeMap<String, u64> {
981 let mut roots = BTreeMap::new();
982
983 for name in self.store.list_collections() {
984 let Some(manager) = self.store.get_collection(&name) else {
985 continue;
986 };
987
988 let stats = manager.stats();
989 let mut root = fnv1a_seed();
990 fnv1a_hash_value(&mut root, &name);
991 fnv1a_hash_value(&mut root, &stats.total_entities);
992 fnv1a_hash_value(&mut root, &stats.growing_count);
993 fnv1a_hash_value(&mut root, &stats.sealed_count);
994 fnv1a_hash_value(&mut root, &stats.archived_count);
995 fnv1a_hash_value(&mut root, &stats.total_memory_bytes);
996 fnv1a_hash_value(&mut root, &stats.seal_ops);
997 fnv1a_hash_value(&mut root, &stats.consolidation.runs_completed);
998
999 let root = manager.cached_physical_root(root, || {
1000 manager.for_each_entity_in_id_order(|entity| {
1001 fnv1a_hash_value(&mut root, &entity.id.raw());
1002 fnv1a_hash_value(&mut root, &entity.kind);
1003 fnv1a_hash_value(&mut root, &entity.created_at);
1004 fnv1a_hash_value(&mut root, &entity.updated_at);
1005 fnv1a_hash_value(&mut root, &entity.data);
1006 fnv1a_hash_value(&mut root, &entity.sequence_id);
1007 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1008 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1009 true
1010 });
1011 root
1012 });
1013
1014 roots.insert(name, root);
1015 }
1016
1017 roots
1018 }
1019
1020 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1026 TableRef::new(table, row_id)
1027 }
1028
1029 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1031 NodeRef::new(collection, node_id)
1032 }
1033
1034 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1036 VectorRef::new(collection, vector_id)
1037 }
1038
1039 pub fn query(&self) -> QueryBuilder {
1045 QueryBuilder::new(self.store.clone())
1046 }
1047
1048 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1055 if self.store.get_collection(collection).is_none() {
1056 return Vec::new();
1057 }
1058
1059 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1061 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1062 let results = hnsw.search(vector, k);
1063 let mapped = self.hnsw_results_to_similar(collection, &results);
1064 if !mapped.is_empty() {
1065 return mapped;
1066 }
1067 }
1068
1069 self.similar_brute_force(collection, vector, k)
1071 }
1072
1073 fn similar_brute_force(
1075 &self,
1076 collection: &str,
1077 vector: &[f32],
1078 k: usize,
1079 ) -> Vec<SimilarResult> {
1080 let manager = match self.store.get_collection(collection) {
1081 Some(m) => m,
1082 None => return Vec::new(),
1083 };
1084
1085 let entities = manager.query_all(|_| true);
1086 let mut results: Vec<SimilarResult> = entities
1087 .iter()
1088 .filter_map(|e| {
1089 let score = match &e.data {
1090 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1091 _ => e
1092 .embeddings()
1093 .iter()
1094 .map(|emb| cosine_similarity(vector, &emb.vector))
1095 .fold(0.0f32, f32::max),
1096 };
1097 let distance = (1.0 - score).max(0.0);
1098 if score > 0.0 {
1099 Some(SimilarResult {
1100 entity_id: e.id,
1101 score,
1102 distance,
1103 entity: e.clone(),
1104 })
1105 } else {
1106 None
1107 }
1108 })
1109 .collect();
1110
1111 results.sort_by(|a, b| {
1112 b.score
1113 .partial_cmp(&a.score)
1114 .unwrap_or(std::cmp::Ordering::Equal)
1115 });
1116 results.truncate(k);
1117 results
1118 }
1119
1120 fn get_or_build_hnsw_index(
1130 &self,
1131 collection: &str,
1132 query_dim: usize,
1133 ) -> Option<Arc<RwLock<HnswIndex>>> {
1134 let manager = self.store.get_collection(collection)?;
1135 let live_count = manager.count();
1136
1137 {
1139 let indexes = self
1140 .vector_indexes
1141 .read()
1142 .unwrap_or_else(|e| e.into_inner());
1143 if let Some(cached) = indexes.get(collection) {
1144 if cached.entity_count == live_count {
1145 return Some(Arc::clone(&cached.index));
1146 }
1147 }
1148 }
1149
1150 let entities = manager.query_all(|_| true);
1152
1153 let vectors: Vec<(u64, Vec<f32>)> = entities
1154 .iter()
1155 .filter_map(|e| match &e.data {
1156 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1157 Some((e.id.raw(), v.dense.clone()))
1158 }
1159 _ => None,
1160 })
1161 .collect();
1162
1163 const MIN_VECTORS_FOR_HNSW: usize = 100;
1165 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1166 return None;
1167 }
1168
1169 let config = crate::storage::engine::HnswConfig::with_m(16)
1171 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1172 .with_ef_construction(100)
1173 .with_ef_search(50);
1174 let mut hnsw = HnswIndex::new(query_dim, config);
1175
1176 for (id, vec) in &vectors {
1177 hnsw.insert_with_id(*id, vec.clone());
1178 }
1179
1180 let index = Arc::new(RwLock::new(hnsw));
1181
1182 let mut indexes = self
1184 .vector_indexes
1185 .write()
1186 .unwrap_or_else(|e| e.into_inner());
1187 if let Some(cached) = indexes.get(collection) {
1189 if cached.entity_count == live_count {
1190 return Some(Arc::clone(&cached.index));
1191 }
1192 }
1193 indexes.insert(
1194 collection.to_string(),
1195 CachedVectorIndex {
1196 index: Arc::clone(&index),
1197 entity_count: live_count,
1198 },
1199 );
1200 Some(index)
1201 }
1202
1203 fn hnsw_results_to_similar(
1205 &self,
1206 collection: &str,
1207 results: &[crate::storage::engine::DistanceResult],
1208 ) -> Vec<SimilarResult> {
1209 results
1210 .iter()
1211 .filter_map(|dr| {
1212 let entity_id = EntityId::new(dr.id);
1213 let entity = self.store.get(collection, entity_id)?;
1214 let score = (1.0 - dr.distance).max(0.0);
1216 if score > 0.0 {
1217 Some(SimilarResult {
1218 entity_id,
1219 score,
1220 distance: dr.distance,
1221 entity,
1222 })
1223 } else {
1224 None
1225 }
1226 })
1227 .collect()
1228 }
1229
1230 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1235 let mut indexes = self
1236 .vector_indexes
1237 .write()
1238 .unwrap_or_else(|e| e.into_inner());
1239 indexes.remove(collection);
1240 }
1241
1242 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1244 self.store.get_any(id).map(|(_, e)| e)
1245 }
1246
1247 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1249 self.store.get_any(id)
1250 }
1251
1252 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1258 ids.iter().map(|id| self.get(*id)).collect()
1259 }
1260
1261 pub fn batch(&self) -> BatchBuilder {
1263 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1264 }
1265
1266 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1272 let mut preprocessors = self
1273 .preprocessors
1274 .write()
1275 .unwrap_or_else(|poisoned| poisoned.into_inner());
1276 preprocessors.push(Arc::from(preprocessor));
1277 }
1278
1279 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1285 self.store
1286 .get_refs_from(id)
1287 .into_iter()
1288 .filter_map(|(target_id, ref_type, collection)| {
1289 self.store
1290 .get(&collection, target_id)
1291 .map(|entity| LinkedEntity {
1292 entity,
1293 ref_type,
1294 collection,
1295 })
1296 })
1297 .collect()
1298 }
1299
1300 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1302 self.store
1303 .get_refs_to(id)
1304 .into_iter()
1305 .filter_map(|(source_id, ref_type, collection)| {
1306 self.store
1307 .get(&collection, source_id)
1308 .map(|entity| LinkedEntity {
1309 entity,
1310 ref_type,
1311 collection,
1312 })
1313 })
1314 .collect()
1315 }
1316
1317 pub fn store(&self) -> Arc<UnifiedStore> {
1319 self.store.clone()
1320 }
1321
1322 pub(crate) fn turbo_collections(
1328 &self,
1329 ) -> &Arc<
1330 parking_lot::Mutex<
1331 std::collections::HashMap<
1332 String,
1333 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1334 >,
1335 >,
1336 > {
1337 self.turbo_collections
1338 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1339 }
1340
1341 pub(crate) fn turbo_state(
1351 &self,
1352 collection: &str,
1353 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1354 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1355 return None;
1356 }
1357 let map = self.turbo_collections();
1358 {
1359 let guard = map.lock();
1360 if let Some(state) = guard.get(collection) {
1361 return Some(Arc::clone(state));
1362 }
1363 }
1364 let contract = self.collection_contract(collection)?;
1365 let dim = contract.vector_dimension?;
1366 let metric = contract
1367 .vector_metric
1368 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1369 let state = Arc::new(
1370 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1371 dim,
1372 metric,
1373 self.store.pager(),
1374 ),
1375 );
1376 if let Some((_, paths)) = self.options.resolve_tiered_layout() {
1381 state.set_snapshot_path(paths.turbo_snapshot_path(collection));
1382 }
1383 let mut guard = map.lock();
1384 let inserted_now = !guard.contains_key(collection);
1385 let entry = guard
1386 .entry(collection.to_string())
1387 .or_insert_with(|| Arc::clone(&state));
1388 let handle = Arc::clone(entry);
1389 drop(guard);
1390 if inserted_now {
1397 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1398 Arc::clone(&self.store),
1399 collection.to_string(),
1400 Arc::clone(&handle),
1401 );
1402 self.turbo_rebuild_workers.lock().push(join);
1403 }
1404 Some(handle)
1405 }
1406
1407 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1413 self.ml_runtime.get_or_init(|| {
1414 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1415 |_handle| Ok(String::new()),
1419 ))
1420 })
1421 }
1422
1423 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1427 self.semantic_cache.get_or_init(|| {
1428 Arc::new(crate::storage::ml::SemanticCache::new(
1429 crate::storage::ml::SemanticCacheConfig::default(),
1430 ))
1431 })
1432 }
1433
1434 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1437 self.hypertables
1438 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1439 }
1440
1441 pub fn continuous_aggregates(
1444 &self,
1445 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1446 self.continuous_aggregates.get_or_init(|| {
1447 Arc::new(
1448 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1449 )
1450 })
1451 }
1452
1453 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1454 let mut file = File::open(path)?;
1455 let mut magic = [0u8; 4];
1456 let read = file.read(&mut magic)?;
1457 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1458 }
1459}