1use super::*;
2
3impl UnifiedStore {
4 pub(crate) fn insert_vault_version(
8 &self,
9 collection: &str,
10 entity: UnifiedEntity,
11 metadata: Metadata,
12 ) -> Result<(), StoreError> {
13 assert!(matches!(entity.data, EntityData::Row(_)));
14 let manager = self
15 .get_collection(collection)
16 .ok_or_else(|| StoreError::CollectionNotFound(collection.into()))?;
17 self.pending_vault_versions.write().insert(entity.id);
18 let id = manager.insert(entity)?;
19 self.register_entity_id(id);
20 if !metadata.fields.is_empty() {
21 manager.set_metadata(id, metadata)?;
22 }
23 let entity = manager
24 .get(id)
25 .expect("inserted vault version remains in memory");
26 self.persist_entities_to_pager_wal_only(collection, &[entity])
27 }
28
29 pub(crate) fn persist_entities_to_pager(
30 &self,
31 collection: &str,
32 entities: &[UnifiedEntity],
33 ) -> Result<(), StoreError> {
34 self.persist_entities_impl(collection, entities, false)
35 }
36
37 pub(crate) fn persist_entity_refs_to_pager(
41 &self,
42 collection: &str,
43 entities: &[&UnifiedEntity],
44 ) -> Result<(), StoreError> {
45 self.persist_entity_refs_impl(collection, entities, false)
46 }
47
48 pub(crate) fn persist_entity_refs_to_pager_wal_only(
51 &self,
52 collection: &str,
53 entities: &[&UnifiedEntity],
54 ) -> Result<(), StoreError> {
55 self.persist_entity_refs_impl(collection, entities, true)
56 }
57
58 fn persist_entity_refs_impl(
59 &self,
60 collection: &str,
61 entities: &[&UnifiedEntity],
62 skip_btree: bool,
63 ) -> Result<(), StoreError> {
64 let skip_btree = skip_btree || self.config.resident_memory_bytes_max.is_some();
65 if entities.is_empty() {
66 return Ok(());
67 }
68 if self.pager.is_none() && self.config.embedded_wal_path.is_none() {
69 return Ok(());
70 }
71 let fv = self.format_version();
72 let manager = self
73 .get_collection(collection)
74 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
75 let mut serialized: Vec<(Vec<u8>, Vec<u8>)> = entities
76 .iter()
77 .map(|entity| {
78 let metadata = manager.get_metadata(entity.id);
79 (
80 entity.id.raw().to_be_bytes().to_vec(),
81 Self::serialize_entity_record(entity, metadata.as_ref(), fv),
82 )
83 })
84 .collect();
85 serialized.sort_by(|a, b| a.0.cmp(&b.0));
86
87 if !skip_btree
88 && !Self::deferred_store_wal_capture_active()
89 && self.config.resident_memory_bytes_max.is_none()
90 {
91 let Some(pager) = &self.pager else {
92 let records: Vec<Vec<u8>> =
93 serialized.into_iter().map(|(_id, record)| record).collect();
94 self.finish_paged_write([StoreWalAction::BulkUpsertEntityRecords {
95 collection: collection.to_string(),
96 records,
97 }])?;
98 return Ok(());
99 };
100 let mut btree_indices = self.btree_indices.write();
101 let btree = btree_indices
102 .entry(collection.to_string())
103 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
104 let root_before = btree.root_page_id();
105 btree.upsert_batch_sorted(&serialized).map_err(|e| {
106 StoreError::Io(std::io::Error::other(format!("B-tree upsert error: {}", e)))
107 })?;
108 let root_after = btree.root_page_id();
109 drop(btree_indices);
110 if root_before != root_after {
111 self.mark_paged_registry_dirty();
112 }
113 }
114 let records: Vec<Vec<u8>> = serialized.into_iter().map(|(_id, record)| record).collect();
115 self.finish_paged_write_for_projection(
116 [StoreWalAction::BulkUpsertEntityRecords {
117 collection: collection.to_string(),
118 records,
119 }],
120 skip_btree,
121 )?;
122 Ok(())
123 }
124
125 pub(crate) fn persist_entities_to_pager_wal_only(
137 &self,
138 collection: &str,
139 entities: &[UnifiedEntity],
140 ) -> Result<(), StoreError> {
141 self.persist_entities_impl(collection, entities, true)
142 }
143
144 fn persist_entities_impl(
145 &self,
146 collection: &str,
147 entities: &[UnifiedEntity],
148 skip_btree: bool,
149 ) -> Result<(), StoreError> {
150 let skip_btree = skip_btree || self.config.resident_memory_bytes_max.is_some();
151 if entities.is_empty() {
152 return Ok(());
153 }
154
155 if self.pager.is_none() && self.config.embedded_wal_path.is_none() {
156 return Ok(());
157 }
158
159 let fv = self.format_version();
160 let manager = self
161 .get_collection(collection)
162 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
163 let mut serialized: Vec<(Vec<u8>, Vec<u8>)> = entities
164 .iter()
165 .map(|entity| {
166 let metadata = manager.get_metadata(entity.id);
167 (
168 entity.id.raw().to_be_bytes().to_vec(),
169 Self::serialize_entity_record(entity, metadata.as_ref(), fv),
170 )
171 })
172 .collect();
173 serialized.sort_by(|a, b| a.0.cmp(&b.0));
175
176 if !skip_btree
177 && !Self::deferred_store_wal_capture_active()
178 && self.config.resident_memory_bytes_max.is_none()
179 {
180 let Some(pager) = &self.pager else {
181 let records: Vec<Vec<u8>> =
182 serialized.into_iter().map(|(_id, record)| record).collect();
183 self.finish_paged_write([StoreWalAction::BulkUpsertEntityRecords {
184 collection: collection.to_string(),
185 records,
186 }])?;
187 return Ok(());
188 };
189 let mut btree_indices = self.btree_indices.write();
190 let btree = btree_indices
191 .entry(collection.to_string())
192 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
193 let root_before = btree.root_page_id();
194
195 btree.upsert_batch_sorted(&serialized).map_err(|e| {
199 StoreError::Io(std::io::Error::other(format!("B-tree upsert error: {}", e)))
200 })?;
201 let root_after = btree.root_page_id();
202 drop(btree_indices);
203 if root_before != root_after {
204 self.mark_paged_registry_dirty();
205 }
206 }
207 let records: Vec<Vec<u8>> = serialized.into_iter().map(|(_id, record)| record).collect();
217 self.finish_paged_write_for_projection(
218 [StoreWalAction::BulkUpsertEntityRecords {
219 collection: collection.to_string(),
220 records,
221 }],
222 skip_btree,
223 )?;
224
225 Ok(())
226 }
227
228 pub(crate) fn update_graph_label_index(
230 &self,
231 collection: &str,
232 label: &str,
233 entity_id: EntityId,
234 ) {
235 self.resident
236 .update_graph_label_index(collection, label, entity_id)
237 }
238
239 pub(crate) fn remove_from_graph_label_index(&self, collection: &str, entity_id: EntityId) {
241 self.resident
242 .remove_from_graph_label_index(collection, entity_id)
243 }
244
245 pub(crate) fn remove_from_graph_label_index_batch(
246 &self,
247 collection: &str,
248 entity_ids: &[EntityId],
249 ) {
250 self.resident
251 .remove_from_graph_label_index_batch(collection, entity_ids)
252 }
253
254 pub fn lookup_graph_nodes_by_label(&self, label: &str) -> Vec<EntityId> {
256 self.resident.lookup_graph_nodes_by_label(label)
257 }
258
259 pub fn lookup_graph_nodes_by_label_in(&self, collection: &str, label: &str) -> Vec<EntityId> {
261 self.resident
262 .lookup_graph_nodes_by_label_in(collection, label)
263 }
264
265 pub fn create_collection(&self, name: impl Into<String>) -> Result<(), StoreError> {
266 let name = name.into();
267 let mut collections = self.resident.collections.write();
268
269 if collections.contains_key(&name) {
270 return Err(StoreError::CollectionExists(name));
271 }
272
273 let manager = SegmentManager::with_config(&name, self.config.manager_config.clone());
274 collections.insert(name.clone(), Arc::new(manager));
275 drop(collections);
276
277 let create = match self.prepare_operational_collection_create(&name) {
278 Ok(create) => create,
279 Err(err) => {
280 let mut collections = self.resident.collections.write();
281 collections.remove(&name);
282 return Err(err);
283 }
284 };
285 if let Some(create) = create {
286 if let Err(err) = create.publish().map_err(StoreError::Io) {
287 let mut collections = self.resident.collections.write();
288 collections.remove(&name);
289 return Err(err);
290 }
291 }
292
293 self.mark_paged_registry_dirty();
294 self.finish_paged_write([StoreWalAction::CreateCollection { name }])?;
295
296 Ok(())
297 }
298
299 pub fn get_or_create_collection(&self, name: impl Into<String>) -> Arc<SegmentManager> {
301 self.resident
302 .get_or_create_collection(name, &self.config.manager_config)
303 }
304
305 pub fn get_collection(&self, name: &str) -> Option<Arc<SegmentManager>> {
307 self.resident.get_collection(name)
308 }
309
310 pub fn context_index(&self) -> &ContextIndex {
312 self.resident.context_index()
313 }
314
315 pub fn take_replayed_turbo_inserts(&self, collection: &str) -> Option<Vec<(u64, Vec<f32>)>> {
321 let mut map = self.replayed_turbo_inserts.lock();
322 map.remove(collection)
323 }
324
325 pub(crate) fn take_replayed_probabilistic_deltas(&self) -> Vec<(u8, u8, String, Vec<Vec<u8>>)> {
326 let mut deltas = self.replayed_probabilistic_deltas.lock();
327 std::mem::take(&mut *deltas)
328 }
329
330 pub fn set_config_tree(&self, prefix: &str, json: &crate::serde_json::Value) -> usize {
337 self.try_set_config_tree(prefix, json).unwrap_or_default()
340 }
341
342 pub fn try_set_config_tree(
343 &self,
344 prefix: &str,
345 json: &crate::serde_json::Value,
346 ) -> Result<usize, StoreError> {
347 let _publication = self.vault_publication_lock.lock();
348 let manager = self.get_or_create_collection("red_config");
349 let mut pairs = Vec::new();
350 flatten_config_json(prefix, json, &mut pairs);
351 let mut previous: HashMap<String, Vec<(EntityId, Option<reddb_types::Value>)>> = pairs
353 .iter()
354 .map(|(key, _)| (key.clone(), Vec::new()))
355 .collect();
356 let mut capture = |entity: &UnifiedEntity| {
357 if let EntityData::Row(row) = &entity.data {
358 if let Some(reddb_types::Value::Text(key)) = row.get_field("key") {
359 if let Some(rows) = previous.get_mut(key.as_ref()) {
360 rows.push((entity.id, row.get_field("value").cloned()));
361 }
362 }
363 }
364 true
365 };
366 if manager.has_backing() {
367 for (key, _) in &pairs {
368 manager.try_for_each_key_entity(key, &mut capture)?;
369 }
370 } else {
371 manager.try_for_each_entity(usize::MAX, &mut capture)?;
372 }
373 let mut saved = 0;
374 for (key, value) in pairs {
375 let stale = previous.remove(&key).unwrap_or_default();
376 if let [(_, Some(current))] = stale.as_slice() {
380 if *current == value {
381 saved += 1;
382 continue;
383 }
384 }
385 let stale: Vec<EntityId> = stale.into_iter().map(|(id, _)| id).collect();
386 let entity = UnifiedEntity::new(
390 EntityId::new(0),
391 EntityKind::TableRow {
392 table: Arc::from("red_config"),
393 row_id: 0,
394 },
395 EntityData::Row(RowData {
396 columns: Vec::new(),
397 named: Some(
398 [
399 ("key".to_string(), reddb_types::Value::text(key)),
400 ("value".to_string(), value),
401 ]
402 .into_iter()
403 .collect(),
404 ),
405 schema: None,
406 }),
407 );
408 self.insert_auto("red_config", entity)?;
409 saved += 1;
410 if !stale.is_empty() {
411 self.delete_batch("red_config", &stale)?;
412 }
413 }
414 Ok(saved)
415 }
416
417 pub fn get_config(&self, key: &str) -> Option<reddb_types::Value> {
421 self.try_get_config(key).ok().flatten()
424 }
425
426 pub fn try_get_config(&self, key: &str) -> Result<Option<reddb_types::Value>, StoreError> {
427 let Some(manager) = self.get_collection("red_config") else {
428 return Ok(None);
429 };
430 let _publication = manager
431 .has_backing()
432 .then(|| self.vault_publication_lock.lock());
433 let mut latest: Option<(u64, reddb_types::Value)> = None;
434 manager.try_for_each_key_entity(key, |entity| {
435 if let EntityData::Row(row) = &entity.data {
436 let key_matches = matches!(row.get_field("key"), Some(reddb_types::Value::Text(stored)) if stored.as_ref() == key);
437 if key_matches && latest.as_ref().is_none_or(|(id, _)| entity.id.raw() >= *id) {
438 if let Some(value) = row.get_field("value") {
439 latest = Some((entity.id.raw(), value.clone()));
440 }
441 }
442 }
443 true
444 })?;
445 Ok(latest.map(|(_, value)| value))
446 }
447
448 pub fn set_aux_metadata(&self, bytes: Vec<u8>) {
453 self.resident.set_aux_metadata(bytes)
454 }
455
456 pub(crate) fn persist_aux_metadata(&self, bytes: Vec<u8>) -> Result<(), StoreError> {
459 let _publication = self.vault_publication_lock.lock();
460 self.finish_paged_write([StoreWalAction::SetAuxMetadata {
461 bytes: bytes.clone(),
462 }])?;
463 self.resident.set_aux_metadata(bytes);
464 Ok(())
465 }
466
467 pub fn aux_metadata(&self) -> Vec<u8> {
469 self.resident.aux_metadata()
470 }
471
472 pub fn list_collections(&self) -> Vec<String> {
474 self.resident.list_collections()
475 }
476
477 pub fn drop_collection(&self, name: &str) -> Result<(), StoreError> {
479 let _vault_guard = self.vault_publication_lock.lock();
480 let drop = self.prepare_operational_collection_drop(name)?;
481 let manager = {
482 let mut collections = self.resident.collections.write();
483
484 collections
485 .remove(name)
486 .ok_or_else(|| StoreError::CollectionNotFound(name.to_string()))?
487 };
488
489 let entities = manager.query_all(|_| true);
490 let entity_ids: Vec<EntityId> = entities.iter().map(|entity| entity.id).collect();
491
492 for entity_id in &entity_ids {
493 self.resident.context_index.remove_entity(*entity_id);
494 let _ = self.unindex_cross_refs(*entity_id);
495 }
496
497 if let Some(tree) = self.btree_indices.write().remove(name) {
498 self.retire_btree(tree);
499 }
500 if let Some(index) = self.key_indices.write().remove(name) {
501 for tree in index.trees() {
502 self.retire_btree(Arc::clone(tree));
503 }
504 }
505 self.secondary_index_specs.write().remove(name);
506
507 self.resident
508 .entity_cache
509 .retain(|entity_id, (collection, _)| {
510 collection != name && !entity_ids.iter().any(|id| id.raw() == entity_id)
511 });
512
513 self.resident.cross_refs.write().retain(|source_id, refs| {
514 refs.retain(|(target_id, _, target_collection)| {
515 target_collection != name && !entity_ids.iter().any(|id| id == target_id)
516 });
517 !entity_ids.iter().any(|id| id == source_id)
518 });
519
520 self.resident
521 .reverse_refs
522 .write()
523 .retain(|target_id, refs| {
524 refs.retain(|(source_id, _, source_collection)| {
525 source_collection != name && !entity_ids.iter().any(|id| id == source_id)
526 });
527 !entity_ids.iter().any(|id| id == target_id)
528 });
529
530 self.mark_paged_registry_dirty();
531 self.finish_paged_write([StoreWalAction::DropCollection {
532 name: name.to_string(),
533 }])?;
534 if let Some(drop) = drop {
535 drop.finish().map_err(StoreError::Io)?;
536 }
537
538 Ok(())
539 }
540
541 pub fn insert(&self, collection: &str, entity: UnifiedEntity) -> Result<EntityId, StoreError> {
543 let manager = self
544 .get_collection(collection)
545 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
546
547 let mut entity = entity;
548 if entity.id.raw() == 0 {
549 entity.id = self.next_entity_id();
550 } else {
551 self.register_entity_id(entity.id);
552 }
553 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
555 if *row_id == 0 {
556 *row_id = manager.next_row_id();
557 } else {
558 manager.register_row_id(*row_id);
559 }
560 }
561 entity.ensure_table_logical_id();
562 let graph_node_label: Option<String> = if let EntityKind::GraphNode(ref node) = entity.kind
564 {
565 Some(node.label.clone())
566 } else {
567 None
568 };
569
570 let id = manager.insert(entity)?;
571 self.register_entity_id(id);
572
573 if let Some(ref label) = graph_node_label {
575 self.update_graph_label_index(collection, label, id);
576 }
577
578 let mut registry_dirty = false;
581 if let Some(pager) = self.pager.as_ref().filter(|_| {
582 !Self::deferred_store_wal_capture_active()
583 && self.config.resident_memory_bytes_max.is_none()
584 }) {
585 if let Some(entity) = manager.get(id) {
586 let mut btree_indices = self.btree_indices.write();
587 let btree = btree_indices
588 .entry(collection.to_string())
589 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
590 let root_before = btree.root_page_id();
591
592 let key = id.raw().to_be_bytes();
593 let metadata = manager.get_metadata(id);
594 let value = Self::serialize_entity_record(
595 &entity,
596 metadata.as_ref(),
597 self.format_version(),
598 );
599 let _ = btree.insert(&key, &value);
601 registry_dirty = root_before != btree.root_page_id();
602 }
603 }
604
605 if self.config.auto_index_refs {
607 if let Some(entity) = manager.get(id) {
608 self.index_cross_refs(&entity, collection)?;
609 }
610 }
611
612 if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
616 let actions = manager
617 .get(id)
618 .map(|entity| {
619 let metadata = manager.get_metadata(id);
620 vec![StoreWalAction::upsert_entity(
621 collection,
622 &entity,
623 metadata.as_ref(),
624 self.format_version(),
625 )]
626 })
627 .unwrap_or_default();
628 if registry_dirty {
629 self.mark_paged_registry_dirty();
630 }
631 self.finish_paged_write(actions)?;
632 }
633
634 Ok(id)
635 }
636
637 pub fn bulk_insert(
642 &self,
643 collection: &str,
644 mut entities: Vec<UnifiedEntity>,
645 ) -> Result<Vec<EntityId>, StoreError> {
646 let _publication = self.vault_publication_lock.lock();
649 let trace = matches!(
653 std::env::var("REDDB_BULK_TIMING").ok().as_deref(),
654 Some("1") | Some("true") | Some("on")
655 );
656 let t_start = std::time::Instant::now();
657 let n = entities.len();
658 let manager = self.get_or_create_collection(collection);
659 let t_get_coll = t_start.elapsed();
660
661 let t0 = std::time::Instant::now();
670 let n_missing_entity_ids = entities.iter().filter(|e| e.id.raw() == 0).count() as u64;
671 let n_missing_row_ids = entities
672 .iter()
673 .filter(|e| matches!(e.kind, EntityKind::TableRow { row_id: 0, .. }))
674 .count() as u64;
675 let mut entity_id_range = if n_missing_entity_ids > 0 {
676 self.reserve_entity_ids(n_missing_entity_ids)
677 } else {
678 0..0
679 };
680 let mut row_id_range = if n_missing_row_ids > 0 {
681 manager.reserve_row_ids(n_missing_row_ids)
682 } else {
683 0..0
684 };
685 for entity in &mut entities {
686 if entity.id.raw() == 0 {
687 let next = entity_id_range
688 .next()
689 .expect("reserved entity-id range exhausted");
690 entity.id = EntityId::new(next);
691 } else {
692 self.register_entity_id(entity.id);
693 }
694 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
695 if *row_id == 0 {
696 *row_id = row_id_range
697 .next()
698 .expect("reserved row-id range exhausted");
699 } else {
700 manager.register_row_id(*row_id);
701 }
702 }
703 entity.ensure_table_logical_id();
704 }
705 let t_assign_ids = t0.elapsed();
706
707 let graph_labels: Vec<Option<(String, EntityId)>> = entities
709 .iter()
710 .map(|e| {
711 if let EntityKind::GraphNode(ref node) = e.kind {
712 Some((node.label.clone(), e.id))
713 } else {
714 None
715 }
716 })
717 .collect();
718
719 let t0 = std::time::Instant::now();
722 let ids = manager.bulk_insert(entities)?;
723 let t_manager = t0.elapsed();
724 for id in &ids {
725 self.register_entity_id(*id);
726 }
727
728 let t0 = std::time::Instant::now();
735 let serialized: Option<Vec<(Vec<u8>, Vec<u8>)>> =
736 if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
737 let fv = self.format_version();
738 let serial_map = |id: &EntityId| {
739 manager
740 .get_with(*id, |entity| {
741 (
742 id.raw().to_be_bytes().to_vec(),
743 Self::serialize_entity_record(entity, None, fv),
744 )
745 })
746 .expect("bulk-inserted entity must remain in the fenced segment")
747 };
748 if ids.len() >= 512 {
754 use rayon::prelude::*;
755 Some(ids.par_iter().map(serial_map).collect())
756 } else {
757 Some(ids.iter().map(serial_map).collect())
758 }
759 } else {
760 None
761 };
762 let t_serialize = t0.elapsed();
763
764 for (label, entity_id) in graph_labels.iter().flatten() {
766 self.update_graph_label_index(collection, label, *entity_id);
767 }
768
769 let skip_btree_requested = matches!(
779 std::env::var("REDDB_BULK_SKIP_PERSIST_UNSAFE")
780 .ok()
781 .as_deref(),
782 Some("1") | Some("true") | Some("on")
783 );
784 let skip_btree = skip_btree_requested && self.pager.is_none();
787 if skip_btree_requested && !skip_btree {
788 static IGNORED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
790 IGNORED.get_or_init(|| {
791 tracing::warn!(
792 "REDDB_BULK_SKIP_PERSIST_UNSAFE set but durable pager is \
793 active — flag ignored; bulk inserts will be persisted normally"
794 );
795 });
796 } else if skip_btree {
797 static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
799 WARNED.get_or_init(|| {
800 tracing::warn!(
801 "REDDB_BULK_SKIP_PERSIST_UNSAFE set (ephemeral/no-pager mode) — \
802 bulk inserts NOT durable; data will be lost on restart"
803 );
804 });
805 }
806
807 let mut t_btree_lock = std::time::Duration::ZERO;
811 let mut t_btree_insert = std::time::Duration::ZERO;
812 let mut t_flush = std::time::Duration::ZERO;
813 if !skip_btree
814 && !Self::deferred_store_wal_capture_active()
815 && self.config.resident_memory_bytes_max.is_none()
816 {
817 if let (Some(pager), Some(batch)) = (&self.pager, serialized.as_ref()) {
818 let t0 = std::time::Instant::now();
819 let mut btree_indices = self.btree_indices.write();
820 let btree = btree_indices
821 .entry(collection.to_string())
822 .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
823 let root_before = btree.root_page_id();
824 t_btree_lock = t0.elapsed();
825
826 let t0 = std::time::Instant::now();
827 let _ = btree.bulk_insert_sorted(batch);
828 t_btree_insert = t0.elapsed();
829 let registry_dirty = root_before != btree.root_page_id();
830
831 let t0 = std::time::Instant::now();
832 if registry_dirty {
833 self.mark_paged_registry_dirty();
834 }
835 t_flush = t0.elapsed();
836 }
837 }
838
839 let actions = serialized
845 .map(|batch| {
846 let records: Vec<Vec<u8>> =
847 batch.into_iter().map(|(_key, record)| record).collect();
848 vec![StoreWalAction::BulkUpsertEntityRecords {
849 collection: collection.to_string(),
850 records,
851 }]
852 })
853 .unwrap_or_default();
854 self.finish_paged_write(actions)?;
855
856 if trace {
857 tracing::debug!(
858 n,
859 total = ?t_start.elapsed(),
860 get_coll = ?t_get_coll,
861 assign = ?t_assign_ids,
862 serialize = ?t_serialize,
863 manager = ?t_manager,
864 btree_lock = ?t_btree_lock,
865 btree = ?t_btree_insert,
866 flush = ?t_flush,
867 "bulk_insert timing"
868 );
869 }
870
871 Ok(ids)
872 }
873
874 pub fn insert_auto(
876 &self,
877 collection: &str,
878 entity: UnifiedEntity,
879 ) -> Result<EntityId, StoreError> {
880 let _publication = self.vault_publication_lock.lock();
882 let manager = self.get_or_create_collection(collection);
883 let mut entity = entity;
884 if entity.id.raw() == 0 {
885 entity.id = self.next_entity_id();
886 } else {
887 self.register_entity_id(entity.id);
888 }
889 if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
891 if *row_id == 0 {
892 *row_id = manager.next_row_id();
893 } else {
894 manager.register_row_id(*row_id);
895 }
896 }
897 entity.ensure_table_logical_id();
898
899 let graph_node_label: Option<String> = if let EntityKind::GraphNode(ref node) = entity.kind
901 {
902 Some(node.label.clone())
903 } else {
904 None
905 };
906 self.resident
908 .context_index
909 .index_entity(collection, &entity);
910
911 let persist_record = self.pager.is_some() || self.config.embedded_wal_path.is_some();
914 if self.config.auto_index_refs {
915 self.index_cross_refs(&entity, collection)?;
916 }
917
918 let paged_btree = if persist_record
925 && !Self::deferred_store_wal_capture_active()
926 && self.config.resident_memory_bytes_max.is_none()
927 {
928 self.get_or_create_btree(collection)
929 } else {
930 None
931 };
932 let persist_fence = paged_btree
933 .as_ref()
934 .map(|_| self.btree_indices.read_recursive());
935 let before = paged_btree.as_ref().map(|_| manager.checkpoint_revisions());
936 let unversioned = entity.xmin == 0 && entity.xmax == 0;
937
938 let id = manager.insert(entity)?;
939 let serialized_record = if persist_record {
940 Some(
941 manager
942 .get_with(id, |entity| {
943 Self::serialize_entity_record(entity, None, self.format_version())
944 })
945 .ok_or(StoreError::EntityNotFound(id))?,
946 )
947 } else {
948 None
949 };
950 if let Some(ref label) = graph_node_label {
958 self.update_graph_label_index(collection, label, id);
959 }
960
961 let mut registry_dirty = false;
962 if let (Some(btree), Some(record)) = (paged_btree.as_ref(), serialized_record.as_ref()) {
963 let root_before = btree.root_page_id();
964
965 let key = id.raw().to_be_bytes();
966 btree.insert(&key, record).map_err(|e| {
967 StoreError::Io(std::io::Error::other(format!(
968 "B-tree insert error while inserting '{collection}'/{id}: {e}"
969 )))
970 })?;
971 registry_dirty = root_before != btree.root_page_id();
972 }
973 drop(persist_fence);
974
975 if self.pager.is_some() || self.config.embedded_wal_path.is_some() {
979 let actions = serialized_record
980 .map(|record| {
981 vec![StoreWalAction::UpsertEntityRecord {
982 collection: collection.to_string(),
983 record,
984 }]
985 })
986 .unwrap_or_default();
987 if registry_dirty {
988 self.mark_paged_registry_dirty();
989 }
990 if let Some(pending) = self.enqueue_grouped_actions(&actions)? {
991 drop(_publication);
992 self.complete_grouped_actions(&pending, &actions)?;
993 } else {
994 self.finish_paged_write(actions)?;
995 }
996 }
997 if unversioned && !Self::deferred_store_wal_capture_active() {
998 if let Some(before) = before {
999 self.note_paged_insert(collection, &manager, &before, id);
1000 }
1001 }
1002 Ok(id)
1003 }
1004
1005 pub fn get(&self, collection: &str, id: EntityId) -> Option<UnifiedEntity> {
1011 self.try_get(collection, id, usize::MAX).ok().flatten()
1014 }
1015
1016 pub fn try_get(
1017 &self,
1018 collection: &str,
1019 id: EntityId,
1020 limit_bytes: usize,
1021 ) -> Result<Option<UnifiedEntity>, StoreError> {
1022 let Some(manager) = self.get_collection(collection) else {
1023 return Ok(None);
1024 };
1025 manager.try_get(id, limit_bytes)
1026 }
1027
1028 pub fn get_table_row_by_logical_id(
1034 &self,
1035 collection: &str,
1036 logical_id: EntityId,
1037 ) -> Option<UnifiedEntity> {
1038 if let Some(entity) = self.get(collection, logical_id) {
1039 if matches!(entity.kind, EntityKind::TableRow { .. })
1040 && entity.logical_id() == logical_id
1041 && entity.xmax == 0
1042 {
1043 return Some(entity);
1044 }
1045 }
1046
1047 let manager = self.get_collection(collection)?;
1048 let mut matches = manager.query_all(|entity| {
1049 matches!(entity.kind, EntityKind::TableRow { .. }) && entity.logical_id() == logical_id
1050 });
1051 matches
1052 .iter()
1053 .find(|entity| entity.xmax == 0)
1054 .cloned()
1055 .or_else(|| matches.pop())
1056 }
1057
1058 pub fn table_row_versions_by_logical_id(
1059 &self,
1060 collection: &str,
1061 logical_id: EntityId,
1062 ) -> Vec<UnifiedEntity> {
1063 self.resident
1064 .table_row_versions_by_logical_id(collection, logical_id)
1065 }
1066
1067 pub(crate) fn try_for_each_table_row_version(
1068 &self,
1069 collection: &str,
1070 logical_id: EntityId,
1071 mut visit: impl FnMut(&UnifiedEntity) -> bool,
1072 ) -> Result<(), StoreError> {
1073 let Some(manager) = self.get_collection(collection) else {
1074 return Ok(());
1075 };
1076 manager.try_for_each_entity(usize::MAX, |entity| {
1077 !matches!(entity.kind, EntityKind::TableRow { .. })
1078 || entity.logical_id() != logical_id
1079 || visit(entity)
1080 })
1081 }
1082
1083 pub(crate) fn try_get_table_row_by_logical_id(
1084 &self,
1085 collection: &str,
1086 logical_id: EntityId,
1087 ) -> Result<Option<UnifiedEntity>, StoreError> {
1088 let Some(manager) = self.get_collection(collection) else {
1089 return Ok(None);
1090 };
1091 if let Some(entity) = manager.try_get(logical_id, usize::MAX)? {
1092 if matches!(entity.kind, EntityKind::TableRow { .. })
1093 && entity.logical_id() == logical_id
1094 && entity.xmax == 0
1095 {
1096 return Ok(Some(entity));
1097 }
1098 }
1099 let mut selected = None;
1102 self.try_for_each_table_row_version(collection, logical_id, |entity| {
1103 selected = Some(entity.clone());
1104 entity.xmax != 0
1105 })?;
1106 manager.restore_owned_row_schemas(selected.iter_mut());
1107 Ok(selected)
1108 }
1109
1110 pub fn vacuum_mvcc_history(
1111 &self,
1112 collection: &str,
1113 cutoff_xid: u64,
1114 ) -> Result<MvccVacuumStats, StoreError> {
1115 let manager = self
1116 .get_collection(collection)
1117 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1118 let mut logical = HashMap::<EntityId, (bool, u64)>::new();
1119 manager.for_each_entity(|entity| {
1120 if !matches!(entity.kind, EntityKind::TableRow { .. }) {
1121 return true;
1122 }
1123 let entry = logical.entry(entity.logical_id()).or_insert((false, 0));
1124 if entity.xmax == 0 {
1125 entry.0 = true;
1126 }
1127 entry.1 = entry.1.max(entity.xmin);
1128 true
1129 });
1130
1131 let mut stats = MvccVacuumStats::default();
1132 let mut reclaim_ids = Vec::new();
1133 manager.for_each_entity(|entity| {
1134 if !matches!(entity.kind, EntityKind::TableRow { .. }) || entity.xmax == 0 {
1135 return true;
1136 }
1137 stats.scanned_versions += 1;
1138 let (has_live_version, max_xmin) = logical
1139 .get(&entity.logical_id())
1140 .copied()
1141 .unwrap_or((false, entity.xmin));
1142 let is_delete_tombstone = !has_live_version && entity.xmin == max_xmin;
1143 if entity.xmax < cutoff_xid {
1144 stats.reclaimed_versions += 1;
1145 if is_delete_tombstone {
1146 stats.reclaimed_tombstones += 1;
1147 } else {
1148 stats.reclaimed_history_versions += 1;
1149 }
1150 reclaim_ids.push(entity.id);
1151 } else {
1152 stats.retained_versions += 1;
1153 if is_delete_tombstone {
1154 stats.retained_tombstones += 1;
1155 } else {
1156 stats.retained_history_versions += 1;
1157 }
1158 }
1159 true
1160 });
1161
1162 if !reclaim_ids.is_empty() {
1163 self.delete_batch(collection, &reclaim_ids)?;
1164 }
1165 Ok(stats)
1166 }
1167
1168 pub(crate) fn install_versioned_table_row_update(
1169 &self,
1170 collection: &str,
1171 old_version: UnifiedEntity,
1172 mut new_version: UnifiedEntity,
1173 metadata: Option<&Metadata>,
1174 ) -> Result<(), StoreError> {
1175 let manager = self
1176 .get_collection(collection)
1177 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1178
1179 let old_id = old_version.id;
1180 let new_id = new_version.id;
1181 let inherited_metadata = metadata.cloned().or_else(|| manager.get_metadata(old_id));
1182
1183 self.resident.entity_cache.remove(old_id.raw());
1184 self.resident.entity_cache.remove(new_id.raw());
1185 manager.update(old_version.clone())?;
1186 self.resident.context_index.remove_entity(old_id);
1187
1188 self.register_entity_id(new_version.id);
1189 if let EntityKind::TableRow { ref mut row_id, .. } = new_version.kind {
1190 if *row_id == 0 {
1191 *row_id = manager.next_row_id();
1192 } else {
1193 manager.register_row_id(*row_id);
1194 }
1195 }
1196 new_version.ensure_table_logical_id();
1197 manager.insert(new_version.clone())?;
1198 if let Some(metadata) = inherited_metadata {
1199 manager.set_metadata(new_id, metadata)?;
1200 }
1201 self.resident
1202 .context_index
1203 .index_entity(collection, &new_version);
1204 if self.config.auto_index_refs {
1205 self.index_cross_refs(&new_version, collection)?;
1206 }
1207
1208 let old_metadata = manager.get_metadata(old_id);
1209 let new_metadata = manager.get_metadata(new_id);
1210 let fv = self.format_version();
1211 let records = vec![
1214 manager
1215 .get_with(old_id, |entity| {
1216 Self::serialize_entity_record(entity, old_metadata.as_ref(), fv)
1217 })
1218 .ok_or(StoreError::EntityNotFound(old_id))?,
1219 manager
1220 .get_with(new_id, |entity| {
1221 Self::serialize_entity_record(entity, new_metadata.as_ref(), fv)
1222 })
1223 .ok_or(StoreError::EntityNotFound(new_id))?,
1224 ];
1225 self.finish_paged_write_for_projection(
1226 [StoreWalAction::BulkUpsertEntityRecords {
1227 collection: collection.to_string(),
1228 records,
1229 }],
1230 true,
1231 )?;
1232
1233 Ok(())
1234 }
1235
1236 pub fn get_batch(&self, collection: &str, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
1242 match self.get_collection(collection) {
1243 Some(manager) => manager.get_many(ids),
1244 None => vec![None; ids.len()],
1245 }
1246 }
1247
1248 pub fn try_get_batch(
1250 &self,
1251 collection: &str,
1252 ids: &[EntityId],
1253 read_bytes_max: usize,
1254 ) -> Result<Vec<Option<UnifiedEntity>>, StoreError> {
1255 match self.get_collection(collection) {
1256 Some(manager) => manager.try_get_many(ids, read_bytes_max),
1257 None => Ok(vec![None; ids.len()]),
1258 }
1259 }
1260
1261 pub fn get_any(&self, id: EntityId) -> Option<(String, UnifiedEntity)> {
1263 self.resident.get_any(id)
1264 }
1265
1266 pub fn entity_cache_hit_rate(&self) -> Option<f64> {
1272 self.resident.entity_cache_hit_rate()
1273 }
1274
1275 pub fn entity_cache_stats(&self) -> super::super::entity_cache::EntityCacheStats {
1277 self.resident.entity_cache_stats()
1278 }
1279
1280 pub fn delete(&self, collection: &str, id: EntityId) -> Result<bool, StoreError> {
1282 let publication = self.vault_publication_lock.lock();
1283 self.resident.entity_cache.remove(id.raw());
1285 let manager = self
1286 .get_collection(collection)
1287 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1288
1289 let deleted = manager.delete(id)?;
1290 if !deleted {
1291 return Ok(false);
1292 }
1293
1294 let mut registry_dirty = false;
1296 if self.pager.is_some() && self.config.resident_memory_bytes_max.is_none() {
1297 let btree_indices = self.btree_indices.read();
1298 if let Some(btree) = btree_indices.get(collection) {
1299 let root_before = btree.root_page_id();
1300 let key = id.raw().to_be_bytes();
1301 let _ = btree.delete(&key);
1302 registry_dirty = root_before != btree.root_page_id();
1303 }
1304 }
1305
1306 self.unindex_cross_refs(id)?;
1308
1309 self.remove_from_graph_label_index(collection, id);
1311
1312 if registry_dirty {
1313 self.mark_paged_registry_dirty();
1314 }
1315 let actions = [StoreWalAction::DeleteEntityRecord {
1316 collection: collection.to_string(),
1317 entity_id: id.raw(),
1318 }];
1319 if let Some(pending) = self.enqueue_grouped_actions(&actions)? {
1320 drop(publication);
1321 self.complete_grouped_actions(&pending, &actions)?;
1322 } else {
1323 self.finish_paged_write(actions)?;
1324 }
1325
1326 Ok(true)
1327 }
1328
1329 pub fn delete_batch(
1330 &self,
1331 collection: &str,
1332 ids: &[EntityId],
1333 ) -> Result<Vec<EntityId>, StoreError> {
1334 if ids.is_empty() {
1335 return Ok(Vec::new());
1336 }
1337 let _publication = self.vault_publication_lock.lock();
1340
1341 self.resident
1346 .entity_cache
1347 .remove_many(ids.iter().map(|id| id.raw()));
1348
1349 let manager = self
1350 .get_collection(collection)
1351 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1352
1353 let before = self.pager.as_ref().map(|_| manager.checkpoint_revisions());
1354 let deleted_ids = manager.delete_batch(ids)?;
1355 if deleted_ids.is_empty() {
1356 return Ok(deleted_ids);
1357 }
1358
1359 let mut registry_dirty = false;
1360 let mut updated_tree = false;
1361 if self.pager.is_some() && self.config.resident_memory_bytes_max.is_none() {
1362 let btree_indices = self.btree_indices.read();
1363 if let Some(btree) = btree_indices.get(collection) {
1364 updated_tree = true;
1365 let root_before = btree.root_page_id();
1366 for id in &deleted_ids {
1367 let key = id.raw().to_be_bytes();
1368 updated_tree &= btree.delete(&key).is_ok();
1371 }
1372 registry_dirty = root_before != btree.root_page_id();
1373 }
1374 }
1375
1376 self.unindex_cross_refs_batch(&deleted_ids)?;
1377 self.remove_from_graph_label_index_batch(collection, &deleted_ids);
1378 if registry_dirty {
1379 self.mark_paged_registry_dirty();
1380 }
1381 let actions = deleted_ids
1382 .iter()
1383 .map(|id| StoreWalAction::DeleteEntityRecord {
1384 collection: collection.to_string(),
1385 entity_id: id.raw(),
1386 })
1387 .collect::<Vec<_>>();
1388 self.finish_paged_write(actions)?;
1389
1390 if updated_tree {
1391 if let Some(before) = before {
1392 self.note_paged_delete(collection, &manager, &before, &deleted_ids);
1393 }
1394 self.flush_paged_registry()?;
1399 }
1400 Ok(deleted_ids)
1401 }
1402
1403 pub fn set_metadata(
1405 &self,
1406 collection: &str,
1407 id: EntityId,
1408 metadata: Metadata,
1409 ) -> Result<(), StoreError> {
1410 let manager = self
1411 .get_collection(collection)
1412 .ok_or_else(|| StoreError::CollectionNotFound(collection.to_string()))?;
1413
1414 manager.set_metadata(id, metadata)?;
1415 if let Some(entity) = manager.get(id) {
1416 self.persist_entities_to_pager(collection, std::slice::from_ref(&entity))?;
1417 }
1418 Ok(())
1419 }
1420
1421 pub fn get_metadata(&self, collection: &str, id: EntityId) -> Option<Metadata> {
1423 self.get_collection(collection)?.get_metadata(id)
1424 }
1425
1426 pub fn try_get_metadata(
1428 &self,
1429 collection: &str,
1430 id: EntityId,
1431 limit_bytes: usize,
1432 ) -> Result<Option<Metadata>, StoreError> {
1433 let Some(manager) = self.get_collection(collection) else {
1434 return Ok(None);
1435 };
1436 manager.try_get_metadata(id, limit_bytes)
1437 }
1438
1439 pub fn add_cross_ref(
1441 &self,
1442 source_collection: &str,
1443 source_id: EntityId,
1444 target_collection: &str,
1445 target_id: EntityId,
1446 ref_type: RefType,
1447 weight: f32,
1448 ) -> Result<(), StoreError> {
1449 let registry_dirty = self.resident.add_cross_ref_in_memory(
1450 source_collection,
1451 source_id,
1452 target_collection,
1453 target_id,
1454 ref_type,
1455 weight,
1456 self.config.max_cross_refs,
1457 |entity| {
1458 self.persist_entities_to_pager(source_collection, std::slice::from_ref(entity))
1459 },
1460 )?;
1461 if registry_dirty
1462 && matches!(
1463 self.config.durability_mode,
1464 crate::api::DurabilityMode::Strict
1465 )
1466 {
1467 self.flush_paged_state()?;
1468 }
1469 Ok(())
1470 }
1471
1472 pub fn get_refs_from(&self, id: EntityId) -> Vec<(EntityId, RefType, String)> {
1474 self.resident.get_refs_from(id)
1475 }
1476
1477 pub fn get_refs_to(&self, id: EntityId) -> Vec<(EntityId, RefType, String)> {
1479 self.resident.get_refs_to(id)
1480 }
1481
1482 pub fn expand_refs(
1484 &self,
1485 id: EntityId,
1486 depth: u32,
1487 ref_types: Option<&[RefType]>,
1488 ) -> Vec<(UnifiedEntity, u32, RefType)> {
1489 let mut results = Vec::new();
1490 let mut visited = std::collections::HashSet::new();
1491 visited.insert(id);
1492
1493 self.expand_refs_recursive(id, depth, ref_types, &mut visited, &mut results, 1);
1494
1495 results
1496 }
1497
1498 fn expand_refs_recursive(
1499 &self,
1500 id: EntityId,
1501 max_depth: u32,
1502 ref_types: Option<&[RefType]>,
1503 visited: &mut std::collections::HashSet<EntityId>,
1504 results: &mut Vec<(UnifiedEntity, u32, RefType)>,
1505 current_depth: u32,
1506 ) {
1507 if current_depth > max_depth {
1508 return;
1509 }
1510
1511 for (target_id, ref_type, target_collection) in self.get_refs_from(id) {
1512 if visited.contains(&target_id) {
1513 continue;
1514 }
1515
1516 if let Some(types) = ref_types {
1517 if !types.contains(&ref_type) {
1518 continue;
1519 }
1520 }
1521
1522 visited.insert(target_id);
1523
1524 if let Some(entity) = self.get(&target_collection, target_id) {
1525 results.push((entity, current_depth, ref_type));
1526
1527 self.expand_refs_recursive(
1529 target_id,
1530 max_depth,
1531 ref_types,
1532 visited,
1533 results,
1534 current_depth + 1,
1535 );
1536 }
1537 }
1538 }
1539
1540 pub(crate) fn index_cross_refs(
1542 &self,
1543 entity: &UnifiedEntity,
1544 collection: &str,
1545 ) -> Result<(), StoreError> {
1546 self.resident.index_cross_refs(entity, collection)
1547 }
1548
1549 pub(crate) fn unindex_cross_refs(&self, id: EntityId) -> Result<(), StoreError> {
1551 self.resident.unindex_cross_refs(id)
1552 }
1553
1554 pub(crate) fn unindex_cross_refs_batch(&self, ids: &[EntityId]) -> Result<(), StoreError> {
1555 self.resident.unindex_cross_refs_batch(ids)
1556 }
1557
1558 pub fn unindex_cross_refs_fast_path_hits(&self) -> u64 {
1562 self.resident.unindex_cross_refs_fast_path_hits()
1563 }
1564
1565 pub fn query_all<F>(&self, filter: F) -> Vec<(String, UnifiedEntity)>
1567 where
1568 F: Fn(&UnifiedEntity) -> bool + Clone + Send + Sync,
1569 {
1570 self.resident.query_all(filter)
1571 }
1572
1573 pub fn scan<F>(
1575 &self,
1576 snapshot: Option<&crate::runtime::impl_core::SnapshotContext>,
1577 filter: F,
1578 ) -> Vec<(String, UnifiedEntity)>
1579 where
1580 F: Fn(&UnifiedEntity) -> bool + Clone + Send + Sync,
1581 {
1582 self.resident.scan(snapshot, filter)
1583 }
1584
1585 pub fn filter_metadata_all(
1587 &self,
1588 filters: &[(String, MetadataFilter)],
1589 ) -> Vec<(String, EntityId)> {
1590 self.resident.filter_metadata_all(filters)
1591 }
1592
1593 pub fn stats(&self) -> StoreStats {
1595 let pairs: Vec<(String, Arc<SegmentManager>)> = {
1601 let collections = self.resident.collections.read();
1602 collections
1603 .iter()
1604 .map(|(name, mgr)| (name.clone(), Arc::clone(mgr)))
1605 .collect()
1606 };
1607
1608 let mut stats = StoreStats {
1609 collection_count: pairs.len(),
1610 ..Default::default()
1611 };
1612
1613 for (name, manager) in &pairs {
1614 let manager_stats = manager.stats();
1615 stats.total_entities += manager_stats.total_entities;
1616 stats.total_memory_bytes += manager_stats.total_memory_bytes;
1617 stats.total_memory_bytes += manager.checkpoint_tracking_bytes();
1618 stats.collections.insert(name.clone(), manager_stats);
1619 }
1620
1621 stats.total_memory_bytes += self.checkpoint_durable_rows.read().memory_bytes();
1622 stats.total_memory_bytes += self
1623 .checkpoint_versions
1624 .read()
1625 .values()
1626 .filter_map(|version| version.deferred_rows.as_ref())
1627 .map(|rows| rows.capacity() * std::mem::size_of::<EntityId>())
1628 .sum::<usize>();
1629 stats
1630 }
1631
1632 pub fn run_maintenance(&self) -> Result<(), StoreError> {
1634 let collections = self.resident.collections.read();
1635 for manager in collections.values() {
1636 manager.run_maintenance()?;
1637 }
1638 Ok(())
1639 }
1640
1641 pub fn reclaimable_segment_bytes(&self) -> u64 {
1643 let collections = self.resident.collections.read();
1644 collections
1645 .values()
1646 .map(|manager| manager.reclaimable_bytes())
1647 .fold(0, u64::saturating_add)
1648 }
1649
1650 pub fn pressure_consolidation_tick(&self) -> bool {
1653 let managers: Vec<_> = self.resident.collections.read().values().cloned().collect();
1654 let mut advanced = false;
1657 for manager in &managers {
1658 advanced |= manager.pressure_consolidation_tick();
1659 }
1660 advanced
1661 }
1662}
1663
1664fn flatten_config_json(
1666 prefix: &str,
1667 value: &crate::serde_json::Value,
1668 out: &mut Vec<(String, reddb_types::Value)>,
1669) {
1670 use reddb_types::Value;
1671 match value {
1672 crate::serde_json::Value::Object(map) => {
1673 for (k, v) in map {
1674 let key = if prefix.is_empty() {
1675 k.clone()
1676 } else {
1677 format!("{prefix}.{k}")
1678 };
1679 flatten_config_json(&key, v, out);
1680 }
1681 }
1682 crate::serde_json::Value::String(s) => {
1683 out.push((prefix.to_string(), Value::text(s.clone())));
1684 }
1685 crate::serde_json::Value::Integer(n) => {
1686 out.push((prefix.to_string(), Value::Integer(*n)));
1687 }
1688 crate::serde_json::Value::Number(n) => {
1689 if n.fract().abs() < f64::EPSILON {
1690 out.push((prefix.to_string(), Value::UnsignedInteger(*n as u64)));
1691 } else {
1692 out.push((prefix.to_string(), Value::Float(*n)));
1693 }
1694 }
1695 crate::serde_json::Value::Decimal(n) => {
1696 out.push((prefix.to_string(), Value::DecimalText(n.clone())));
1697 }
1698 crate::serde_json::Value::Bool(b) => {
1699 out.push((prefix.to_string(), Value::Boolean(*b)));
1700 }
1701 crate::serde_json::Value::Null => {
1702 out.push((prefix.to_string(), Value::Null));
1703 }
1704 crate::serde_json::Value::Array(arr) => {
1705 let json_str = crate::serde_json::to_string(value).unwrap_or_default();
1706 out.push((prefix.to_string(), Value::text(json_str)));
1707 }
1708 }
1709}