Skip to main content

reddb_server/storage/unified/devx/reddb/
impl_access.rs

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    /// Access the quorum coordinator (Phase 2.6 PG parity).
10    ///
11    /// `None` when this instance is not a primary. Write paths call
12    /// `quorum.wait_for_quorum(lsn)` after the primary WAL commit to
13    /// block until the configured replica quorum has acked; `Async`
14    /// mode (default) returns instantly so no behavioural change.
15    pub fn quorum_coordinator(
16        &self,
17    ) -> Option<&Arc<crate::replication::quorum::QuorumCoordinator>> {
18        self.quorum.as_ref()
19    }
20
21    /// Wait for the configured replica quorum on a given LSN.
22    ///
23    /// Convenience wrapper that returns `Ok(())` when:
24    /// * there is no quorum coordinator (non-primary instance), OR
25    /// * the coordinator is in `Async` mode (historical behaviour).
26    ///
27    /// Callers in the write path (INSERT/UPDATE/DELETE dispatch) invoke
28    /// this after logging the commit record. Returning `Err` signals
29    /// that replication did NOT reach quorum — the primary still has
30    /// the record but the client ack should be deferred or rejected.
31    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        // These optional derived snapshots share a small, conservative slice
157        // of the process budget. Their builders amplify source memory through
158        // tokens, postings and encoded frames, so bound input before copying.
159        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        // Use a lightweight catalog snapshot that does NOT call physical_metadata()
1093        // to avoid infinite recursion: physical_metadata → metadata_from_native_state
1094        // → physical_index_state → catalog_model_snapshot → physical_metadata → ...
1095        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, // No declarations — breaks the recursive cycle
1101            None, // No contracts — avoids pulling physical metadata during bootstrap
1102        );
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            // This infallible snapshot only observes resident entities. A cold
1111            // base, including one with a resident overlay, cannot supply an
1112            // inferred model here. The caller reconciles its durable index
1113            // descriptors from the native registry and physical metadata.
1114            // Treating an unobserved base as Mixed invents graph/search indexes
1115            // on ordinary tables each time metadata is republished.
1116            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    // ========================================================================
1191    // Reference Helpers - For Metadata Linking
1192    // ========================================================================
1193
1194    /// Create a reference to a table row
1195    pub fn table_ref(&self, table: impl Into<String>, row_id: u64) -> TableRef {
1196        TableRef::new(table, row_id)
1197    }
1198
1199    /// Create a reference to a graph node
1200    pub fn node_ref(&self, collection: impl Into<String>, node_id: EntityId) -> NodeRef {
1201        NodeRef::new(collection, node_id)
1202    }
1203
1204    /// Create a reference to a vector
1205    pub fn vector_ref(&self, collection: impl Into<String>, vector_id: EntityId) -> VectorRef {
1206        VectorRef::new(collection, vector_id)
1207    }
1208
1209    // ========================================================================
1210    // Query API
1211    // ========================================================================
1212
1213    /// Start building a query
1214    pub fn query(&self) -> QueryBuilder {
1215        QueryBuilder::new(self.store.clone())
1216    }
1217
1218    /// Quick vector similarity search.
1219    ///
1220    /// For collections with >= 100 vectors a lazily-built HNSW index is used
1221    /// for fast approximate nearest-neighbor lookup.  Smaller collections fall
1222    /// back to an exact brute-force scan so that the overhead of building an
1223    /// index is avoided when it would not pay off.
1224    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        // Try the HNSW fast path for collections with enough vectors.
1230        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        // Fallback: brute-force scan (small / mixed-type collections).
1240        self.similar_brute_force(collection, vector, k)
1241    }
1242
1243    /// Brute-force cosine similarity scan (exact results, O(n)).
1244    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    /// Return (or lazily build) a per-collection HNSW index.
1291    ///
1292    /// Returns `None` when the collection has fewer than 100 dense vectors
1293    /// (the brute-force path is cheaper for tiny collections) or when there
1294    /// is a dimension mismatch with the query vector.
1295    ///
1296    /// The cached index is automatically invalidated when the live entity
1297    /// count in the collection changes, so inserts and deletes are picked up
1298    /// transparently without requiring explicit invalidation calls.
1299    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        // Fast path: check if a fresh index already exists.
1308        {
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        // Either no cached index exists or it is stale -- (re)build it.
1321        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        // Only build the HNSW index when there are enough vectors to justify it.
1334        const MIN_VECTORS_FOR_HNSW: usize = 100;
1335        if vectors.len() < MIN_VECTORS_FOR_HNSW {
1336            return None;
1337        }
1338
1339        // Build the HNSW index with cosine distance (matching the brute-force path).
1340        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        // Store in the cache (double-check pattern to avoid duplicate builds).
1353        let mut indexes = self
1354            .vector_indexes
1355            .write()
1356            .unwrap_or_else(|e| e.into_inner());
1357        // Re-check under write lock: another thread may have built in the meantime.
1358        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    /// Convert HNSW `DistanceResult`s back to the DevX `SimilarResult` type.
1374    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                // Cosine distance = 1 - similarity.
1385                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    /// Invalidate the cached HNSW index for a collection.
1401    ///
1402    /// Called after vector inserts / deletes so the next search lazily rebuilds
1403    /// a fresh index that includes the new data.
1404    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    /// Get entity by ID from any collection
1413    pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
1414        self.store.get_any(id).map(|(_, e)| e)
1415    }
1416
1417    /// Get entity with its collection name
1418    pub fn get_with_collection(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1419        self.store.get_any(id)
1420    }
1421
1422    // ========================================================================
1423    // Batch Operations - Performance
1424    // ========================================================================
1425
1426    /// Batch get multiple entities by ID. More efficient than N individual get() calls.
1427    pub fn batch_get(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1428        ids.iter().map(|id| self.get(*id)).collect()
1429    }
1430
1431    /// Start a batch operation for bulk inserts
1432    pub fn batch(&self) -> BatchBuilder {
1433        BatchBuilder::new(self.store.clone(), self.preprocessors.clone())
1434    }
1435
1436    // ========================================================================
1437    // Preprocessing
1438    // ========================================================================
1439
1440    /// Add a preprocessor hook
1441    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    // ========================================================================
1450    // Cross-Reference Navigation
1451    // ========================================================================
1452
1453    /// Get all entities linked FROM the given entity
1454    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    /// Get all entities linked TO the given entity
1471    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    /// Get the underlying store (for advanced operations)
1488    pub fn store(&self) -> Arc<UnifiedStore> {
1489        self.store.clone()
1490    }
1491
1492    /// Per-process registry of `vector.turbo` collection state (issue
1493    /// #693). Lazily allocated on first access; the inner map holds an
1494    /// `Arc<TurboCollectionState>` per turbo collection so concurrent
1495    /// SEARCHers and INSERTers can share the in-memory index without
1496    /// holding the outer map lock for the whole operation.
1497    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    /// Look up (or lazily create) the runtime state for a turbo
1512    /// collection. Returns `None` for collections that are not marked
1513    /// `turbo` in the catalog.
1514    ///
1515    /// Lazy creation matters for two reasons: (a) a fresh process
1516    /// reopens a persistent DB without any turbo state in memory; (b)
1517    /// the contract (dim + metric) is read here instead of being
1518    /// stored separately, which keeps the catalog as the single source
1519    /// of truth.
1520    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        // Issue #674 — derive the `.tv` snapshot path from the
1547        // resolved tier layout if the active layout enables snapshot
1548        // files. `Minimal` (embedded) returns `None` and the state
1549        // never writes a `.tv` for this collection.
1550        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        // Issue #673 — first materialisation of this collection's
1561        // state kicks off a background rebuild worker. Non-vector
1562        // traffic is unaffected (it never reaches `turbo_state`);
1563        // vector SEARCH/INSERT observes the readiness flag via
1564        // `wait_until_ready` instead of blocking inline on the
1565        // synchronous lazy populate.
1566        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    /// Lazily-initialised ML runtime. First caller wins; subsequent
1578    /// callers observe the same instance. The runtime is created
1579    /// with an in-memory persistence backend by default — production
1580    /// deployments that need durable model state will swap this for
1581    /// a store-backed backend when the persistence integration lands.
1582    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                // No-op work fn — the SQL scalars call predict
1586                // synchronously; async training jobs come in a later
1587                // sprint alongside `CREATE MODEL`.
1588                |_handle| Ok(String::new()),
1589            ))
1590        })
1591    }
1592
1593    /// Shared semantic cache for `SEMANTIC_CACHE_*` scalars. Uses
1594    /// default config until a `SET SEMANTIC_CACHE` admin surface
1595    /// lands.
1596    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    /// Lazily-initialised hypertable registry. Populated by `CREATE
1605    /// HYPERTABLE` DDL; consumed by chunk routing + retention.
1606    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    /// Lazily-initialised continuous-aggregate engine. Populated by
1612    /// `CA_REGISTER` and read by `CA_REFRESH` / `CA_STATE` scalars.
1613    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}