1use super::*;
2
3pub(crate) struct NativeArtifactBuild {
4 pub records: Vec<(NativeVectorArtifactSummary, Vec<u8>)>,
5 pub deferred_collections: std::collections::HashSet<String>,
6}
7
8impl RedDB {
9 pub fn quorum_coordinator(
16 &self,
17 ) -> Option<&Arc<crate::replication::quorum::QuorumCoordinator>> {
18 self.quorum.as_ref()
19 }
20
21 pub fn wait_for_replication_quorum(
32 &self,
33 target_lsn: u64,
34 ) -> Result<(), crate::replication::QuorumError> {
35 match &self.quorum {
36 Some(q) => q.wait_for_quorum(target_lsn),
37 None => Ok(()),
38 }
39 }
40}
41
42impl RedDB {
43 pub(crate) fn native_registry_summary_from_metadata(
44 &self,
45 metadata: &PhysicalMetadataFile,
46 artifact_summaries: &[NativeVectorArtifactSummary],
47 artifact_count: usize,
48 ) -> NativeRegistrySummary {
49 const SAMPLE_LIMIT: usize = 32;
50
51 let collection_names: Vec<_> = metadata
52 .catalog
53 .stats_by_collection
54 .keys()
55 .take(SAMPLE_LIMIT)
56 .cloned()
57 .collect();
58 let indexes: Vec<_> = metadata
59 .indexes
60 .iter()
61 .take(SAMPLE_LIMIT)
62 .map(|index| NativeRegistryIndexSummary {
63 name: index.name.clone(),
64 kind: index.kind.as_str().to_string(),
65 collection: index.collection.clone(),
66 enabled: index.enabled,
67 entries: index.entries as u64,
68 estimated_memory_bytes: index.estimated_memory_bytes,
69 last_refresh_ms: index.last_refresh_ms,
70 backend: index.backend.clone(),
71 })
72 .collect();
73 let graph_projections: Vec<_> = metadata
74 .graph_projections
75 .iter()
76 .take(SAMPLE_LIMIT)
77 .map(|projection| NativeRegistryProjectionSummary {
78 name: projection.name.clone(),
79 source: projection.source.clone(),
80 created_at_unix_ms: projection.created_at_unix_ms,
81 updated_at_unix_ms: projection.updated_at_unix_ms,
82 node_labels: projection.node_labels.clone(),
83 node_types: projection.node_types.clone(),
84 edge_labels: projection.edge_labels.clone(),
85 last_materialized_sequence: projection.last_materialized_sequence,
86 })
87 .collect();
88 let analytics_jobs = metadata
89 .analytics_jobs
90 .iter()
91 .take(SAMPLE_LIMIT)
92 .map(|job| NativeRegistryJobSummary {
93 id: job.id.clone(),
94 kind: job.kind.clone(),
95 projection: job.projection.clone(),
96 state: job.state.clone(),
97 created_at_unix_ms: job.created_at_unix_ms,
98 updated_at_unix_ms: job.updated_at_unix_ms,
99 last_run_sequence: job.last_run_sequence,
100 metadata: job.metadata.clone(),
101 })
102 .collect::<Vec<_>>();
103 let vector_artifacts = artifact_summaries
104 .iter()
105 .take(SAMPLE_LIMIT)
106 .cloned()
107 .collect::<Vec<_>>();
108 let vector_artifact_count = artifact_count as u32;
109
110 NativeRegistrySummary {
111 collection_count: metadata.catalog.total_collections as u32,
112 index_count: metadata.indexes.len() as u32,
113 graph_projection_count: metadata.graph_projections.len() as u32,
114 analytics_job_count: metadata.analytics_jobs.len() as u32,
115 vector_artifact_count,
116 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
117 indexes_complete: metadata.indexes.len() <= SAMPLE_LIMIT,
118 graph_projections_complete: metadata.graph_projections.len() <= SAMPLE_LIMIT,
119 analytics_jobs_complete: metadata.analytics_jobs.len() <= SAMPLE_LIMIT,
120 vector_artifacts_complete: vector_artifact_count as usize <= SAMPLE_LIMIT,
121 omitted_collection_count: metadata
122 .catalog
123 .stats_by_collection
124 .len()
125 .saturating_sub(collection_names.len())
126 as u32,
127 omitted_index_count: metadata.indexes.len().saturating_sub(indexes.len()) as u32,
128 omitted_graph_projection_count: metadata
129 .graph_projections
130 .len()
131 .saturating_sub(graph_projections.len())
132 as u32,
133 omitted_analytics_job_count: metadata
134 .analytics_jobs
135 .len()
136 .saturating_sub(analytics_jobs.len())
137 as u32,
138 omitted_vector_artifact_count: vector_artifact_count
139 .saturating_sub(vector_artifacts.len() as u32),
140 collection_names,
141 indexes,
142 graph_projections,
143 analytics_jobs,
144 vector_artifacts,
145 }
146 }
147
148 pub(crate) fn native_vector_artifact_records(
149 &self,
150 ) -> Result<NativeArtifactBuild, Box<dyn std::error::Error>> {
151 let budget = crate::storage::memory_budget::resolve_for_boot(
152 self.options.storage_profile.deploy_profile,
153 self.options.memory_budget_bytes,
154 )?
155 .resolved_bytes;
156 let source_limit = usize::try_from((budget / 256).clamp(1024, 2 * 1024 * 1024))
160 .expect("bounded source allowance");
161 let output_limit = usize::try_from((budget / 128).min(8 * 1024 * 1024))
162 .expect("bounded artifact allowance");
163 let mut artifacts = Vec::new();
164 let mut retained_bytes = 0usize;
165 let mut deferred_collections = std::collections::HashSet::new();
166 let mut collections = self.store.list_collections();
167 collections.sort_unstable();
168 'collections: for collection in collections {
169 let Some(manager) = self.store.get_collection(&collection) else {
170 continue;
171 };
172 if manager.count() == 0 {
173 continue;
174 }
175 let mut entities = Vec::new();
176 let mut source_bytes = 0usize;
177 let mut exceeded = false;
178 let column_schema = manager.column_schema();
179 manager.for_each_entity(|entity| {
180 let eligible = match &entity.data {
181 EntityData::Vector(vector) => {
182 self.options.has_capability(Capability::Vector) && !vector.dense.is_empty()
183 }
184 EntityData::Edge(_) => self.options.has_capability(Capability::Graph),
185 _ => self.options.has_capability(Capability::FullText),
186 };
187 if !eligible {
188 return true;
189 }
190 let bytes = reddb_engine::storage::memory_size::entity_bytes(entity);
191 if bytes > source_limit.saturating_sub(source_bytes)
192 || retained_bytes >= output_limit
193 {
194 exceeded = true;
195 return false;
196 }
197 source_bytes += bytes;
198 let mut owned = entity.clone();
199 if let EntityData::Row(row) = &mut owned.data {
200 if row.named.is_none() && row.schema.is_none() {
201 row.schema = column_schema.clone();
202 }
203 }
204 entities.push(owned);
205 true
206 });
207 if exceeded {
208 deferred_collections.insert(collection);
209 continue;
210 }
211 if entities.is_empty() {
212 continue;
213 }
214 let first_artifact = artifacts.len();
215 let mut vectors = Vec::new();
216 let mut graph_edges = Vec::new();
217 let mut fulltext_documents = Vec::new();
218 let mut document_records = Vec::new();
219 let mut document_bytes = 0usize;
220 for entity in entities {
221 match entity.data {
222 EntityData::Vector(vector) => {
223 if !vector.dense.is_empty() {
224 vectors.push((entity.id, vector.dense));
225 }
226 }
227 EntityData::Edge(edge) => {
228 if let EntityKind::GraphEdge(edge_kind) = entity.kind {
229 graph_edges.push((
230 entity.id,
231 edge_kind.from_node,
232 edge_kind.to_node,
233 edge_kind.label,
234 edge.weight,
235 ));
236 }
237 }
238 data => {
239 let text = Self::native_fulltext_text_for_entity(&data);
240 if !text.trim().is_empty() {
241 fulltext_documents.push((entity.id, text));
242 }
243 let document = match Self::native_document_pathvalue_for_entity(
244 entity.id,
245 &data,
246 source_limit.saturating_sub(document_bytes),
247 ) {
248 Ok(document) => document,
249 Err(()) => {
250 artifacts.truncate(first_artifact);
251 deferred_collections.insert(collection);
252 continue 'collections;
253 }
254 };
255 if let Some(document) = document {
256 document_bytes += document
257 .1
258 .iter()
259 .map(|(path, value)| {
260 path.len()
261 + value.len()
262 + std::mem::size_of::<(String, String)>()
263 })
264 .sum::<usize>();
265 document_records.push(document);
266 }
267 }
268 }
269 }
270 if !vectors.is_empty() {
271 let dimension = vectors[0].1.len();
272 let mut hnsw = HnswIndex::with_dimension(dimension);
273 for (id, vector) in vectors
274 .into_iter()
275 .filter(|(_, vector)| vector.len() == dimension)
276 {
277 hnsw.insert_with_id(id.raw(), vector);
278 }
279 let stats = hnsw.stats();
280 let bytes = hnsw.to_bytes();
281 let summary = NativeVectorArtifactSummary {
282 collection: collection.clone(),
283 artifact_kind: "hnsw".to_string(),
284 vector_count: stats.node_count as u64,
285 dimension: stats.dimension as u32,
286 max_layer: stats.max_layer as u32,
287 serialized_bytes: bytes.len() as u64,
288 checksum: crate::storage::engine::crc32(&bytes) as u64,
289 };
290 artifacts.push((summary, bytes));
291
292 let n_lists = ((stats.node_count as f64).sqrt().ceil() as usize).max(1);
293 let mut ivf = IvfIndex::new(IvfConfig::new(dimension, n_lists));
294 let training = manager
295 .query_all(|_| true)
296 .into_iter()
297 .filter_map(|entity| match entity.data {
298 EntityData::Vector(vector) if vector.dense.len() == dimension => {
299 Some(vector.dense)
300 }
301 _ => None,
302 })
303 .collect::<Vec<_>>();
304 ivf.train(&training);
305 let items = manager
306 .query_all(|_| true)
307 .into_iter()
308 .filter_map(|entity| match entity.data {
309 EntityData::Vector(vector) if vector.dense.len() == dimension => {
310 Some((entity.id.raw(), vector.dense))
311 }
312 _ => None,
313 })
314 .collect::<Vec<_>>();
315 ivf.add_batch_with_ids(items);
316 let ivf_stats = ivf.stats();
317 let ivf_bytes = ivf.to_bytes();
318 let ivf_summary = NativeVectorArtifactSummary {
319 collection: collection.clone(),
320 artifact_kind: "ivf".to_string(),
321 vector_count: ivf_stats.total_vectors as u64,
322 dimension: ivf_stats.dimension as u32,
323 max_layer: ivf_stats.n_lists as u32,
324 serialized_bytes: ivf_bytes.len() as u64,
325 checksum: crate::storage::engine::crc32(&ivf_bytes) as u64,
326 };
327 artifacts.push((ivf_summary, ivf_bytes));
328 }
329
330 if !graph_edges.is_empty() {
331 let bytes = Self::serialize_native_graph_adjacency_artifact(&graph_edges);
332 let (edge_count, node_count, label_count) =
333 Self::inspect_native_graph_adjacency_artifact(&bytes).unwrap_or((0, 0, 0));
334 let summary = NativeVectorArtifactSummary {
335 collection: collection.clone(),
336 artifact_kind: "graph.adjacency".to_string(),
337 vector_count: edge_count,
338 dimension: node_count as u32,
339 max_layer: label_count,
340 serialized_bytes: bytes.len() as u64,
341 checksum: crate::storage::engine::crc32(&bytes) as u64,
342 };
343 artifacts.push((summary, bytes));
344 }
345
346 if !fulltext_documents.is_empty() {
347 let bytes =
348 Self::serialize_native_fulltext_artifact(&collection, &fulltext_documents);
349 let (doc_count, term_count, posting_count) =
350 Self::inspect_native_fulltext_artifact(&bytes).unwrap_or((0, 0, 0));
351 let summary = NativeVectorArtifactSummary {
352 collection: collection.clone(),
353 artifact_kind: "text.fulltext".to_string(),
354 vector_count: posting_count,
355 dimension: doc_count as u32,
356 max_layer: term_count as u32,
357 serialized_bytes: bytes.len() as u64,
358 checksum: crate::storage::engine::crc32(&bytes) as u64,
359 };
360 artifacts.push((summary, bytes));
361 }
362
363 if !document_records.is_empty() {
364 let bytes = Self::serialize_native_document_pathvalue_artifact(
365 &collection,
366 &document_records,
367 );
368 let (doc_count, path_count, value_count, unique_value_count) =
369 Self::inspect_native_document_pathvalue_artifact(&bytes)
370 .unwrap_or((0, 0, 0, 0));
371 let _ = unique_value_count;
372 let summary = NativeVectorArtifactSummary {
373 collection: collection.clone(),
374 artifact_kind: "document.pathvalue".to_string(),
375 vector_count: value_count,
376 dimension: doc_count as u32,
377 max_layer: path_count as u32,
378 serialized_bytes: bytes.len() as u64,
379 checksum: crate::storage::engine::crc32(&bytes) as u64,
380 };
381 artifacts.push((summary, bytes));
382 }
383 let bytes = artifacts[first_artifact..]
384 .iter()
385 .map(|(_, bytes)| bytes.len())
386 .sum::<usize>();
387 if bytes > output_limit.saturating_sub(retained_bytes) {
388 artifacts.truncate(first_artifact);
389 deferred_collections.insert(collection);
390 } else {
391 retained_bytes += bytes;
392 }
393 }
394 for collection in &deferred_collections {
395 tracing::warn!(collection, source_bytes_limit=source_limit, retained_bytes_limit=output_limit,
396 "native search artifact deferred by memory budget; canonical records remain available");
397 }
398 Ok(NativeArtifactBuild {
399 records: artifacts,
400 deferred_collections,
401 })
402 }
403
404 fn serialize_native_graph_adjacency_artifact(
405 edges: &[(EntityId, String, String, String, f32)],
406 ) -> Vec<u8> {
407 let edges: Vec<reddb_file::NativeGraphEdge> = edges
408 .iter()
409 .map(
410 |(edge_id, from_node, to_node, label, weight)| reddb_file::NativeGraphEdge {
411 edge_id: edge_id.raw(),
412 from_node: from_node.clone(),
413 to_node: to_node.clone(),
414 label: label.clone(),
415 weight: *weight,
416 },
417 )
418 .collect();
419 reddb_file::encode_native_graph_adjacency_frame(&reddb_file::NativeGraphAdjacencyFrame {
420 edges,
421 })
422 }
423
424 pub(crate) fn inspect_native_graph_adjacency_artifact(
425 bytes: &[u8],
426 ) -> Result<(u64, u64, u32), String> {
427 let frame =
428 reddb_file::decode_native_graph_adjacency_frame(bytes).map_err(|e| e.to_string())?;
429 let edge_count = frame.edges.len() as u64;
430 let mut nodes = BTreeSet::new();
431 let mut labels = BTreeSet::new();
432 for edge in frame.edges {
433 nodes.insert(edge.from_node);
434 nodes.insert(edge.to_node);
435 labels.insert(edge.label);
436 }
437 Ok((edge_count, nodes.len() as u64, labels.len() as u32))
438 }
439
440 fn serialize_native_fulltext_artifact(
441 collection: &str,
442 documents: &[(EntityId, String)],
443 ) -> Vec<u8> {
444 let mut postings: BTreeMap<String, Vec<(u64, u32)>> = BTreeMap::new();
445 for (entity_id, text) in documents {
446 let mut frequencies: BTreeMap<String, u32> = BTreeMap::new();
447 for token in Self::native_fulltext_tokenize(text) {
448 *frequencies.entry(token).or_insert(0) += 1;
449 }
450 for (token, count) in frequencies {
451 postings
452 .entry(token)
453 .or_default()
454 .push((entity_id.raw(), count));
455 }
456 }
457
458 let terms = postings
459 .into_iter()
460 .map(|(term, postings)| reddb_file::NativeFulltextTerm {
461 term,
462 postings: postings
463 .into_iter()
464 .map(
465 |(entity_id, term_count)| reddb_file::NativeFulltextPosting {
466 entity_id,
467 term_count,
468 },
469 )
470 .collect(),
471 })
472 .collect();
473 reddb_file::encode_native_fulltext_frame(&reddb_file::NativeFulltextFrame {
474 collection: collection.to_string(),
475 doc_count: documents.len() as u32,
476 terms,
477 })
478 }
479
480 pub(crate) fn inspect_native_fulltext_artifact(
481 bytes: &[u8],
482 ) -> Result<(u64, u64, u64), String> {
483 let frame = reddb_file::decode_native_fulltext_frame(bytes).map_err(|e| e.to_string())?;
484 let posting_count: u64 = frame
485 .terms
486 .iter()
487 .map(|term| term.postings.len() as u64)
488 .sum();
489 Ok((
490 frame.doc_count as u64,
491 frame.terms.len() as u64,
492 posting_count,
493 ))
494 }
495
496 fn serialize_native_document_pathvalue_artifact(
497 collection: &str,
498 documents: &[(EntityId, Vec<(String, String)>)],
499 ) -> Vec<u8> {
500 let docs: Vec<reddb_file::NativeDocPathValue> = documents
501 .iter()
502 .map(|(entity_id, entries)| reddb_file::NativeDocPathValue {
503 entity_id: entity_id.raw(),
504 entries: entries
505 .iter()
506 .map(|(path, value)| reddb_file::NativeDocPathValueEntry {
507 path: path.clone(),
508 value: value.clone(),
509 })
510 .collect(),
511 })
512 .collect();
513 reddb_file::encode_native_doc_pathvalue_frame(&reddb_file::NativeDocPathValueFrame {
514 collection: collection.to_string(),
515 docs,
516 })
517 }
518
519 pub(crate) fn inspect_native_document_pathvalue_artifact(
520 bytes: &[u8],
521 ) -> Result<(u64, u64, u64, u64), String> {
522 let frame =
523 reddb_file::decode_native_doc_pathvalue_frame(bytes).map_err(|e| e.to_string())?;
524 let doc_count = frame.docs.len() as u64;
525 let mut paths = BTreeSet::new();
526 let mut values = BTreeSet::new();
527 let mut total_entries = 0u64;
528 for doc in frame.docs {
529 for entry in doc.entries {
530 paths.insert(entry.path);
531 values.insert(entry.value);
532 total_entries += 1;
533 }
534 }
535 Ok((
536 doc_count,
537 paths.len() as u64,
538 total_entries,
539 values.len() as u64,
540 ))
541 }
542
543 fn native_document_pathvalue_for_entity(
544 entity_id: EntityId,
545 data: &EntityData,
546 limit: usize,
547 ) -> Result<Option<(EntityId, Vec<(String, String)>)>, ()> {
548 let mut entries = Vec::new();
549 let mut bytes = 0;
550 match data {
551 EntityData::Row(row) => {
552 if row.named.is_some() || row.schema.is_some() {
553 for (key, value) in row.iter_fields() {
554 Self::collect_native_document_entries_from_value(
555 key,
556 value,
557 &mut entries,
558 &mut bytes,
559 limit,
560 )?;
561 }
562 } else {
563 for (idx, value) in row.columns.iter().enumerate() {
564 let path = format!("columns[{idx}]");
565 Self::collect_native_document_entries_from_value(
566 &path,
567 value,
568 &mut entries,
569 &mut bytes,
570 limit,
571 )?;
572 }
573 }
574 }
575 EntityData::Node(node) => {
576 for (key, value) in &node.properties {
577 Self::collect_native_document_entries_from_value(
578 key,
579 value,
580 &mut entries,
581 &mut bytes,
582 limit,
583 )?;
584 }
585 }
586 EntityData::Edge(edge) => {
587 for (key, value) in &edge.properties {
588 Self::collect_native_document_entries_from_value(
589 key,
590 value,
591 &mut entries,
592 &mut bytes,
593 limit,
594 )?;
595 }
596 }
597 EntityData::Vector(_) => {}
598 EntityData::TimeSeries(_) => {}
599 EntityData::QueueMessage(_) => {}
600 }
601 if entries.is_empty() {
602 Ok(None)
603 } else {
604 Ok(Some((entity_id, entries)))
605 }
606 }
607
608 fn collect_native_document_entries_from_value(
609 path: &str,
610 value: &Value,
611 out: &mut Vec<(String, String)>,
612 bytes: &mut usize,
613 limit: usize,
614 ) -> Result<(), ()> {
615 match value {
616 Value::Json(payload) | Value::Blob(payload) => {
617 if let Ok(json) = crate::json::from_slice::<JsonValue>(payload) {
618 Self::collect_native_document_entries_from_json(
619 path, &json, out, bytes, limit,
620 )?;
621 }
622 }
623 _ => {}
624 }
625 Ok(())
626 }
627
628 fn collect_native_document_entries_from_json(
629 path: &str,
630 value: &JsonValue,
631 out: &mut Vec<(String, String)>,
632 bytes: &mut usize,
633 limit: usize,
634 ) -> Result<(), ()> {
635 let mut pending = vec![(path.to_string(), value)];
636 let mut pending_bytes = path.len() + std::mem::size_of::<(String, &JsonValue)>();
637 while let Some((path, value)) = pending.pop() {
638 pending_bytes -= path.len() + std::mem::size_of::<(String, &JsonValue)>();
639 match value {
640 JsonValue::Object(entries) => {
641 for (key, value) in entries.iter().rev() {
642 let next = if path.is_empty() {
643 key.clone()
644 } else {
645 format!("{path}.{key}")
646 };
647 let cost = next.len() + std::mem::size_of::<(String, &JsonValue)>();
648 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
649 return Err(());
650 }
651 pending_bytes += cost;
652 pending.push((next, value));
653 }
654 }
655 JsonValue::Array(items) => {
656 for (idx, value) in items.iter().enumerate().rev() {
657 let next = format!("{path}[{idx}]");
658 let cost = next.len() + std::mem::size_of::<(String, &JsonValue)>();
659 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
660 return Err(());
661 }
662 pending_bytes += cost;
663 pending.push((next, value));
664 }
665 }
666 _ => {
667 if let Some(text) = Self::native_json_scalar_text(value) {
668 let cost =
669 path.len() + text.len() + std::mem::size_of::<(String, String)>();
670 if cost > limit.saturating_sub(bytes.saturating_add(pending_bytes)) {
671 return Err(());
672 }
673 *bytes += cost;
674 out.push((path, text));
675 }
676 }
677 }
678 }
679 Ok(())
680 }
681
682 fn native_json_scalar_text(value: &JsonValue) -> Option<String> {
683 match value {
684 JsonValue::Null => None,
685 JsonValue::Bool(value) => Some(value.to_string()),
686 JsonValue::Integer(value) => Some(value.to_string()),
687 JsonValue::Number(value) => Some(value.to_string()),
688 JsonValue::Decimal(value) => Some(value.clone()),
689 JsonValue::String(value) => Some(value.clone()),
690 JsonValue::Array(_) | JsonValue::Object(_) => None,
691 }
692 }
693
694 fn native_fulltext_text_for_entity(data: &EntityData) -> String {
695 match data {
696 EntityData::Row(row) => {
697 let mut parts = Vec::new();
698 if let Some(named) = &row.named {
699 for value in named.values() {
700 if let Some(text) = Self::native_value_text(value) {
701 parts.push(text);
702 }
703 }
704 }
705 for value in &row.columns {
706 if let Some(text) = Self::native_value_text(value) {
707 parts.push(text);
708 }
709 }
710 parts.join(" ")
711 }
712 EntityData::Node(node) => node
713 .properties
714 .values()
715 .filter_map(Self::native_value_text)
716 .collect::<Vec<_>>()
717 .join(" "),
718 EntityData::Edge(edge) => edge
719 .properties
720 .values()
721 .filter_map(Self::native_value_text)
722 .collect::<Vec<_>>()
723 .join(" "),
724 EntityData::Vector(vector) => vector.content.clone().unwrap_or_default(),
725 EntityData::TimeSeries(ts) => ts.metric.clone(),
726 EntityData::QueueMessage(_) => String::new(),
727 }
728 }
729
730 fn native_value_text(value: &Value) -> Option<String> {
731 match value {
732 Value::Text(value) => Some(value.to_string()),
733 Value::Json(value) => String::from_utf8(value.clone()).ok(),
734 Value::Blob(value) => String::from_utf8(value.clone()).ok(),
735 Value::Integer(value) => Some(value.to_string()),
736 Value::UnsignedInteger(value) => Some(value.to_string()),
737 Value::Float(value) => Some(value.to_string()),
738 Value::Boolean(value) => Some(value.to_string()),
739 Value::IpAddr(value) => Some(value.to_string()),
740 Value::NodeRef(value) => Some(value.clone()),
741 Value::EdgeRef(value) => Some(value.clone()),
742 Value::RowRef(table, row_id) => Some(format!("{table}:{row_id}")),
743 Value::VectorRef(collection, vector_id) => Some(format!("{collection}:{vector_id}")),
744 Value::Timestamp(value) => Some(value.to_string()),
745 Value::Duration(value) => Some(value.to_string()),
746 Value::Uuid(_) | Value::MacAddr(_) | Value::Vector(_) | Value::Null => None,
747 Value::Color([r, g, b]) => Some(format!("#{:02X}{:02X}{:02X}", r, g, b)),
748 Value::Email(s) => Some(s.clone()),
749 Value::Url(s) => Some(s.clone()),
750 Value::Phone(n) => Some(format!("+{}", n)),
751 Value::Semver(packed) => Some(format!(
752 "{}.{}.{}",
753 packed / 1_000_000,
754 (packed / 1_000) % 1_000,
755 packed % 1_000
756 )),
757 Value::Cidr(ip, prefix) => Some(format!(
758 "{}.{}.{}.{}/{}",
759 (ip >> 24) & 0xFF,
760 (ip >> 16) & 0xFF,
761 (ip >> 8) & 0xFF,
762 ip & 0xFF,
763 prefix
764 )),
765 Value::Date(days) => Some(days.to_string()),
766 Value::Time(ms) => {
767 let total_secs = ms / 1000;
768 Some(format!(
769 "{:02}:{:02}:{:02}",
770 total_secs / 3600,
771 (total_secs / 60) % 60,
772 total_secs % 60
773 ))
774 }
775 Value::Decimal(v) => Some(Value::Decimal(*v).display_string()),
776 Value::DecimalText(v) => Some(v.clone()),
777 Value::EnumValue(i) => Some(format!("enum({})", i)),
778 Value::Array(_) => None,
779 Value::TimestampMs(ms) => Some(ms.to_string()),
780 Value::Ipv4(ip) => Some(format!(
781 "{}.{}.{}.{}",
782 (ip >> 24) & 0xFF,
783 (ip >> 16) & 0xFF,
784 (ip >> 8) & 0xFF,
785 ip & 0xFF
786 )),
787 Value::Ipv6(bytes) => Some(format!("{}", std::net::Ipv6Addr::from(*bytes))),
788 Value::Subnet(ip, mask) => {
789 let prefix = mask.leading_ones();
790 Some(format!(
791 "{}.{}.{}.{}/{}",
792 (ip >> 24) & 0xFF,
793 (ip >> 16) & 0xFF,
794 (ip >> 8) & 0xFF,
795 ip & 0xFF,
796 prefix
797 ))
798 }
799 Value::Port(p) => Some(p.to_string()),
800 Value::Latitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
801 Value::Longitude(micro) => Some(format!("{:.6}", *micro as f64 / 1_000_000.0)),
802 Value::GeoPoint(lat, lon) => Some(format!(
803 "{:.6},{:.6}",
804 *lat as f64 / 1_000_000.0,
805 *lon as f64 / 1_000_000.0
806 )),
807 Value::Country2(c) => Some(String::from_utf8_lossy(c).to_string()),
808 Value::Country3(c) => Some(String::from_utf8_lossy(c).to_string()),
809 Value::Lang2(c) => Some(String::from_utf8_lossy(c).to_string()),
810 Value::Lang5(c) => Some(String::from_utf8_lossy(c).to_string()),
811 Value::Currency(c) => Some(String::from_utf8_lossy(c).to_string()),
812 Value::AssetCode(code) => Some(code.clone()),
813 Value::Money { .. } => Some(value.display_string()),
814 Value::ColorAlpha([r, g, b, a]) => {
815 Some(format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a))
816 }
817 Value::BigInt(v) => Some(v.to_string()),
818 Value::KeyRef(col, key) => Some(format!("{}:{}", col, key)),
819 Value::DocRef(col, id) => Some(format!("{}#{}", col, id)),
820 Value::TableRef(name) => Some(name.clone()),
821 Value::PageRef(page_id) => Some(format!("page:{}", page_id)),
822 Value::Secret(_) | Value::Password(_) => None,
823 }
824 }
825
826 fn native_fulltext_tokenize(text: &str) -> Vec<String> {
827 text.to_lowercase()
828 .split(|c: char| !c.is_alphanumeric())
829 .filter(|s| s.len() >= 2)
830 .map(|s| s.to_string())
831 .collect()
832 }
833
834 pub(crate) fn native_recovery_summary_from_metadata(
835 metadata: &PhysicalMetadataFile,
836 ) -> NativeRecoverySummary {
837 const SAMPLE_LIMIT: usize = 16;
838
839 let snapshots: Vec<_> = metadata
840 .snapshots
841 .iter()
842 .rev()
843 .take(SAMPLE_LIMIT)
844 .map(|snapshot| NativeSnapshotSummary {
845 snapshot_id: snapshot.snapshot_id,
846 created_at_unix_ms: snapshot.created_at_unix_ms,
847 superblock_sequence: snapshot.superblock_sequence,
848 collection_count: snapshot.collection_count as u32,
849 total_entities: snapshot.total_entities as u64,
850 })
851 .collect();
852 let exports: Vec<_> = metadata
853 .exports
854 .iter()
855 .rev()
856 .take(SAMPLE_LIMIT)
857 .map(|export| NativeExportSummary {
858 name: export.name.clone(),
859 created_at_unix_ms: export.created_at_unix_ms,
860 snapshot_id: export.snapshot_id,
861 superblock_sequence: export.superblock_sequence,
862 collection_count: export.collection_count as u32,
863 total_entities: export.total_entities as u64,
864 })
865 .collect();
866
867 NativeRecoverySummary {
868 snapshot_count: metadata.snapshots.len() as u32,
869 export_count: metadata.exports.len() as u32,
870 snapshots_complete: metadata.snapshots.len() <= SAMPLE_LIMIT,
871 exports_complete: metadata.exports.len() <= SAMPLE_LIMIT,
872 omitted_snapshot_count: metadata.snapshots.len().saturating_sub(snapshots.len()) as u32,
873 omitted_export_count: metadata.exports.len().saturating_sub(exports.len()) as u32,
874 snapshots,
875 exports,
876 }
877 }
878
879 pub(crate) fn native_catalog_summary_from_metadata(
880 metadata: &PhysicalMetadataFile,
881 ) -> NativeCatalogSummary {
882 const SAMPLE_LIMIT: usize = 32;
883
884 let collections: Vec<_> = metadata
885 .catalog
886 .stats_by_collection
887 .iter()
888 .take(SAMPLE_LIMIT)
889 .map(|(name, stats)| NativeCatalogCollectionSummary {
890 name: name.clone(),
891 entities: stats.entities as u64,
892 cross_refs: stats.cross_refs as u64,
893 segments: stats.segments as u32,
894 })
895 .collect();
896
897 NativeCatalogSummary {
898 collection_count: metadata.catalog.total_collections as u32,
899 total_entities: metadata.catalog.total_entities as u64,
900 collections_complete: metadata.catalog.stats_by_collection.len() <= SAMPLE_LIMIT,
901 omitted_collection_count: metadata
902 .catalog
903 .stats_by_collection
904 .len()
905 .saturating_sub(collections.len()) as u32,
906 collections,
907 }
908 }
909
910 pub(crate) fn native_metadata_state_summary_from_metadata(
911 metadata: &PhysicalMetadataFile,
912 ) -> NativeMetadataStateSummary {
913 NativeMetadataStateSummary {
914 protocol_version: metadata.protocol_version.clone(),
915 generated_at_unix_ms: metadata.generated_at_unix_ms,
916 last_loaded_from: metadata.last_loaded_from.clone(),
917 last_healed_at_unix_ms: metadata.last_healed_at_unix_ms,
918 }
919 }
920
921 pub(crate) fn inspect_native_header_against_metadata(
922 native: PhysicalFileHeader,
923 metadata: &PhysicalMetadataFile,
924 ) -> NativeHeaderInspection {
925 let expected = Self::native_header_from_metadata(metadata);
926 let mut mismatches = Vec::new();
927
928 if native.format_version != expected.format_version {
929 mismatches.push(NativeHeaderMismatch {
930 field: "format_version",
931 native: native.format_version.to_string(),
932 expected: expected.format_version.to_string(),
933 });
934 }
935 if native.sequence != expected.sequence {
936 mismatches.push(NativeHeaderMismatch {
937 field: "sequence",
938 native: native.sequence.to_string(),
939 expected: expected.sequence.to_string(),
940 });
941 }
942 if native.manifest_oldest_root != expected.manifest_oldest_root {
943 mismatches.push(NativeHeaderMismatch {
944 field: "manifest_oldest_root",
945 native: native.manifest_oldest_root.to_string(),
946 expected: expected.manifest_oldest_root.to_string(),
947 });
948 }
949 if native.manifest_root != expected.manifest_root {
950 mismatches.push(NativeHeaderMismatch {
951 field: "manifest_root",
952 native: native.manifest_root.to_string(),
953 expected: expected.manifest_root.to_string(),
954 });
955 }
956 if native.free_set_root != expected.free_set_root {
957 mismatches.push(NativeHeaderMismatch {
958 field: "free_set_root",
959 native: native.free_set_root.to_string(),
960 expected: expected.free_set_root.to_string(),
961 });
962 }
963 if native.collection_root_count != expected.collection_root_count {
964 mismatches.push(NativeHeaderMismatch {
965 field: "collection_root_count",
966 native: native.collection_root_count.to_string(),
967 expected: expected.collection_root_count.to_string(),
968 });
969 }
970 if native.snapshot_count != expected.snapshot_count {
971 mismatches.push(NativeHeaderMismatch {
972 field: "snapshot_count",
973 native: native.snapshot_count.to_string(),
974 expected: expected.snapshot_count.to_string(),
975 });
976 }
977 if native.index_count != expected.index_count {
978 mismatches.push(NativeHeaderMismatch {
979 field: "index_count",
980 native: native.index_count.to_string(),
981 expected: expected.index_count.to_string(),
982 });
983 }
984 if native.catalog_collection_count != expected.catalog_collection_count {
985 mismatches.push(NativeHeaderMismatch {
986 field: "catalog_collection_count",
987 native: native.catalog_collection_count.to_string(),
988 expected: expected.catalog_collection_count.to_string(),
989 });
990 }
991 if native.catalog_total_entities != expected.catalog_total_entities {
992 mismatches.push(NativeHeaderMismatch {
993 field: "catalog_total_entities",
994 native: native.catalog_total_entities.to_string(),
995 expected: expected.catalog_total_entities.to_string(),
996 });
997 }
998 if native.export_count != expected.export_count {
999 mismatches.push(NativeHeaderMismatch {
1000 field: "export_count",
1001 native: native.export_count.to_string(),
1002 expected: expected.export_count.to_string(),
1003 });
1004 }
1005 if native.graph_projection_count != expected.graph_projection_count {
1006 mismatches.push(NativeHeaderMismatch {
1007 field: "graph_projection_count",
1008 native: native.graph_projection_count.to_string(),
1009 expected: expected.graph_projection_count.to_string(),
1010 });
1011 }
1012 if native.analytics_job_count != expected.analytics_job_count {
1013 mismatches.push(NativeHeaderMismatch {
1014 field: "analytics_job_count",
1015 native: native.analytics_job_count.to_string(),
1016 expected: expected.analytics_job_count.to_string(),
1017 });
1018 }
1019 if native.manifest_event_count != expected.manifest_event_count {
1020 mismatches.push(NativeHeaderMismatch {
1021 field: "manifest_event_count",
1022 native: native.manifest_event_count.to_string(),
1023 expected: expected.manifest_event_count.to_string(),
1024 });
1025 }
1026
1027 NativeHeaderInspection {
1028 native,
1029 expected,
1030 consistent: mismatches.is_empty(),
1031 mismatches,
1032 }
1033 }
1034
1035 pub(crate) fn repair_policy_for_inspection(
1036 inspection: &NativeHeaderInspection,
1037 ) -> NativeHeaderRepairPolicy {
1038 if inspection.consistent {
1039 return NativeHeaderRepairPolicy::InSync;
1040 }
1041
1042 if inspection.expected.sequence >= inspection.native.sequence {
1043 NativeHeaderRepairPolicy::RepairNativeFromMetadata
1044 } else {
1045 NativeHeaderRepairPolicy::NativeAheadOfMetadata
1046 }
1047 }
1048
1049 pub(crate) fn prune_export_registry(&self, exports: &mut Vec<ExportDescriptor>) {
1050 let retention = self.options.export_retention.max(1);
1051 if exports.len() <= retention {
1052 return;
1053 }
1054
1055 exports.sort_by_key(|export| export.created_at_unix_ms);
1056 let removed: Vec<ExportDescriptor> =
1057 exports.drain(0..(exports.len() - retention)).collect();
1058
1059 for export in removed {
1060 let _ = fs::remove_file(&export.data_path);
1061 let _ = fs::remove_file(&export.metadata_path);
1062 let binary_path = PhysicalMetadataFile::metadata_binary_path_for(std::path::Path::new(
1063 &export.data_path,
1064 ));
1065 let _ = fs::remove_file(binary_path);
1066 }
1067 }
1068
1069 pub(crate) fn runtime_index_catalog(&self) -> IndexCatalog {
1070 let mut catalog = IndexCatalog::register_default_vector_graph(
1071 self.options.has_capability(Capability::Table),
1072 self.options.has_capability(Capability::Graph),
1073 );
1074 if self.options.has_capability(Capability::FullText) {
1075 catalog.register(RuntimeIndexConfig::new(
1076 "text-fulltext",
1077 IndexKind::FullText,
1078 ));
1079 catalog.register(RuntimeIndexConfig::new(
1080 "document-pathvalue",
1081 IndexKind::DocumentPathValue,
1082 ));
1083 }
1084 catalog.register(RuntimeIndexConfig::new(
1085 "search-hybrid",
1086 IndexKind::HybridSearch,
1087 ));
1088 catalog
1089 }
1090
1091 pub(crate) fn physical_index_state(&self) -> Vec<PhysicalIndexState> {
1092 let catalog = self.runtime_index_catalog();
1096 let snapshot = crate::catalog::snapshot_store_with_declarations(
1097 "reddb",
1098 self.store.as_ref(),
1099 Some(&catalog),
1100 None, None, );
1103 let mut metrics_by_name = std::collections::BTreeMap::new();
1104 for metric in &snapshot.indices {
1105 metrics_by_name.insert(metric.name.clone(), metric.clone());
1106 }
1107
1108 let mut states = Vec::new();
1109 for collection in snapshot.collections {
1110 if self
1117 .store
1118 .get_collection(&collection.name)
1119 .is_some_and(|manager| manager.has_backing())
1120 {
1121 continue;
1122 }
1123 for index_name in &collection.indices {
1124 let metric = metrics_by_name.get(index_name);
1125 let kind = metric
1126 .map(|metric| metric.kind)
1127 .unwrap_or_else(|| infer_collection_index_kind(collection.model, index_name));
1128 let entries = estimate_index_entries(&collection, kind);
1129 states.push(PhysicalIndexState {
1130 name: format!("{}::{}", collection.name, index_name),
1131 kind,
1132 collection: Some(collection.name.clone()),
1133 enabled: metric.map(|metric| metric.enabled).unwrap_or(true),
1134 entries,
1135 estimated_memory_bytes: estimate_index_memory(entries, kind),
1136 last_refresh_ms: metric.and_then(|metric| metric.last_refresh_ms),
1137 backend: index_backend_name(kind).to_string(),
1138 artifact_kind: None,
1139 artifact_root_page: None,
1140 artifact_checksum: None,
1141 build_state: "catalog-derived".to_string(),
1142 });
1143 }
1144 }
1145
1146 states
1147 }
1148
1149 pub(crate) fn physical_collection_roots(&self) -> BTreeMap<String, u64> {
1150 let mut roots = BTreeMap::new();
1151
1152 for name in self.store.list_collections() {
1153 let Some(manager) = self.store.get_collection(&name) else {
1154 continue;
1155 };
1156
1157 let stats = manager.stats();
1158 let mut root = fnv1a_seed();
1159 fnv1a_hash_value(&mut root, &name);
1160 fnv1a_hash_value(&mut root, &stats.total_entities);
1161 fnv1a_hash_value(&mut root, &stats.growing_count);
1162 fnv1a_hash_value(&mut root, &stats.sealed_count);
1163 fnv1a_hash_value(&mut root, &stats.archived_count);
1164 fnv1a_hash_value(&mut root, &stats.total_memory_bytes);
1165 fnv1a_hash_value(&mut root, &stats.seal_ops);
1166 fnv1a_hash_value(&mut root, &stats.consolidation.runs_completed);
1167
1168 let root = manager.cached_physical_root(root, || {
1169 manager.for_each_entity_in_id_order(|entity| {
1170 fnv1a_hash_value(&mut root, &entity.id.raw());
1171 fnv1a_hash_value(&mut root, &entity.kind);
1172 fnv1a_hash_value(&mut root, &entity.created_at);
1173 fnv1a_hash_value(&mut root, &entity.updated_at);
1174 fnv1a_hash_value(&mut root, &entity.data);
1175 fnv1a_hash_value(&mut root, &entity.sequence_id);
1176 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1177 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1178 true
1179 });
1180 root
1181 });
1182
1183 roots.insert(name, root);
1184 }
1185
1186 roots
1187 }
1188
1189 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1195 TableRef::new(table, row_id)
1196 }
1197
1198 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1200 NodeRef::new(collection, node_id)
1201 }
1202
1203 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1205 VectorRef::new(collection, vector_id)
1206 }
1207
1208 pub fn query(&self) -> QueryBuilder {
1214 QueryBuilder::new(self.store.clone())
1215 }
1216
1217 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1224 if self.store.get_collection(collection).is_none() {
1225 return Vec::new();
1226 }
1227
1228 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1230 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1231 let results = hnsw.search(vector, k);
1232 let mapped = self.hnsw_results_to_similar(collection, &results);
1233 if !mapped.is_empty() {
1234 return mapped;
1235 }
1236 }
1237
1238 self.similar_brute_force(collection, vector, k)
1240 }
1241
1242 fn similar_brute_force(
1244 &self,
1245 collection: &str,
1246 vector: &[f32],
1247 k: usize,
1248 ) -> Vec<SimilarResult> {
1249 let manager = match self.store.get_collection(collection) {
1250 Some(m) => m,
1251 None => return Vec::new(),
1252 };
1253
1254 let entities = manager.query_all(|_| true);
1255 let mut results: Vec<SimilarResult> = entities
1256 .iter()
1257 .filter_map(|e| {
1258 let score = match &e.data {
1259 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1260 _ => e
1261 .embeddings()
1262 .iter()
1263 .map(|emb| cosine_similarity(vector, &emb.vector))
1264 .fold(0.0f32, f32::max),
1265 };
1266 let distance = (1.0 - score).max(0.0);
1267 if score > 0.0 {
1268 Some(SimilarResult {
1269 entity_id: e.id,
1270 score,
1271 distance,
1272 entity: e.clone(),
1273 })
1274 } else {
1275 None
1276 }
1277 })
1278 .collect();
1279
1280 results.sort_by(|a, b| {
1281 b.score
1282 .partial_cmp(&a.score)
1283 .unwrap_or(std::cmp::Ordering::Equal)
1284 });
1285 results.truncate(k);
1286 results
1287 }
1288
1289 fn get_or_build_hnsw_index(
1299 &self,
1300 collection: &str,
1301 query_dim: usize,
1302 ) -> Option<Arc<RwLock<HnswIndex>>> {
1303 let manager = self.store.get_collection(collection)?;
1304 let live_count = manager.count();
1305
1306 {
1308 let indexes = self
1309 .vector_indexes
1310 .read()
1311 .unwrap_or_else(|e| e.into_inner());
1312 if let Some(cached) = indexes.get(collection) {
1313 if cached.entity_count == live_count {
1314 return Some(Arc::clone(&cached.index));
1315 }
1316 }
1317 }
1318
1319 let entities = manager.query_all(|_| true);
1321
1322 let vectors: Vec<(u64, Vec<f32>)> = entities
1323 .iter()
1324 .filter_map(|e| match &e.data {
1325 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1326 Some((e.id.raw(), v.dense.clone()))
1327 }
1328 _ => None,
1329 })
1330 .collect();
1331
1332 const MIN_VECTORS_FOR_HNSW: usize = 100;
1334 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1335 return None;
1336 }
1337
1338 let config = crate::storage::engine::HnswConfig::with_m(16)
1340 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1341 .with_ef_construction(100)
1342 .with_ef_search(50);
1343 let mut hnsw = HnswIndex::new(query_dim, config);
1344
1345 for (id, vec) in &vectors {
1346 hnsw.insert_with_id(*id, vec.clone());
1347 }
1348
1349 let index = Arc::new(RwLock::new(hnsw));
1350
1351 let mut indexes = self
1353 .vector_indexes
1354 .write()
1355 .unwrap_or_else(|e| e.into_inner());
1356 if let Some(cached) = indexes.get(collection) {
1358 if cached.entity_count == live_count {
1359 return Some(Arc::clone(&cached.index));
1360 }
1361 }
1362 indexes.insert(
1363 collection.to_string(),
1364 CachedVectorIndex {
1365 index: Arc::clone(&index),
1366 entity_count: live_count,
1367 },
1368 );
1369 Some(index)
1370 }
1371
1372 fn hnsw_results_to_similar(
1374 &self,
1375 collection: &str,
1376 results: &[crate::storage::engine::DistanceResult],
1377 ) -> Vec<SimilarResult> {
1378 results
1379 .iter()
1380 .filter_map(|dr| {
1381 let entity_id = EntityId::new(dr.id);
1382 let entity = self.store.get(collection, entity_id)?;
1383 let score = (1.0 - dr.distance).max(0.0);
1385 if score > 0.0 {
1386 Some(SimilarResult {
1387 entity_id,
1388 score,
1389 distance: dr.distance,
1390 entity,
1391 })
1392 } else {
1393 None
1394 }
1395 })
1396 .collect()
1397 }
1398
1399 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1404 let mut indexes = self
1405 .vector_indexes
1406 .write()
1407 .unwrap_or_else(|e| e.into_inner());
1408 indexes.remove(collection);
1409 }
1410
1411 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1413 self.store.get_any(id).map(|(_, e)| e)
1414 }
1415
1416 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1418 self.store.get_any(id)
1419 }
1420
1421 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1427 ids.iter().map(|id| self.get(*id)).collect()
1428 }
1429
1430 pub fn batch(&self) -> BatchBuilder {
1432 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1433 }
1434
1435 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1441 let mut preprocessors = self
1442 .preprocessors
1443 .write()
1444 .unwrap_or_else(|poisoned| poisoned.into_inner());
1445 preprocessors.push(Arc::from(preprocessor));
1446 }
1447
1448 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1454 self.store
1455 .get_refs_from(id)
1456 .into_iter()
1457 .filter_map(|(target_id, ref_type, collection)| {
1458 self.store
1459 .get(&collection, target_id)
1460 .map(|entity| LinkedEntity {
1461 entity,
1462 ref_type,
1463 collection,
1464 })
1465 })
1466 .collect()
1467 }
1468
1469 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1471 self.store
1472 .get_refs_to(id)
1473 .into_iter()
1474 .filter_map(|(source_id, ref_type, collection)| {
1475 self.store
1476 .get(&collection, source_id)
1477 .map(|entity| LinkedEntity {
1478 entity,
1479 ref_type,
1480 collection,
1481 })
1482 })
1483 .collect()
1484 }
1485
1486 pub fn store(&self) -> Arc<UnifiedStore> {
1488 self.store.clone()
1489 }
1490
1491 pub(crate) fn turbo_collections(
1497 &self,
1498 ) -> &Arc<
1499 parking_lot::Mutex<
1500 std::collections::HashMap<
1501 String,
1502 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1503 >,
1504 >,
1505 > {
1506 self.turbo_collections
1507 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1508 }
1509
1510 pub(crate) fn turbo_state(
1520 &self,
1521 collection: &str,
1522 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1523 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1524 return None;
1525 }
1526 let map = self.turbo_collections();
1527 {
1528 let guard = map.lock();
1529 if let Some(state) = guard.get(collection) {
1530 return Some(Arc::clone(state));
1531 }
1532 }
1533 let contract = self.collection_contract(collection)?;
1534 let dim = contract.vector_dimension?;
1535 let metric = contract
1536 .vector_metric
1537 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1538 let state = Arc::new(
1539 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1540 dim,
1541 metric,
1542 self.store.pager(),
1543 ),
1544 );
1545 if let Some((_, paths)) = self.options.resolve_tiered_layout() {
1550 state.set_snapshot_path(paths.turbo_snapshot_path(collection));
1551 }
1552 let mut guard = map.lock();
1553 let inserted_now = !guard.contains_key(collection);
1554 let entry = guard
1555 .entry(collection.to_string())
1556 .or_insert_with(|| Arc::clone(&state));
1557 let handle = Arc::clone(entry);
1558 drop(guard);
1559 if inserted_now {
1566 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1567 Arc::clone(&self.store),
1568 collection.to_string(),
1569 Arc::clone(&handle),
1570 );
1571 self.turbo_rebuild_workers.lock().push(join);
1572 }
1573 Some(handle)
1574 }
1575
1576 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1582 self.ml_runtime.get_or_init(|| {
1583 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1584 |_handle| Ok(String::new()),
1588 ))
1589 })
1590 }
1591
1592 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1596 self.semantic_cache.get_or_init(|| {
1597 Arc::new(crate::storage::ml::SemanticCache::new(
1598 crate::storage::ml::SemanticCacheConfig::default(),
1599 ))
1600 })
1601 }
1602
1603 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1606 self.hypertables
1607 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1608 }
1609
1610 pub fn continuous_aggregates(
1613 &self,
1614 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1615 self.continuous_aggregates.get_or_init(|| {
1616 Arc::new(
1617 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1618 )
1619 })
1620 }
1621
1622 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1623 let mut file = File::open(path)?;
1624 let mut magic = [0u8; 4];
1625 let read = file.read(&mut magic)?;
1626 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1627 }
1628}
1629
1630#[cfg(test)]
1631mod artifact_memory_tests {
1632 use super::*;
1633
1634 #[test]
1635 fn physical_roots_hash_borrowed_payloads_without_rendered_copies() {
1636 fn legacy_root(db: &RedDB) -> u64 {
1637 fn hash<T: Debug>(root: &mut u64, value: &T) {
1638 fnv1a_hash_bytes(root, format!("{value:?}").as_bytes());
1639 }
1640 let manager = db.store().get_collection("root_source").expect("source");
1641 let stats = manager.stats();
1642 let mut root = fnv1a_seed();
1643 hash(&mut root, &"root_source".to_string());
1644 hash(&mut root, &stats.total_entities);
1645 hash(&mut root, &stats.growing_count);
1646 hash(&mut root, &stats.sealed_count);
1647 hash(&mut root, &stats.archived_count);
1648 hash(&mut root, &stats.total_memory_bytes);
1649 hash(&mut root, &stats.seal_ops);
1650 hash(&mut root, &stats.consolidation.runs_completed);
1651 manager.for_each_entity_in_id_order(|entity| {
1652 hash(&mut root, &entity.id.raw());
1653 hash(&mut root, &entity.kind);
1654 hash(&mut root, &entity.created_at);
1655 hash(&mut root, &entity.updated_at);
1656 hash(&mut root, &entity.data);
1657 hash(&mut root, &entity.sequence_id);
1658 hash(&mut root, &entity.embeddings().len());
1659 hash(&mut root, &entity.cross_refs().len());
1660 true
1661 });
1662 root
1663 }
1664
1665 let runtime =
1666 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1667 runtime
1668 .execute_query("CREATE TABLE root_source (payload BLOB, label TEXT, body JSON)")
1669 .expect("table");
1670 let db = runtime.db();
1671 let manager = db.store().get_collection("root_source").expect("source");
1672 let mut entity = UnifiedEntity::table_row(
1673 EntityId::new(1001),
1674 "root_source",
1675 1,
1676 vec![
1677 Value::Blob(vec![255; 2 * 1024 * 1024]),
1678 Value::text("escaped\n\t\0\"\\ and Unicode ç🦀"),
1679 Value::Json(br#"{"nested":[null,true,17,"kept"]}"#.to_vec()),
1680 ],
1681 );
1682 manager.insert(entity.clone()).expect("source row");
1683 let expected = legacy_root(&db);
1684 let (roots, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1685 db.physical_collection_roots()
1686 });
1687 assert_eq!(roots["root_source"], expected, "retain legacy hash bytes");
1688 eprintln!(
1689 "physical root allocation bytes: {}",
1690 measured.allocated_bytes
1691 );
1692 assert!(
1693 measured.allocated_bytes < 64 * 1024,
1694 "fingerprinting must not render the borrowed payload: {measured:?}"
1695 );
1696 assert_eq!(db.physical_collection_roots(), roots, "unchanged cache");
1697 let EntityData::Row(row) = &mut entity.data else {
1698 unreachable!("fixture is a table row");
1699 };
1700 row.columns[0] = Value::Blob(vec![254; 2 * 1024 * 1024]);
1701 manager
1702 .update(entity)
1703 .expect("replace payload of equal size");
1704 let updated = db.physical_collection_roots();
1705 assert_ne!(updated["root_source"], roots["root_source"]);
1706 assert_eq!(updated["root_source"], legacy_root(&db));
1707 }
1708
1709 #[test]
1710 fn document_artifact_paths_survive_shared_recovery_shapes() {
1711 let mut entity = UnifiedEntity::table_row(EntityId::new(1), "documents", 1, vec![]);
1712 entity.data = EntityData::Row(crate::storage::unified::entity::RowData::with_names(
1713 vec![Value::Json(br#"{"thing":"kept"}"#.to_vec())],
1714 vec!["body".into()],
1715 ));
1716 let before = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1717 .expect("named document");
1718 let EntityData::Row(row) = &mut entity.data else {
1719 unreachable!("fixture is a row");
1720 };
1721 *row = crate::storage::unified::entity::RowData {
1722 columns: vec![row.get_field("body").expect("body field").clone()],
1723 named: None,
1724 schema: Some(Arc::new(vec!["body".into()])),
1725 };
1726 let after = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1727 .expect("shared document");
1728 assert_eq!(
1729 before, after,
1730 "compaction must preserve named document paths"
1731 );
1732 }
1733
1734 #[test]
1735 fn disabled_fulltext_does_not_copy_rows_or_publish_search_artifacts() {
1736 let runtime =
1737 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1738 runtime
1739 .execute_query("CREATE TABLE source (payload BLOB)")
1740 .expect("table");
1741 let db = runtime.db();
1742 let manager = db.store().get_collection("source").expect("manager");
1743 let (_, baseline) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1744 db.native_vector_artifact_records().expect("empty build")
1745 });
1746 for id in 1..=32 {
1747 manager
1748 .insert(UnifiedEntity::table_row(
1749 EntityId::new(1000 + id),
1750 "source",
1751 id,
1752 vec![Value::Blob(vec![255; 64 * 1024])],
1753 ))
1754 .expect("source row");
1755 }
1756 let (build, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1757 db.native_vector_artifact_records().expect("disabled build")
1758 });
1759 assert!(build.records.is_empty());
1760 assert!(
1761 build.deferred_collections.is_empty(),
1762 "disabled capabilities do not consume artifact allowance"
1763 );
1764 assert!(
1765 measured.allocated_bytes <= baseline.allocated_bytes + 4096,
1766 "disabled fulltext copied source payloads: {baseline:?} -> {measured:?}"
1767 );
1768 }
1769
1770 #[test]
1771 fn document_artifact_traversal_is_ordered_and_bounded() {
1772 let json = crate::json::from_slice::<JsonValue>(br#"{"a":{"b":1},"z":[2,3]}"#)
1773 .expect("JSON fixture");
1774 let mut entries = Vec::new();
1775 let mut bytes = 0;
1776 RedDB::collect_native_document_entries_from_json(
1777 "body",
1778 &json,
1779 &mut entries,
1780 &mut bytes,
1781 4096,
1782 )
1783 .expect("small document fits");
1784 assert_eq!(
1785 entries,
1786 vec![
1787 ("body.a.b".into(), "1".into()),
1788 ("body.z[0]".into(), "2".into()),
1789 ("body.z[1]".into(), "3".into())
1790 ]
1791 );
1792 let mut deep = JsonValue::Integer(1);
1793 for _ in 0..64 {
1794 deep = JsonValue::Object([("child".to_string(), deep)].into());
1795 }
1796 let mut entries = Vec::new();
1797 let mut bytes = 0;
1798 assert!(RedDB::collect_native_document_entries_from_json(
1799 "body",
1800 &deep,
1801 &mut entries,
1802 &mut bytes,
1803 128
1804 )
1805 .is_err());
1806 assert!(bytes <= 128);
1807 }
1808}