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 record_physical_root_row_scan(&name);
1171 fnv1a_hash_value(&mut root, &entity.id.raw());
1172 fnv1a_hash_value(&mut root, &entity.kind);
1173 fnv1a_hash_value(&mut root, &entity.created_at);
1174 fnv1a_hash_value(&mut root, &entity.updated_at);
1175 fnv1a_hash_value(&mut root, &entity.data);
1176 fnv1a_hash_value(&mut root, &entity.sequence_id);
1177 fnv1a_hash_value(&mut root, &entity.embeddings().len());
1178 fnv1a_hash_value(&mut root, &entity.cross_refs().len());
1179 true
1180 });
1181 root
1182 });
1183
1184 roots.insert(name, root);
1185 }
1186
1187 roots
1188 }
1189
1190 pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1196 TableRef::new(table, row_id)
1197 }
1198
1199 pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1201 NodeRef::new(collection, node_id)
1202 }
1203
1204 pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1206 VectorRef::new(collection, vector_id)
1207 }
1208
1209 pub fn query(&self) -> QueryBuilder {
1215 QueryBuilder::new(self.store.clone())
1216 }
1217
1218 pub fn similar(&self, collection: &str, vector: &[f32], k: usize) -> Vec<SimilarResult> {
1225 if self.store.get_collection(collection).is_none() {
1226 return Vec::new();
1227 }
1228
1229 if let Some(index) = self.get_or_build_hnsw_index(collection, vector.len()) {
1231 let hnsw = index.read().unwrap_or_else(|e| e.into_inner());
1232 let results = hnsw.search(vector, k);
1233 let mapped = self.hnsw_results_to_similar(collection, &results);
1234 if !mapped.is_empty() {
1235 return mapped;
1236 }
1237 }
1238
1239 self.similar_brute_force(collection, vector, k)
1241 }
1242
1243 fn similar_brute_force(
1245 &self,
1246 collection: &str,
1247 vector: &[f32],
1248 k: usize,
1249 ) -> Vec<SimilarResult> {
1250 let manager = match self.store.get_collection(collection) {
1251 Some(m) => m,
1252 None => return Vec::new(),
1253 };
1254
1255 let entities = manager.query_all(|_| true);
1256 let mut results: Vec<SimilarResult> = entities
1257 .iter()
1258 .filter_map(|e| {
1259 let score = match &e.data {
1260 EntityData::Vector(v) => cosine_similarity(vector, &v.dense),
1261 _ => e
1262 .embeddings()
1263 .iter()
1264 .map(|emb| cosine_similarity(vector, &emb.vector))
1265 .fold(0.0f32, f32::max),
1266 };
1267 let distance = (1.0 - score).max(0.0);
1268 if score > 0.0 {
1269 Some(SimilarResult {
1270 entity_id: e.id,
1271 score,
1272 distance,
1273 entity: e.clone(),
1274 })
1275 } else {
1276 None
1277 }
1278 })
1279 .collect();
1280
1281 results.sort_by(|a, b| {
1282 b.score
1283 .partial_cmp(&a.score)
1284 .unwrap_or(std::cmp::Ordering::Equal)
1285 });
1286 results.truncate(k);
1287 results
1288 }
1289
1290 fn get_or_build_hnsw_index(
1300 &self,
1301 collection: &str,
1302 query_dim: usize,
1303 ) -> Option<Arc<RwLock<HnswIndex>>> {
1304 let manager = self.store.get_collection(collection)?;
1305 let live_count = manager.count();
1306
1307 {
1309 let indexes = self
1310 .vector_indexes
1311 .read()
1312 .unwrap_or_else(|e| e.into_inner());
1313 if let Some(cached) = indexes.get(collection) {
1314 if cached.entity_count == live_count {
1315 return Some(Arc::clone(&cached.index));
1316 }
1317 }
1318 }
1319
1320 let entities = manager.query_all(|_| true);
1322
1323 let vectors: Vec<(u64, Vec<f32>)> = entities
1324 .iter()
1325 .filter_map(|e| match &e.data {
1326 EntityData::Vector(v) if !v.dense.is_empty() && v.dense.len() == query_dim => {
1327 Some((e.id.raw(), v.dense.clone()))
1328 }
1329 _ => None,
1330 })
1331 .collect();
1332
1333 const MIN_VECTORS_FOR_HNSW: usize = 100;
1335 if vectors.len() < MIN_VECTORS_FOR_HNSW {
1336 return None;
1337 }
1338
1339 let config = crate::storage::engine::HnswConfig::with_m(16)
1341 .with_metric(crate::storage::engine::DistanceMetric::Cosine)
1342 .with_ef_construction(100)
1343 .with_ef_search(50);
1344 let mut hnsw = HnswIndex::new(query_dim, config);
1345
1346 for (id, vec) in &vectors {
1347 hnsw.insert_with_id(*id, vec.clone());
1348 }
1349
1350 let index = Arc::new(RwLock::new(hnsw));
1351
1352 let mut indexes = self
1354 .vector_indexes
1355 .write()
1356 .unwrap_or_else(|e| e.into_inner());
1357 if let Some(cached) = indexes.get(collection) {
1359 if cached.entity_count == live_count {
1360 return Some(Arc::clone(&cached.index));
1361 }
1362 }
1363 indexes.insert(
1364 collection.to_string(),
1365 CachedVectorIndex {
1366 index: Arc::clone(&index),
1367 entity_count: live_count,
1368 },
1369 );
1370 Some(index)
1371 }
1372
1373 fn hnsw_results_to_similar(
1375 &self,
1376 collection: &str,
1377 results: &[crate::storage::engine::DistanceResult],
1378 ) -> Vec<SimilarResult> {
1379 results
1380 .iter()
1381 .filter_map(|dr| {
1382 let entity_id = EntityId::new(dr.id);
1383 let entity = self.store.get(collection, entity_id)?;
1384 let score = (1.0 - dr.distance).max(0.0);
1386 if score > 0.0 {
1387 Some(SimilarResult {
1388 entity_id,
1389 score,
1390 distance: dr.distance,
1391 entity,
1392 })
1393 } else {
1394 None
1395 }
1396 })
1397 .collect()
1398 }
1399
1400 pub(crate) fn invalidate_vector_index(&self, collection: &str) {
1405 let mut indexes = self
1406 .vector_indexes
1407 .write()
1408 .unwrap_or_else(|e| e.into_inner());
1409 indexes.remove(collection);
1410 }
1411
1412 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1414 self.store.get_any(id).map(|(_, e)| e)
1415 }
1416
1417 pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1419 self.store.get_any(id)
1420 }
1421
1422 pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1428 ids.iter().map(|id| self.get(*id)).collect()
1429 }
1430
1431 pub fn batch(&self) -> BatchBuilder {
1433 BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1434 }
1435
1436 pub fn add_preprocessor(&mut self, preprocessor: Box<dyn Preprocessor>) {
1442 let mut preprocessors = self
1443 .preprocessors
1444 .write()
1445 .unwrap_or_else(|poisoned| poisoned.into_inner());
1446 preprocessors.push(Arc::from(preprocessor));
1447 }
1448
1449 pub fn linked_from(&self, id: EntityId) -> Vec<LinkedEntity> {
1455 self.store
1456 .get_refs_from(id)
1457 .into_iter()
1458 .filter_map(|(target_id, ref_type, collection)| {
1459 self.store
1460 .get(&collection, target_id)
1461 .map(|entity| LinkedEntity {
1462 entity,
1463 ref_type,
1464 collection,
1465 })
1466 })
1467 .collect()
1468 }
1469
1470 pub fn linked_to(&self, id: EntityId) -> Vec<LinkedEntity> {
1472 self.store
1473 .get_refs_to(id)
1474 .into_iter()
1475 .filter_map(|(source_id, ref_type, collection)| {
1476 self.store
1477 .get(&collection, source_id)
1478 .map(|entity| LinkedEntity {
1479 entity,
1480 ref_type,
1481 collection,
1482 })
1483 })
1484 .collect()
1485 }
1486
1487 pub fn store(&self) -> Arc<UnifiedStore> {
1489 self.store.clone()
1490 }
1491
1492 pub(crate) fn turbo_collections(
1498 &self,
1499 ) -> &Arc<
1500 parking_lot::Mutex<
1501 std::collections::HashMap<
1502 String,
1503 Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>,
1504 >,
1505 >,
1506 > {
1507 self.turbo_collections
1508 .get_or_init(|| Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())))
1509 }
1510
1511 pub(crate) fn turbo_state(
1521 &self,
1522 collection: &str,
1523 ) -> Option<Arc<crate::runtime::vector_turbo_kind::TurboCollectionState>> {
1524 if !crate::runtime::vector_turbo_kind::is_turbo(&self.store, collection) {
1525 return None;
1526 }
1527 let map = self.turbo_collections();
1528 {
1529 let guard = map.lock();
1530 if let Some(state) = guard.get(collection) {
1531 return Some(Arc::clone(state));
1532 }
1533 }
1534 let contract = self.collection_contract(collection)?;
1535 let dim = contract.vector_dimension?;
1536 let metric = contract
1537 .vector_metric
1538 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine);
1539 let state = Arc::new(
1540 crate::runtime::vector_turbo_kind::TurboCollectionState::new(
1541 dim,
1542 metric,
1543 self.store.pager(),
1544 ),
1545 );
1546 if let Some((_, paths)) = self.options.resolve_tiered_layout() {
1551 state.set_snapshot_path(paths.turbo_snapshot_path(collection));
1552 }
1553 let mut guard = map.lock();
1554 let inserted_now = !guard.contains_key(collection);
1555 let entry = guard
1556 .entry(collection.to_string())
1557 .or_insert_with(|| Arc::clone(&state));
1558 let handle = Arc::clone(entry);
1559 drop(guard);
1560 if inserted_now {
1567 let join = crate::runtime::vector_turbo_kind::spawn_background_rebuild(
1568 Arc::clone(&self.store),
1569 collection.to_string(),
1570 Arc::clone(&handle),
1571 );
1572 self.turbo_rebuild_workers.lock().push(join);
1573 }
1574 Some(handle)
1575 }
1576
1577 pub fn ml_runtime(&self) -> &crate::storage::ml::MlRuntime {
1583 self.ml_runtime.get_or_init(|| {
1584 crate::storage::ml::MlRuntime::in_memory(std::sync::Arc::new(
1585 |_handle| Ok(String::new()),
1589 ))
1590 })
1591 }
1592
1593 pub fn semantic_cache(&self) -> &Arc<crate::storage::ml::SemanticCache> {
1597 self.semantic_cache.get_or_init(|| {
1598 Arc::new(crate::storage::ml::SemanticCache::new(
1599 crate::storage::ml::SemanticCacheConfig::default(),
1600 ))
1601 })
1602 }
1603
1604 pub fn hypertables(&self) -> &Arc<crate::storage::timeseries::HypertableRegistry> {
1607 self.hypertables
1608 .get_or_init(|| Arc::new(crate::storage::timeseries::HypertableRegistry::new()))
1609 }
1610
1611 pub fn continuous_aggregates(
1614 &self,
1615 ) -> &Arc<crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine> {
1616 self.continuous_aggregates.get_or_init(|| {
1617 Arc::new(
1618 crate::storage::timeseries::continuous_aggregate::ContinuousAggregateEngine::new(),
1619 )
1620 })
1621 }
1622
1623 pub(crate) fn is_binary_dump(path: &Path) -> Result<bool, std::io::Error> {
1624 let mut file = File::open(path)?;
1625 let mut magic = [0u8; 4];
1626 let read = file.read(&mut magic)?;
1627 Ok(read == 4 && reddb_file::native_store_magic_matches(&magic))
1628 }
1629}
1630
1631#[cfg(not(test))]
1632fn record_physical_root_row_scan(_collection: &str) {}
1633
1634#[cfg(test)]
1635std::thread_local! {
1636 static PHYSICAL_ROOT_ROW_SCANS: std::cell::RefCell<std::collections::BTreeMap<String, u64>> =
1637 const { std::cell::RefCell::new(std::collections::BTreeMap::new()) };
1638}
1639
1640#[cfg(test)]
1641fn record_physical_root_row_scan(collection: &str) {
1642 PHYSICAL_ROOT_ROW_SCANS.with(|scans| {
1643 *scans
1644 .borrow_mut()
1645 .entry(collection.to_string())
1646 .or_default() += 1;
1647 });
1648}
1649
1650#[cfg(test)]
1651impl RedDB {
1652 pub(crate) fn reset_physical_root_row_scans_for_test(&self) {
1653 PHYSICAL_ROOT_ROW_SCANS.with(|scans| scans.borrow_mut().clear());
1654 }
1655
1656 pub(crate) fn physical_root_row_scans_for_test(
1657 &self,
1658 ) -> std::collections::BTreeMap<String, u64> {
1659 PHYSICAL_ROOT_ROW_SCANS.with(|scans| scans.borrow().clone())
1660 }
1661}
1662
1663#[cfg(test)]
1664mod artifact_memory_tests {
1665 use super::*;
1666
1667 #[test]
1668 fn physical_roots_hash_borrowed_payloads_without_rendered_copies() {
1669 fn legacy_root(db: &RedDB) -> u64 {
1670 fn hash<T: Debug>(root: &mut u64, value: &T) {
1671 fnv1a_hash_bytes(root, format!("{value:?}").as_bytes());
1672 }
1673 let manager = db.store().get_collection("root_source").expect("source");
1674 let stats = manager.stats();
1675 let mut root = fnv1a_seed();
1676 hash(&mut root, &"root_source".to_string());
1677 hash(&mut root, &stats.total_entities);
1678 hash(&mut root, &stats.growing_count);
1679 hash(&mut root, &stats.sealed_count);
1680 hash(&mut root, &stats.archived_count);
1681 hash(&mut root, &stats.total_memory_bytes);
1682 hash(&mut root, &stats.seal_ops);
1683 hash(&mut root, &stats.consolidation.runs_completed);
1684 manager.for_each_entity_in_id_order(|entity| {
1685 hash(&mut root, &entity.id.raw());
1686 hash(&mut root, &entity.kind);
1687 hash(&mut root, &entity.created_at);
1688 hash(&mut root, &entity.updated_at);
1689 hash(&mut root, &entity.data);
1690 hash(&mut root, &entity.sequence_id);
1691 hash(&mut root, &entity.embeddings().len());
1692 hash(&mut root, &entity.cross_refs().len());
1693 true
1694 });
1695 root
1696 }
1697
1698 let runtime =
1699 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1700 runtime
1701 .execute_query("CREATE TABLE root_source (payload BLOB, label TEXT, body JSON)")
1702 .expect("table");
1703 let db = runtime.db();
1704 let manager = db.store().get_collection("root_source").expect("source");
1705 let mut entity = UnifiedEntity::table_row(
1706 EntityId::new(1001),
1707 "root_source",
1708 1,
1709 vec![
1710 Value::Blob(vec![255; 2 * 1024 * 1024]),
1711 Value::text("escaped\n\t\0\"\\ and Unicode ç🦀"),
1712 Value::Json(br#"{"nested":[null,true,17,"kept"]}"#.to_vec()),
1713 ],
1714 );
1715 manager.insert(entity.clone()).expect("source row");
1716 let expected = legacy_root(&db);
1717 let (roots, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1718 db.physical_collection_roots()
1719 });
1720 assert_eq!(roots["root_source"], expected, "retain legacy hash bytes");
1721 eprintln!(
1722 "physical root allocation bytes: {}",
1723 measured.allocated_bytes
1724 );
1725 assert!(
1726 measured.allocated_bytes < 64 * 1024,
1727 "fingerprinting must not render the borrowed payload: {measured:?}"
1728 );
1729 assert_eq!(db.physical_collection_roots(), roots, "unchanged cache");
1730 let EntityData::Row(row) = &mut entity.data else {
1731 unreachable!("fixture is a table row");
1732 };
1733 row.columns[0] = Value::Blob(vec![254; 2 * 1024 * 1024]);
1734 manager
1735 .update(entity)
1736 .expect("replace payload of equal size");
1737 let updated = db.physical_collection_roots();
1738 assert_ne!(updated["root_source"], roots["root_source"]);
1739 assert_eq!(updated["root_source"], legacy_root(&db));
1740 }
1741
1742 #[test]
1743 fn document_artifact_paths_survive_shared_recovery_shapes() {
1744 let mut entity = UnifiedEntity::table_row(EntityId::new(1), "documents", 1, vec![]);
1745 entity.data = EntityData::Row(crate::storage::unified::entity::RowData::with_names(
1746 vec![Value::Json(br#"{"thing":"kept"}"#.to_vec())],
1747 vec!["body".into()],
1748 ));
1749 let before = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1750 .expect("named document");
1751 let EntityData::Row(row) = &mut entity.data else {
1752 unreachable!("fixture is a row");
1753 };
1754 *row = crate::storage::unified::entity::RowData {
1755 columns: vec![row.get_field("body").expect("body field").clone()],
1756 named: None,
1757 schema: Some(Arc::new(vec!["body".into()])),
1758 };
1759 let after = RedDB::native_document_pathvalue_for_entity(entity.id, &entity.data, 4096)
1760 .expect("shared document");
1761 assert_eq!(
1762 before, after,
1763 "compaction must preserve named document paths"
1764 );
1765 }
1766
1767 #[test]
1768 fn disabled_fulltext_does_not_copy_rows_or_publish_search_artifacts() {
1769 let runtime =
1770 crate::RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
1771 runtime
1772 .execute_query("CREATE TABLE source (payload BLOB)")
1773 .expect("table");
1774 let db = runtime.db();
1775 let manager = db.store().get_collection("source").expect("manager");
1776 let (_, baseline) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1777 db.native_vector_artifact_records().expect("empty build")
1778 });
1779 for id in 1..=32 {
1780 manager
1781 .insert(UnifiedEntity::table_row(
1782 EntityId::new(1000 + id),
1783 "source",
1784 id,
1785 vec![Value::Blob(vec![255; 64 * 1024])],
1786 ))
1787 .expect("source row");
1788 }
1789 let (build, measured) = crate::storage::no_malloc_hot_paths::measure_allocations(|| {
1790 db.native_vector_artifact_records().expect("disabled build")
1791 });
1792 assert!(build.records.is_empty());
1793 assert!(
1794 build.deferred_collections.is_empty(),
1795 "disabled capabilities do not consume artifact allowance"
1796 );
1797 assert!(
1798 measured.allocated_bytes <= baseline.allocated_bytes + 4096,
1799 "disabled fulltext copied source payloads: {baseline:?} -> {measured:?}"
1800 );
1801 }
1802
1803 #[test]
1804 fn document_artifact_traversal_is_ordered_and_bounded() {
1805 let json = crate::json::from_slice::<JsonValue>(br#"{"a":{"b":1},"z":[2,3]}"#)
1806 .expect("JSON fixture");
1807 let mut entries = Vec::new();
1808 let mut bytes = 0;
1809 RedDB::collect_native_document_entries_from_json(
1810 "body",
1811 &json,
1812 &mut entries,
1813 &mut bytes,
1814 4096,
1815 )
1816 .expect("small document fits");
1817 assert_eq!(
1818 entries,
1819 vec![
1820 ("body.a.b".into(), "1".into()),
1821 ("body.z[0]".into(), "2".into()),
1822 ("body.z[1]".into(), "3".into())
1823 ]
1824 );
1825 let mut deep = JsonValue::Integer(1);
1826 for _ in 0..64 {
1827 deep = JsonValue::Object([("child".to_string(), deep)].into());
1828 }
1829 let mut entries = Vec::new();
1830 let mut bytes = 0;
1831 assert!(RedDB::collect_native_document_entries_from_json(
1832 "body",
1833 &deep,
1834 &mut entries,
1835 &mut bytes,
1836 128
1837 )
1838 .is_err());
1839 assert!(bytes <= 128);
1840 }
1841}