1#![allow(clippy::too_many_arguments)] mod graph_index_merge;
18
19use parking_lot::RwLock;
20use std::collections::HashMap;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::sync::{Arc, Weak};
23
24use super::entity::{EntityId, EntityKind, UnifiedEntity};
25use super::metadata::{Metadata, MetadataFilter};
26use super::segment::{
27 GraphEntityKind, GrowingSegment, SegmentConfig, SegmentError, SegmentId, SegmentScanCursor,
28 SegmentState, UnifiedSegment, ZoneColPred,
29};
30use super::visibility_map::VisibilityMap;
31use crate::storage::visibility::entity_visible_with_context;
32use crate::transaction::read::SnapshotContext;
33
34pub const CONSOLIDATION_TOMBSTONE_RATIO: f64 = 0.20;
41
42pub const CONSOLIDATION_FRAGMENTATION_RATIO: f64 = 0.30;
47
48pub const CONSOLIDATION_ENTITIES_PER_TICK: usize = 4_096;
54
55#[derive(Debug, Clone)]
57pub struct ManagerConfig {
58 pub segment_config: SegmentConfig,
60 pub max_sealed_segments: usize,
62 pub idle_seal_secs: u64,
64 pub enable_consolidation: bool,
66 pub consolidation_entities_per_tick: usize,
68 pub enable_archival: bool,
70 pub archive_age_secs: u64,
72}
73
74impl Default for ManagerConfig {
75 fn default() -> Self {
76 Self {
77 segment_config: SegmentConfig::default(),
78 max_sealed_segments: 10,
79 idle_seal_secs: 300, enable_consolidation: true,
81 consolidation_entities_per_tick: CONSOLIDATION_ENTITIES_PER_TICK,
82 enable_archival: true,
83 archive_age_secs: 86400 * 7, }
85 }
86}
87
88#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
93pub struct ConsolidationStats {
94 pub runs_started: u64,
96 pub runs_completed: u64,
98 pub segments_merged: u64,
100 pub tombstones_reclaimed: u64,
102 pub bytes_reclaimed: u64,
104}
105
106#[derive(Debug, Clone, Default)]
108pub struct ManagerStats {
109 pub total_entities: usize,
111 pub growing_count: usize,
113 pub sealed_count: usize,
115 pub archived_count: usize,
117 pub total_memory_bytes: usize,
119 pub seal_ops: u64,
121 pub consolidation: ConsolidationStats,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub struct SegmentScanStats {
127 pub segments_total: u64,
128 pub segments_scanned: u64,
129 pub segments_pruned: u64,
130}
131
132#[derive(Debug, Clone)]
134pub enum LifecycleEvent {
135 SegmentCreated(SegmentId),
136 SegmentSealed(SegmentId),
137 SegmentConsolidated {
138 source: Vec<SegmentId>,
139 target: SegmentId,
140 },
141 SegmentArchived(SegmentId),
142 EntityInserted(EntityId, SegmentId),
143 EntityDeleted(EntityId, SegmentId),
144}
145
146struct Consolidation {
153 sources: Vec<SourceProgress>,
155 merged_id: SegmentId,
157 merged: GrowingSegment,
159 cursor: usize,
161 pending_ids: Vec<EntityId>,
163 pending_cursor: usize,
165 entities_copied: u64,
167}
168
169struct SourceProgress {
171 id: SegmentId,
172 copied: Vec<EntityId>,
174 tombstones_at_open: Option<usize>,
183}
184
185struct PhysicalRootCacheEntry {
186 segments: Vec<(u64, u64)>,
187 seed: u64,
188 root: u64,
189}
190
191#[doc(hidden)]
194pub struct PhysicalScanCursor {
195 sources: Vec<(Weak<RwLock<GrowingSegment>>, SegmentScanCursor)>,
196 source: usize,
197}
198
199impl PhysicalScanCursor {
200 #[doc(hidden)]
201 pub fn finished(&self) -> bool {
202 self.source == self.sources.len()
203 }
204}
205
206pub struct SegmentManager {
208 row_constraint_lock: Arc<parking_lot::ReentrantMutex<()>>,
211 collection: String,
213 config: ManagerConfig,
215 next_segment_id: AtomicU64,
217 next_entity_id: AtomicU64,
219 next_row_id: AtomicU64,
221 total_entities_atomic: AtomicU64,
225 growing: RwLock<Option<Arc<RwLock<GrowingSegment>>>>,
227 sealed: RwLock<Vec<Arc<RwLock<GrowingSegment>>>>,
229 retired: RwLock<Vec<Weak<RwLock<GrowingSegment>>>>,
231 archived: RwLock<Vec<SegmentId>>,
233 entity_segment: RwLock<HashMap<EntityId, SegmentId>>,
237 column_schema: RwLock<Option<Arc<Vec<String>>>>,
240 recovery_row_schemas: RwLock<Vec<Arc<Vec<String>>>>,
243 stats: RwLock<ManagerStats>,
245 consolidation: RwLock<Option<Consolidation>>,
248 events: RwLock<Vec<LifecycleEvent>>,
250 visibility_map: VisibilityMap,
254 physical_root_cache: parking_lot::Mutex<Option<PhysicalRootCacheEntry>>,
255}
256
257impl SegmentManager {
258 pub fn new(collection: impl Into<String>) -> Self {
260 Self::with_config(collection, ManagerConfig::default())
261 }
262
263 pub fn with_config(collection: impl Into<String>, config: ManagerConfig) -> Self {
265 Self {
266 row_constraint_lock: Arc::new(parking_lot::ReentrantMutex::new(())),
267 physical_root_cache: parking_lot::Mutex::new(None),
268 collection: collection.into(),
269 config,
270 next_segment_id: AtomicU64::new(1),
271 next_entity_id: AtomicU64::new(1),
272 next_row_id: AtomicU64::new(1),
273 total_entities_atomic: AtomicU64::new(0),
274 growing: RwLock::new(None),
275 sealed: RwLock::new(Vec::new()),
276 retired: RwLock::new(Vec::new()),
277 archived: RwLock::new(Vec::new()),
278 entity_segment: RwLock::new(HashMap::new()),
279 column_schema: RwLock::new(None),
280 recovery_row_schemas: RwLock::new(Vec::new()),
281 stats: RwLock::new(ManagerStats::default()),
282 consolidation: RwLock::new(None),
283 events: RwLock::new(Vec::new()),
284 visibility_map: VisibilityMap::new(),
285 }
286 }
287
288 #[doc(hidden)]
289 pub fn row_constraint_lock(&self) -> Arc<parking_lot::ReentrantMutex<()>> {
290 Arc::clone(&self.row_constraint_lock)
291 }
292
293 pub fn get_or_init_schema(
295 &self,
296 named: &HashMap<String, reddb_types::Value>,
297 ) -> Arc<Vec<String>> {
298 {
299 let schema = self.column_schema.read();
300 if let Some(ref s) = *schema {
301 return Arc::clone(s);
302 }
303 }
304 let cols: Vec<String> = named.keys().cloned().collect();
305 let arc = Arc::new(cols);
306 *self.column_schema.write() = Some(Arc::clone(&arc));
307 arc
308 }
309
310 pub fn column_schema(&self) -> Option<Arc<Vec<String>>> {
312 self.column_schema.read().clone()
313 }
314
315 fn restore_owned_row_schemas<'a>(&self, entities: impl Iterator<Item = &'a mut UnifiedEntity>) {
319 let schema = self.column_schema();
320 for entity in entities {
321 if let super::entity::EntityData::Row(row) = &mut entity.data {
322 if row.named.is_none() && row.schema.is_none() {
323 row.schema = schema.clone();
324 }
325 }
326 }
327 }
328
329 #[doc(hidden)]
330 pub fn set_column_schema_if_empty(&self, columns: Vec<String>) {
331 if columns.is_empty()
332 || columns
333 .iter()
334 .collect::<std::collections::HashSet<_>>()
335 .len()
336 != columns.len()
337 {
338 return;
339 }
340 let mut schema = self.column_schema.write();
341 if schema.is_none() {
342 *schema = Some(Arc::new(columns));
343 }
344 }
345
346 #[doc(hidden)]
350 pub fn compact_recovered_rows(&self) -> usize {
351 if self.recovery_preserves_named_rows() {
352 return 0;
353 }
354 let Some(schema) = self.column_schema() else {
355 return 0;
356 };
357 if schema
358 .iter()
359 .collect::<std::collections::HashSet<_>>()
360 .len()
361 != schema.len()
362 {
363 return 0;
364 }
365 let mut growing = self.growing.write();
366 let mut sealed = self.sealed.write();
367 let mut compacted = 0;
368 for segment in growing.iter_mut().chain(sealed.iter_mut()) {
369 if let Some(segment) = Arc::get_mut(segment) {
370 compacted += segment.get_mut().compact_recovered_rows(&schema);
371 }
372 }
373 compacted
374 }
375
376 pub fn collection(&self) -> &str {
378 &self.collection
379 }
380
381 pub fn config(&self) -> &ManagerConfig {
383 &self.config
384 }
385
386 pub fn stats(&self) -> ManagerStats {
389 let mut s = self.stats.read().clone();
390 s.total_entities = self.total_entities_atomic.load(Ordering::Relaxed) as usize;
391 s.total_memory_bytes = self.resident_bytes() as usize;
392 s
393 }
394
395 pub fn resident_bytes(&self) -> u64 {
398 self.memory_usage().1
399 }
400
401 pub fn memory_bytes(&self) -> u64 {
404 self.memory_usage().0
405 }
406
407 fn memory_usage(&self) -> (u64, u64) {
411 let consolidation = self.consolidation.read();
412 let growing = self.growing.read();
413 let sealed = self.sealed.read();
414 let mut retired = self.retired.write();
415 let mut payload = 0u64;
416 let mut resident = 0u64;
417 {
418 let mut add = |segment: &GrowingSegment| {
419 payload = payload.saturating_add(segment.memory_bytes());
420 resident = resident.saturating_add(segment.resident_bytes());
421 };
422 for segment in growing.iter().chain(sealed.iter()) {
423 add(&segment.read());
424 }
425 retired.retain(|source| {
426 if let Some(source) = source.upgrade() {
427 add(&source.read());
428 true
429 } else {
430 false
431 }
432 });
433 if let Some(run) = consolidation.as_ref() {
434 add(&run.merged);
435 }
436 }
437 let mut bookkeeping =
438 (retired.capacity() * std::mem::size_of::<Weak<RwLock<GrowingSegment>>>()) as u64;
439 if let Some(run) = consolidation.as_ref() {
440 bookkeeping = bookkeeping
441 .saturating_add(
442 (run.sources.capacity() * std::mem::size_of::<SourceProgress>()) as u64,
443 )
444 .saturating_add(
445 (run.pending_ids.capacity() * std::mem::size_of::<EntityId>()) as u64,
446 );
447 for source in &run.sources {
448 bookkeeping = bookkeeping.saturating_add(
449 (source.copied.capacity() * std::mem::size_of::<EntityId>()) as u64,
450 );
451 }
452 }
453 (
454 payload.saturating_add(bookkeeping),
455 resident.saturating_add(bookkeeping),
456 )
457 }
458
459 pub fn reclaimable_bytes(&self) -> u64 {
461 self.sealed
462 .read()
463 .iter()
464 .map(|segment| segment.read().reclaimable_bytes())
465 .fold(0, u64::saturating_add)
466 }
467
468 pub fn next_entity_id(&self) -> EntityId {
470 EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
471 }
472
473 pub fn next_row_id(&self) -> u64 {
475 self.next_row_id.fetch_add(1, Ordering::SeqCst)
476 }
477
478 pub fn reserve_row_ids(&self, n: u64) -> std::ops::Range<u64> {
482 let start = self.next_row_id.fetch_add(n, Ordering::SeqCst);
483 start..start + n
484 }
485
486 pub fn register_row_id(&self, id: u64) {
489 let candidate = id.saturating_add(1);
490 let mut current = self.next_row_id.load(Ordering::SeqCst);
491 while candidate > current {
492 match self.next_row_id.compare_exchange(
493 current,
494 candidate,
495 Ordering::SeqCst,
496 Ordering::SeqCst,
497 ) {
498 Ok(_) => break,
499 Err(updated) => current = updated,
500 }
501 }
502 }
503
504 fn get_or_create_growing(&self) -> Arc<RwLock<GrowingSegment>> {
511 {
513 let growing = self.growing.read();
514 if let Some(segment) = growing.as_ref() {
515 return Arc::clone(segment);
516 }
517 }
518
519 let mut growing = self.growing.write();
521 if let Some(segment) = growing.as_ref() {
523 return Arc::clone(segment);
524 }
525
526 let id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
527 let segment = GrowingSegment::new(id, &self.collection);
528 let segment_arc = Arc::new(RwLock::new(segment));
529 *growing = Some(Arc::clone(&segment_arc));
530
531 self.emit(LifecycleEvent::SegmentCreated(id));
532
533 self.stats.write().growing_count += 1;
536
537 segment_arc
538 }
539
540 pub fn insert(&self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
542 self.maybe_seal_growing()?;
544
545 let segment_arc = self.get_or_create_growing();
546 let mut segment = segment_arc.write();
547
548 if entity.id.raw() == 0 {
550 entity.id = self.next_entity_id();
551 }
552
553 let entity_id = entity.id;
554 let segment_id = segment.id();
555
556 segment.insert(entity)?;
557
558 self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
560
561 self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
570
571 Ok(entity_id)
572 }
573
574 fn with_restoration_segment<T>(
578 &self,
579 id: EntityId,
580 restore: impl FnOnce(&mut GrowingSegment) -> Result<T, SegmentError>,
581 ) -> Result<T, SegmentError> {
582 loop {
583 let segment = self.get_or_create_growing();
584 let mut growing = segment.write();
585 if !growing.state().is_writable() {
586 continue;
587 }
588 if !growing.is_tombstoned(id) {
589 return restore(&mut growing);
590 }
591 drop(growing);
592 self.force_seal()?;
593 }
594 }
595
596 fn recovery_preserves_named_rows(&self) -> bool {
597 self.collection.starts_with("red_") || self.collection.starts_with("red.")
601 }
602
603 fn recovery_schema(&self, row: &super::entity::RowData) -> Option<Arc<Vec<String>>> {
604 if self.recovery_preserves_named_rows() {
605 return None;
606 }
607 let named = row.named.as_ref()?;
608 let matches = |schema: &Vec<String>| {
609 schema.len() == named.len() && schema.iter().all(|name| named.contains_key(name))
610 };
611 if let Some(schema) = self.column_schema().filter(|schema| matches(schema)) {
612 return Some(schema);
613 }
614 {
615 let schemas = self.recovery_row_schemas.read();
616 if let Some(schema) = schemas.iter().find(|schema| matches(schema)) {
617 return Some(Arc::clone(schema));
618 }
619 }
620 let mut schemas = self.recovery_row_schemas.write();
621 if let Some(schema) = schemas.iter().find(|schema| matches(schema)) {
622 return Some(Arc::clone(schema));
623 }
624 const MAX_RECOVERY_ROW_SCHEMAS: usize = 16;
625 if schemas.len() >= MAX_RECOVERY_ROW_SCHEMAS {
626 return None;
627 }
628 let mut names: Vec<_> = named.keys().cloned().collect();
629 names.sort_unstable();
630 let schema = Arc::new(names);
631 schemas.push(Arc::clone(&schema));
632 Some(schema)
633 }
634
635 #[doc(hidden)]
637 pub fn restore_snapshot_entity(
638 &self,
639 mut entity: UnifiedEntity,
640 ) -> Result<EntityId, SegmentError> {
641 if let super::entity::EntityData::Row(row) = &mut entity.data {
642 if let Some(schema) = self.recovery_schema(row) {
643 row.compact_with_schema(&schema);
644 }
645 }
646 self.maybe_seal_growing()?;
647 if self.get(entity.id).is_some() {
648 return Err(SegmentError::AlreadyExists(entity.id));
649 }
650 let entity_id = entity.id;
651 self.with_restoration_segment(entity_id, |segment| {
652 let segment_id = segment.id();
653 segment.adopt_entity(entity, None);
655 self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
656 self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
657 Ok(entity_id)
658 })
659 }
660
661 pub fn insert_batch(
663 &self,
664 entities: Vec<UnifiedEntity>,
665 ) -> Result<Vec<EntityId>, SegmentError> {
666 let mut ids = Vec::with_capacity(entities.len());
667 for entity in entities {
668 ids.push(self.insert(entity)?);
669 }
670 Ok(ids)
671 }
672
673 pub fn bulk_insert(
676 &self,
677 mut entities: Vec<UnifiedEntity>,
678 ) -> Result<Vec<EntityId>, SegmentError> {
679 for entity in &mut entities {
681 if entity.id.raw() == 0 {
682 entity.id = self.next_entity_id();
683 }
684 if let super::entity::EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
685 if *row_id == 0 {
686 *row_id = self.next_row_id();
687 } else {
688 self.register_row_id(*row_id);
689 }
690 }
691 }
692
693 if let Some(first_row) = entities.first() {
696 if let super::entity::EntityData::Row(ref row) = first_row.data {
697 if let Some(ref named) = row.named {
698 let schema = self.get_or_init_schema(named);
699 for entity in &mut entities {
700 if let super::entity::EntityData::Row(ref mut row) = entity.data {
701 if let Some(named) = row.named.take() {
702 let mut cols = Vec::with_capacity(schema.len());
703 for col_name in schema.iter() {
704 cols.push(
705 named
706 .get(col_name)
707 .cloned()
708 .unwrap_or(reddb_types::Value::Null),
709 );
710 }
711 row.columns = cols;
712 row.schema = Some(Arc::clone(&schema));
713 }
714 }
715 }
716 }
717 }
718 }
719
720 let segment_arc = self.get_or_create_growing();
721 let mut segment = segment_arc.write();
722 let ids = segment.bulk_insert(entities)?;
724
725 self.total_entities_atomic
730 .fetch_add(ids.len() as u64, Ordering::Relaxed);
731
732 Ok(ids)
733 }
734
735 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
737 let mut entity = self.get_with(id, Clone::clone)?;
738 self.restore_owned_row_schemas(std::iter::once(&mut entity));
739 Some(entity)
740 }
741
742 #[doc(hidden)]
745 pub fn get_with<T>(&self, id: EntityId, visit: impl FnOnce(&UnifiedEntity) -> T) -> Option<T> {
746 if let Some(growing_arc) = self.growing.read().as_ref() {
748 let growing = growing_arc.read();
749 if let Some(entity) = growing.get(id) {
750 return Some(visit(entity));
751 }
752 }
753
754 let sealed = self.sealed.read();
756 for segment in sealed.iter() {
757 let seg = segment.read();
758 if let Some(entity) = seg.get(id) {
759 return Some(visit(entity));
760 }
761 }
762
763 None
764 }
765
766 #[doc(hidden)]
770 pub fn graph_node_logical_id(
771 &self,
772 id: EntityId,
773 mut before_segment: impl FnMut() -> bool,
774 ) -> Option<EntityId> {
775 if let Some(growing_arc) = self.growing.read().as_ref() {
776 if !before_segment() {
777 return None;
778 }
779 let growing = growing_arc.read();
780 if let Some(entity) = growing.get(id) {
781 return matches!(entity.kind, EntityKind::GraphNode(_))
782 .then(|| entity.logical_id());
783 }
784 }
785 let sealed = self.sealed.read();
786 for segment in sealed.iter() {
787 if !before_segment() {
788 return None;
789 }
790 let segment = segment.read();
791 if let Some(entity) = segment.get(id) {
792 return matches!(entity.kind, EntityKind::GraphNode(_))
793 .then(|| entity.logical_id());
794 }
795 }
796 None
797 }
798
799 #[doc(hidden)]
804 pub fn visit_graph_index_batches<E>(
805 &self,
806 kind: GraphEntityKind,
807 keys: &[EntityId],
808 mut before_work: impl FnMut() -> bool,
809 mut admit: impl FnMut(usize) -> Result<(), E>,
810 mut consume: impl FnMut(&[EntityId]) -> Result<bool, E>,
811 ) -> Result<bool, E> {
812 use super::segment::{GraphIndexCursor, SCAN_BATCH_SIZE};
813 admit(std::mem::size_of::<[EntityId; SCAN_BATCH_SIZE]>())?;
814 let mut admitted = 0;
815 let sources = loop {
816 let count = {
817 let growing = self.growing.read();
818 let sealed = self.sealed.read();
819 sealed.len() + usize::from(growing.is_some())
820 };
821 let bytes = count.saturating_mul(std::mem::size_of::<(
822 Arc<RwLock<GrowingSegment>>,
823 GraphIndexCursor,
824 )>());
825 admit(bytes.saturating_sub(admitted))?;
826 admitted = admitted.max(bytes);
827 let mut sources = Vec::with_capacity(count);
828 let growing = self.growing.read();
829 let sealed = self.sealed.read();
830 if sealed.len() + usize::from(growing.is_some()) > sources.capacity() {
831 continue;
832 }
833 for segment in growing.iter().chain(sealed.iter()) {
834 if !before_work() {
835 return Ok(false);
836 }
837 let view = segment.read();
838 if !view.may_contain_graph_kind(kind) {
839 continue;
840 }
841 let Some(cursor) = view.graph_index_cursor(kind, keys, &mut before_work) else {
842 return Ok(false);
843 };
844 sources.push((Arc::clone(segment), cursor));
845 }
846 break sources;
847 };
848 let mut ids = [EntityId::new(0); SCAN_BATCH_SIZE];
849 for (source, mut cursor) in sources {
850 while !cursor.finished(keys) {
851 let mut count = 0;
852 {
853 let segment = source.read();
854 if !segment.may_contain_graph_kind(kind) {
855 break;
856 }
857 if !segment.graph_index_batch(
858 kind,
859 keys,
860 &mut cursor,
861 &mut before_work,
862 &mut |id| {
863 ids[count] = id;
864 count += 1;
865 },
866 ) {
867 return Ok(false);
868 }
869 }
870 if !before_work() || !consume(&ids[..count])? {
871 return Ok(false);
872 }
873 }
874 }
875 Ok(true)
876 }
877
878 pub fn get_many(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
883 let schema = self.column_schema();
884 let clone_row = |entity: &UnifiedEntity| {
885 let mut owned = entity.clone();
886 if let super::entity::EntityData::Row(row) = &mut owned.data {
887 if row.schema.is_none() {
888 row.schema = schema.clone();
889 }
890 }
891 owned
892 };
893 let mut out: Vec<Option<UnifiedEntity>> = vec![None; ids.len()];
894 let mut remaining: Vec<usize> = (0..ids.len()).collect(); if let Some(growing_arc) = self.growing.read().as_ref() {
899 let growing = if let Some(g) = growing_arc.try_read() {
900 g
901 } else {
902 growing_arc.read()
903 };
904 remaining.retain(|&i| {
905 if let Some(entity) = growing.get(ids[i]) {
906 out[i] = Some(clone_row(entity));
907 false } else {
909 true }
911 });
912 }
913
914 if remaining.is_empty() {
915 self.restore_owned_row_schemas(out.iter_mut().flatten());
916 return out;
917 }
918
919 let sealed = self.sealed.read();
921 for segment in sealed.iter() {
922 if remaining.is_empty() {
923 break;
924 }
925 let seg = segment.read();
926 remaining.retain(|&i| {
927 if let Some(entity) = seg.get(ids[i]) {
928 out[i] = Some(clone_row(entity));
929 false
930 } else {
931 true
932 }
933 });
934 }
935
936 drop(sealed);
937 self.restore_owned_row_schemas(out.iter_mut().flatten());
938 out
939 }
940
941 pub fn for_each_id<F>(&self, ids: &[EntityId], mut f: F)
954 where
955 F: FnMut(usize, &UnifiedEntity),
956 {
957 thread_local! {
966 static REMAINING_SCRATCH: std::cell::Cell<Vec<usize>> =
967 const { std::cell::Cell::new(Vec::new()) };
968 }
969
970 let mut remaining: Vec<usize> = REMAINING_SCRATCH.with(|cell| cell.take());
971 remaining.clear();
972
973 if let Some(growing_arc) = self.growing.read().as_ref() {
974 let growing = if let Some(g) = growing_arc.try_read() {
975 g
976 } else {
977 growing_arc.read()
978 };
979 for (i, id) in ids.iter().enumerate() {
980 if let Some(entity) = growing.get(*id) {
981 f(i, entity);
982 } else {
983 remaining.push(i);
984 }
985 }
986 } else {
987 remaining.reserve(ids.len());
988 remaining.extend(0..ids.len());
989 }
990
991 if !remaining.is_empty() {
992 let sealed = self.sealed.read();
993 for segment in sealed.iter() {
994 if remaining.is_empty() {
995 break;
996 }
997 let seg = segment.read();
998 remaining.retain(|&i| {
999 if let Some(entity) = seg.get(ids[i]) {
1000 f(i, entity);
1001 false
1002 } else {
1003 true
1004 }
1005 });
1006 }
1007 }
1008
1009 REMAINING_SCRATCH.with(|cell| cell.set(remaining));
1010 }
1011
1012 #[allow(dead_code)] fn scan_for_entity(&self, id: EntityId) -> Option<UnifiedEntity> {
1015 if let Some(growing_arc) = self.growing.read().as_ref() {
1017 let growing = growing_arc.read();
1018 if let Some(entity) = growing.get(id) {
1019 return Some(entity.clone());
1020 }
1021 }
1022
1023 let sealed = self.sealed.read();
1025 for segment in sealed.iter() {
1026 if let Some(entity) = segment.get(id) {
1027 return Some(entity.clone());
1028 }
1029 }
1030
1031 None
1032 }
1033
1034 fn find_sealed_segment_arc(&self, id: EntityId) -> Option<Arc<RwLock<GrowingSegment>>> {
1035 let sealed = self.sealed.read();
1036 sealed
1037 .iter()
1038 .find(|segment_arc| segment_arc.read().contains(id))
1039 .map(Arc::clone)
1040 }
1041
1042 fn rewrite_sealed_entity_into_growing(
1043 &self,
1044 entity: UnifiedEntity,
1045 metadata: Option<&Metadata>,
1046 ) -> Result<(), SegmentError> {
1047 let entity_id = entity.id;
1048 let sealed_arc = self
1049 .find_sealed_segment_arc(entity_id)
1050 .ok_or(SegmentError::NotFound(entity_id))?;
1051
1052 let metadata_to_apply = {
1053 let mut sealed = sealed_arc.write();
1054 let existing_metadata = sealed.get_metadata(entity_id);
1055 if !sealed.force_delete(entity_id) {
1056 return Err(SegmentError::NotFound(entity_id));
1057 }
1058 metadata.cloned().or(existing_metadata)
1059 };
1060
1061 let growing_id = self.with_restoration_segment(entity_id, |growing| {
1062 growing.insert_preserving_sequence(entity)?;
1063 if let Some(metadata) = metadata_to_apply {
1064 growing.set_metadata(entity_id, metadata)?;
1065 }
1066 Ok(growing.id())
1067 })?;
1068
1069 self.entity_segment.write().insert(entity_id, growing_id);
1070 Ok(())
1071 }
1072
1073 pub fn update(&self, entity: UnifiedEntity) -> Result<(), SegmentError> {
1075 let entity_id = entity.id;
1076 let mut entity = Some(entity);
1077
1078 if let Some(growing_arc) = self.growing.read().as_ref() {
1080 let mut growing = growing_arc.write();
1081 if growing.contains(entity_id) && growing.state().is_writable() {
1082 return growing.update(entity.take().expect("entity already moved"));
1083 }
1084 }
1085
1086 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1088 if let Some(seg_id) = segment_id {
1089 if let Some(growing_arc) = self.growing.read().as_ref() {
1090 let mut growing = growing_arc.write();
1091 if growing.id() == seg_id && growing.state().is_writable() {
1092 return growing.update(entity.take().expect("entity already moved"));
1093 }
1094 }
1095 }
1096
1097 if let Some(entity) = entity.take() {
1098 return self.rewrite_sealed_entity_into_growing(entity, None);
1099 }
1100
1101 Err(SegmentError::NotFound(entity_id))
1102 }
1103
1104 pub fn update_with_metadata(
1107 &self,
1108 entity: UnifiedEntity,
1109 metadata: Option<&Metadata>,
1110 ) -> Result<(), SegmentError> {
1111 let entity_id = entity.id;
1112 let mut entity = Some(entity);
1113
1114 if let Some(growing_arc) = self.growing.read().as_ref() {
1116 let mut growing = growing_arc.write();
1117 if growing.contains(entity_id) && growing.state().is_writable() {
1118 growing.update(entity.take().expect("entity already moved"))?;
1119 if let Some(metadata) = metadata {
1120 growing.set_metadata(entity_id, metadata.clone())?;
1121 }
1122 return Ok(());
1123 }
1124 }
1125
1126 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1128 if let Some(seg_id) = segment_id {
1129 if let Some(growing_arc) = self.growing.read().as_ref() {
1130 let mut growing = growing_arc.write();
1131 if growing.id() == seg_id && growing.state().is_writable() {
1132 growing.update(entity.take().expect("entity already moved"))?;
1133 if let Some(metadata) = metadata {
1134 growing.set_metadata(entity_id, metadata.clone())?;
1135 }
1136 return Ok(());
1137 }
1138 }
1139 }
1140
1141 if let Some(entity) = entity.take() {
1142 return self.rewrite_sealed_entity_into_growing(entity, metadata);
1143 }
1144
1145 Err(SegmentError::NotFound(entity_id))
1146 }
1147
1148 pub fn update_hot(
1152 &self,
1153 entity: UnifiedEntity,
1154 modified_columns: &[String],
1155 ) -> Result<(), SegmentError> {
1156 let entity_id = entity.id;
1157 let mut entity = Some(entity);
1158
1159 if let Some(growing_arc) = self.growing.read().as_ref() {
1160 let mut growing = growing_arc.write();
1161 if growing.contains(entity_id) && growing.state().is_writable() {
1162 return growing.update_hot(
1163 entity.take().expect("entity already moved"),
1164 modified_columns,
1165 );
1166 }
1167 }
1168
1169 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1170 if let Some(seg_id) = segment_id {
1171 if let Some(growing_arc) = self.growing.read().as_ref() {
1172 let mut growing = growing_arc.write();
1173 if growing.id() == seg_id && growing.state().is_writable() {
1174 return growing.update_hot(
1175 entity.take().expect("entity already moved"),
1176 modified_columns,
1177 );
1178 }
1179 }
1180 }
1181
1182 if let Some(entity) = entity.take() {
1183 return self.rewrite_sealed_entity_into_growing(entity, None);
1184 }
1185
1186 Err(SegmentError::NotFound(entity_id))
1187 }
1188
1189 pub fn update_hot_with_metadata(
1192 &self,
1193 entity: UnifiedEntity,
1194 modified_columns: &[String],
1195 metadata: Option<&Metadata>,
1196 ) -> Result<(), SegmentError> {
1197 let entity_id = entity.id;
1198 let mut entity = Some(entity);
1199
1200 if let Some(growing_arc) = self.growing.read().as_ref() {
1201 let mut growing = growing_arc.write();
1202 if growing.contains(entity_id) && growing.state().is_writable() {
1203 growing.update_hot(
1204 entity.take().expect("entity already moved"),
1205 modified_columns,
1206 )?;
1207 if let Some(metadata) = metadata {
1208 growing.set_metadata(entity_id, metadata.clone())?;
1209 }
1210 return Ok(());
1211 }
1212 }
1213
1214 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1215 if let Some(seg_id) = segment_id {
1216 if let Some(growing_arc) = self.growing.read().as_ref() {
1217 let mut growing = growing_arc.write();
1218 if growing.id() == seg_id && growing.state().is_writable() {
1219 growing.update_hot(
1220 entity.take().expect("entity already moved"),
1221 modified_columns,
1222 )?;
1223 if let Some(metadata) = metadata {
1224 growing.set_metadata(entity_id, metadata.clone())?;
1225 }
1226 return Ok(());
1227 }
1228 }
1229 }
1230
1231 if let Some(entity) = entity.take() {
1232 return self.rewrite_sealed_entity_into_growing(entity, metadata);
1233 }
1234
1235 Err(SegmentError::NotFound(entity_id))
1236 }
1237
1238 pub fn update_hot_batch_with_metadata<'a, I>(&self, items: I) -> Result<(), SegmentError>
1241 where
1242 I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
1243 {
1244 let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
1245 items.into_iter().collect();
1246 if items.is_empty() {
1247 return Ok(());
1248 }
1249
1250 if let Some(growing_arc) = self.growing.read().as_ref() {
1251 let mut growing = growing_arc.write();
1252 if growing.state().is_writable() {
1253 match growing.update_hot_batch_with_metadata(items.iter().copied()) {
1254 Ok(()) => return Ok(()),
1255 Err(SegmentError::NotFound(_)) => {}
1256 Err(other) => return Err(other),
1257 }
1258 }
1259 }
1260
1261 for (entity, modified_columns, metadata) in items {
1262 self.update_hot_with_metadata(entity.clone(), modified_columns, metadata)?;
1263 }
1264 Ok(())
1265 }
1266
1267 pub fn delete(&self, id: EntityId) -> Result<bool, SegmentError> {
1269 if let Some(growing_arc) = self.growing.read().as_ref() {
1272 let mut growing = growing_arc.write();
1273 if growing.contains(id) && growing.state().is_writable() {
1274 let seg_id = growing.id();
1275 let deleted = growing.delete(id)?;
1276 if deleted {
1277 self.entity_segment.write().remove(&id);
1278 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1279 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1280 }
1281 return Ok(deleted);
1282 }
1283 }
1284
1285 let segment_id = self.entity_segment.read().get(&id).copied();
1288
1289 if let Some(seg_id) = segment_id {
1290 if let Some(growing_arc) = self.growing.read().as_ref() {
1291 let mut growing = growing_arc.write();
1292 if growing.id() == seg_id && growing.state().is_writable() {
1293 let deleted = growing.delete(id)?;
1294 if deleted {
1295 self.entity_segment.write().remove(&id);
1296 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1297 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1298 }
1299 return Ok(deleted);
1300 }
1301 }
1302 }
1303
1304 {
1307 let sealed = self.sealed.read();
1308 for segment_arc in sealed.iter() {
1309 let mut seg = segment_arc.write();
1310 let seg_id = seg.id();
1311 if seg.contains(id) {
1312 let deleted = seg.force_delete(id);
1313 drop(seg);
1314 if deleted {
1315 self.entity_segment.write().remove(&id);
1316 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1317 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1318 }
1319 return Ok(deleted);
1320 }
1321 }
1322 }
1323
1324 Ok(false)
1325 }
1326
1327 pub fn delete_batch(&self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
1328 if ids.is_empty() {
1329 return Ok(Vec::new());
1330 }
1331
1332 let mut deleted_ids = Vec::with_capacity(ids.len());
1333
1334 if let Some(growing_arc) = self.growing.read().as_ref() {
1335 let mut growing = growing_arc.write();
1336 if growing.state().is_writable() {
1337 let seg_id = growing.id();
1338 let deleted = growing.delete_batch(ids)?;
1339 if !deleted.is_empty() {
1340 {
1341 let mut entity_segment = self.entity_segment.write();
1342 for id in &deleted {
1343 entity_segment.remove(id);
1344 }
1345 }
1346 self.total_entities_atomic
1347 .fetch_sub(deleted.len() as u64, Ordering::Relaxed);
1348 for id in &deleted {
1349 self.emit(LifecycleEvent::EntityDeleted(*id, seg_id));
1350 }
1351 deleted_ids.extend(deleted);
1352 }
1353 }
1354 }
1355
1356 if deleted_ids.len() == ids.len() {
1357 return Ok(deleted_ids);
1358 }
1359
1360 let deleted_set: std::collections::HashSet<EntityId> =
1361 deleted_ids.iter().copied().collect();
1362 for &id in ids {
1363 if deleted_set.contains(&id) {
1364 continue;
1365 }
1366 if self.delete(id)? {
1367 deleted_ids.push(id);
1368 }
1369 }
1370
1371 Ok(deleted_ids)
1372 }
1373
1374 pub fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1376 if let Some(growing_arc) = self.growing.read().as_ref() {
1378 let growing = growing_arc.read();
1379 if growing.contains(id) {
1380 return growing.get_metadata(id);
1381 }
1382 }
1383
1384 let segment_id = self.entity_segment.read().get(&id).copied();
1386
1387 if let Some(seg_id) = segment_id {
1388 if let Some(growing_arc) = self.growing.read().as_ref() {
1389 let growing = growing_arc.read();
1390 if growing.id() == seg_id {
1391 return growing.get_metadata(id);
1392 }
1393 }
1394
1395 let sealed = self.sealed.read();
1396 for segment in sealed.iter() {
1397 if segment.id() == seg_id {
1398 return segment.get_metadata(id);
1399 }
1400 }
1401 }
1402
1403 if let Some(segment_arc) = self.find_sealed_segment_arc(id) {
1404 return segment_arc.read().get_metadata(id);
1405 }
1406
1407 None
1408 }
1409
1410 pub fn set_metadata(&self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1412 if let Some(growing_arc) = self.growing.read().as_ref() {
1415 let mut growing = growing_arc.write();
1416 if growing.contains(id) && growing.state().is_writable() {
1417 return growing.set_metadata(id, metadata);
1418 }
1419 }
1420
1421 let segment_id = self.entity_segment.read().get(&id).copied();
1423
1424 if let Some(seg_id) = segment_id {
1425 if let Some(growing_arc) = self.growing.read().as_ref() {
1426 let mut growing = growing_arc.write();
1427 if growing.id() == seg_id && growing.state().is_writable() {
1428 return growing.set_metadata(id, metadata);
1429 }
1430 }
1431 }
1432
1433 if let Some(entity) = self.get(id) {
1434 return self.rewrite_sealed_entity_into_growing(entity, Some(&metadata));
1435 }
1436
1437 Err(SegmentError::NotFound(id))
1438 }
1439
1440 fn maybe_seal_growing(&self) -> Result<(), SegmentError> {
1442 let should_seal = {
1443 let growing_opt = self.growing.read();
1444 if let Some(growing_arc) = growing_opt.as_ref() {
1445 let growing = growing_arc.read();
1446 growing.should_seal(&self.config.segment_config)
1447 || growing.idle_secs() >= self.config.idle_seal_secs
1448 } else {
1449 false
1450 }
1451 };
1452
1453 if should_seal {
1454 self.seal_current()?;
1455 }
1456
1457 Ok(())
1458 }
1459
1460 pub fn seal_current(&self) -> Result<SegmentId, SegmentError> {
1462 let mut growing_slot = self.growing.write();
1465 if let Some(growing_arc) = growing_slot.take() {
1466 let mut growing = growing_arc.write();
1467 let seg_id = growing.id();
1468 let entity_count = growing.stats().entity_count as u64;
1469
1470 growing.seal()?;
1472
1473 drop(growing); self.sealed.write().push(growing_arc);
1479 drop(growing_slot);
1480
1481 self.mark_sealed_pages_visible(entity_count);
1483
1484 {
1486 let mut stats = self.stats.write();
1487 stats.growing_count = stats.growing_count.saturating_sub(1);
1488 stats.sealed_count += 1;
1489 stats.seal_ops += 1;
1490 }
1491
1492 self.emit(LifecycleEvent::SegmentSealed(seg_id));
1493
1494 return Ok(seg_id);
1495 }
1496
1497 Err(SegmentError::InvalidState(SegmentState::Sealed))
1498 }
1499
1500 pub fn force_seal(&self) -> Result<Option<SegmentId>, SegmentError> {
1502 let has_growing = self.growing.read().is_some();
1503 if has_growing {
1504 Ok(Some(self.seal_current()?))
1505 } else {
1506 Ok(None)
1507 }
1508 }
1509
1510 pub fn all_visible_fraction(&self) -> f64 {
1520 const ROWS_PER_PAGE: u32 = 256;
1521 let sealed = self.sealed.read();
1522 if sealed.is_empty() {
1523 return 0.0;
1524 }
1525 let mut total_pages: u64 = 0;
1526 for seg_arc in sealed.iter() {
1527 let seg = seg_arc.read();
1528 let entity_count = seg.stats().entity_count as u64;
1529 let pages = entity_count.div_ceil(ROWS_PER_PAGE as u64);
1530 total_pages += pages;
1531 }
1532 if total_pages == 0 {
1533 return 0.0;
1534 }
1535 let visible = self.visibility_map.all_visible_count();
1536 (visible as f64 / total_pages as f64).min(1.0)
1537 }
1538
1539 fn mark_sealed_pages_visible(&self, seg_entity_count: u64) {
1542 const ROWS_PER_PAGE: u32 = 256;
1543 let existing_visible = self.visibility_map.all_visible_count();
1544 let start_page = existing_visible as u32;
1546 let new_pages = seg_entity_count.div_ceil(ROWS_PER_PAGE as u64);
1547 let end_page = start_page + new_pages as u32;
1548 self.visibility_map.mark_range_visible(start_page, end_page);
1549 }
1550
1551 #[doc(hidden)]
1555 pub fn find_unique_key_conflict(
1556 &self,
1557 columns: &[Vec<String>],
1558 key_index: usize,
1559 signatures: &[String],
1560 exclude_id: Option<EntityId>,
1561 reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1562 ) -> Option<EntityId> {
1563 if let Some(growing) = self.growing.read().as_ref() {
1564 if let Some(id) = growing.read().find_unique_key_conflict(
1565 columns,
1566 key_index,
1567 signatures,
1568 exclude_id,
1569 reserves_key,
1570 ) {
1571 return Some(id);
1572 }
1573 }
1574 for segment in self.sealed.read().iter() {
1575 if let Some(id) = segment.read().find_unique_key_conflict(
1576 columns,
1577 key_index,
1578 signatures,
1579 exclude_id,
1580 reserves_key,
1581 ) {
1582 return Some(id);
1583 }
1584 }
1585 None
1586 }
1587
1588 pub fn for_each_entity<F>(&self, callback: F)
1594 where
1595 F: FnMut(&UnifiedEntity) -> bool,
1596 {
1597 self.for_each_entity_in_segments(|_| true, || true, callback);
1598 }
1599
1600 #[doc(hidden)]
1602 pub fn checkpoint_revisions(&self) -> Vec<(u64, u64)> {
1603 let growing = self.growing.read();
1604 let sealed = self.sealed.read();
1605 growing
1606 .iter()
1607 .chain(sealed.iter())
1608 .map(|segment| {
1609 let segment = segment.read();
1610 (segment.id(), segment.checkpoint_revision())
1611 })
1612 .collect()
1613 }
1614
1615 #[doc(hidden)]
1616 pub fn checkpoint_mutations_since(&self, before: &[(u64, u64)]) -> Option<Vec<EntityId>> {
1617 let growing = self.growing.read();
1618 let sealed = self.sealed.read();
1619 let previous: HashMap<_, _> = before.iter().copied().collect();
1624 if previous.len() != before.len() {
1625 return None;
1626 }
1627 let mut retained_sources = 0;
1628 let mut changed = std::collections::BTreeSet::new();
1629 for segment in growing.iter().chain(sealed.iter()) {
1630 let segment = segment.read();
1631 let revision = previous.get(&segment.id()).copied();
1632 retained_sources += usize::from(revision.is_some());
1633 let revision = revision.unwrap_or(0);
1636 changed.extend(segment.checkpoint_mutations_since(revision)?);
1637 if changed.len() > super::segment::CHECKPOINT_MUTATIONS_MAX {
1638 return None;
1639 }
1640 }
1641 (retained_sources == previous.len()).then(|| changed.into_iter().collect())
1644 }
1645
1646 #[doc(hidden)]
1647 pub fn checkpoint_tracking_bytes(&self) -> usize {
1648 let growing = self.growing.read();
1649 let sealed = self.sealed.read();
1650 growing
1651 .iter()
1652 .chain(sealed.iter())
1653 .map(|segment| segment.read().checkpoint_tracking_bytes())
1654 .sum()
1655 }
1656
1657 #[doc(hidden)]
1659 pub fn cached_physical_root(&self, seed: u64, compute: impl FnOnce() -> u64) -> u64 {
1660 let segments = self.checkpoint_revisions();
1661 let mut cache = self.physical_root_cache.lock();
1662 if let Some(previous) = cache.as_ref() {
1663 if previous.seed == seed && previous.segments == segments {
1664 return previous.root;
1665 }
1666 }
1667 let root = compute();
1668 *cache = Some(PhysicalRootCacheEntry {
1669 segments,
1670 seed,
1671 root,
1672 });
1673 root
1674 }
1675
1676 #[doc(hidden)]
1681 pub fn for_each_entity_in_id_order(&self, mut visit: impl FnMut(&UnifiedEntity) -> bool) {
1682 self.for_each_entity_source_in_id_order(|_, entity| visit(entity));
1683 }
1684
1685 #[doc(hidden)]
1687 pub fn page_in_id_order(&self, offset: usize, limit: usize) -> (usize, Vec<UnifiedEntity>) {
1688 let end = offset.saturating_add(limit);
1689 let mut total = 0;
1690 let mut items = Vec::with_capacity(limit.min(self.count()));
1691 self.for_each_entity_in_id_order(|entity| {
1692 if total >= offset && total < end {
1693 items.push(entity.clone());
1694 }
1695 total += 1;
1696 true
1697 });
1698 self.restore_owned_row_schemas(items.iter_mut());
1699 (total, items)
1700 }
1701
1702 #[doc(hidden)]
1703 pub fn for_each_entity_with_metadata_in_id_order(
1704 &self,
1705 mut visit: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1706 ) {
1707 self.for_each_entity_source_in_id_order(|segment, entity| {
1708 visit(entity, segment.get_metadata(entity.id))
1709 });
1710 }
1711
1712 fn for_each_entity_source_in_id_order(
1713 &self,
1714 mut visit: impl FnMut(&GrowingSegment, &UnifiedEntity) -> bool,
1715 ) {
1716 let segments: Vec<_> = {
1717 let growing = self.growing.read();
1718 let sealed = self.sealed.read();
1719 growing.iter().chain(sealed.iter()).cloned().collect()
1720 };
1721 let mut order = Vec::new();
1722 for (source, segment) in segments.iter().enumerate() {
1723 segment.read().for_each_fast(|entity| {
1724 order.push((entity.id, source));
1725 true
1726 });
1727 }
1728 order.sort_by_key(|(id, _)| id.raw());
1729 for (id, source) in order {
1730 let segment = segments[source].read();
1731 if let Some(entity) = segment.get(id) {
1732 if !visit(&segment, entity) {
1733 break;
1734 }
1735 }
1736 }
1737 }
1738
1739 #[doc(hidden)]
1742 pub fn for_each_entity_with_metadata(
1743 &self,
1744 mut callback: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1745 ) {
1746 if let Some(growing_arc) = self.growing.read().as_ref() {
1747 let growing = growing_arc.read();
1748 if !growing.for_each_fast(|entity| callback(entity, growing.get_metadata(entity.id))) {
1749 return;
1750 }
1751 }
1752 let sealed = self.sealed.read();
1753 for segment_arc in sealed.iter() {
1754 let segment = segment_arc.read();
1755 if !segment.for_each_fast(|entity| callback(entity, segment.get_metadata(entity.id))) {
1756 return;
1757 }
1758 }
1759 }
1760
1761 fn for_each_entity_in_segments<F>(
1762 &self,
1763 segment_filter: impl Fn(&GrowingSegment) -> bool,
1764 mut before_segment: impl FnMut() -> bool,
1765 mut callback: F,
1766 ) where
1767 F: FnMut(&UnifiedEntity) -> bool,
1768 {
1769 if let Some(growing_arc) = self.growing.read().as_ref() {
1773 if !before_segment() {
1774 return;
1775 }
1776 let growing = if let Some(g) = growing_arc.try_read() {
1777 g
1778 } else {
1779 growing_arc.read()
1780 };
1781 if segment_filter(&growing) && !growing.for_each_fast(&mut callback) {
1782 return;
1783 }
1784 }
1785
1786 let sealed = self.sealed.read();
1788 for segment_arc in sealed.iter() {
1789 if !before_segment() {
1790 return;
1791 }
1792 let segment = segment_arc.read();
1793 if segment_filter(&segment) && !segment.for_each_fast(&mut callback) {
1794 return;
1795 }
1796 }
1797 }
1798
1799 pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1815 &self,
1816 init: FInit,
1817 fold: FFold,
1818 reduce: FReduce,
1819 ) -> T
1820 where
1821 T: Send,
1822 FInit: Fn() -> T + Send + Sync,
1823 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1824 FReduce: Fn(T, T) -> T + Send + Sync,
1825 {
1826 #[cfg(not(target_arch = "wasm32"))]
1827 use rayon::prelude::*;
1828
1829 let mut acc = init();
1832 if let Some(growing_arc) = self.growing.read().as_ref() {
1833 let growing = if let Some(g) = growing_arc.try_read() {
1834 g
1835 } else {
1836 growing_arc.read()
1837 };
1838 growing.for_each_fast(|entity| {
1839 acc = fold(std::mem::replace(&mut acc, init()), entity);
1840 true
1841 });
1842 }
1843
1844 let segments: Vec<_> = {
1848 let sealed = self.sealed.read();
1849 sealed.iter().cloned().collect()
1850 };
1851
1852 if segments.len() <= 1 {
1853 for seg_arc in &segments {
1854 let seg = seg_arc.read();
1855 seg.for_each_fast(|entity| {
1856 acc = fold(std::mem::replace(&mut acc, init()), entity);
1857 true
1858 });
1859 }
1860 return acc;
1861 }
1862
1863 #[cfg(not(target_arch = "wasm32"))]
1864 let segments = segments.into_par_iter();
1865 #[cfg(target_arch = "wasm32")]
1866 let segments = segments.into_iter();
1867 let partials = segments.map(|seg_arc| {
1868 let mut local = init();
1869 let seg = seg_arc.read();
1870 seg.for_each_fast(|entity| {
1871 local = fold(std::mem::replace(&mut local, init()), entity);
1872 true
1873 });
1874 local
1875 });
1876 #[cfg(not(target_arch = "wasm32"))]
1877 let sealed_acc = partials.reduce(&init, &reduce);
1878 #[cfg(target_arch = "wasm32")]
1879 let sealed_acc = partials.fold(init(), &reduce);
1880
1881 reduce(acc, sealed_acc)
1882 }
1883
1884 pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1893 where
1894 F: FnMut(&UnifiedEntity) -> bool,
1895 {
1896 let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1897 }
1898
1899 pub fn for_each_entity_zoned_with_stats<F>(
1900 &self,
1901 zone_preds: &[(&str, ZoneColPred<'_>)],
1902 mut callback: F,
1903 ) -> SegmentScanStats
1904 where
1905 F: FnMut(&UnifiedEntity) -> bool,
1906 {
1907 let mut scan_stats = SegmentScanStats::default();
1908 if let Some(growing_arc) = self.growing.read().as_ref() {
1914 scan_stats.segments_total += 1;
1915 scan_stats.segments_scanned += 1;
1916 let growing = if let Some(g) = growing_arc.try_read() {
1917 g
1918 } else {
1919 growing_arc.read()
1920 };
1921 if !growing.for_each_fast(&mut callback) {
1922 return scan_stats;
1923 }
1924 }
1925
1926 let sealed = self.sealed.read();
1928 for segment_arc in sealed.iter() {
1929 scan_stats.segments_total += 1;
1930 let segment = segment_arc.read();
1931 if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1932 scan_stats.segments_pruned += 1;
1933 continue; }
1935 scan_stats.segments_scanned += 1;
1936 if !segment.for_each_fast(&mut callback) {
1937 return scan_stats;
1938 }
1939 }
1940 scan_stats
1941 }
1942
1943 pub fn query_all_zoned<F>(
1953 &self,
1954 zone_preds: &[(&str, ZoneColPred<'_>)],
1955 filter: F,
1956 ) -> Vec<UnifiedEntity>
1957 where
1958 F: Fn(&UnifiedEntity) -> bool + Sync,
1959 {
1960 self.query_all_zoned_with_stats(zone_preds, filter).0
1961 }
1962
1963 pub fn query_all_zoned_with_stats<F>(
1964 &self,
1965 zone_preds: &[(&str, ZoneColPred<'_>)],
1966 filter: F,
1967 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1968 where
1969 F: Fn(&UnifiedEntity) -> bool + Sync,
1970 {
1971 let mut results = Vec::new();
1972 let mut scan_stats = SegmentScanStats::default();
1973
1974 if let Some(growing_arc) = self.growing.read().as_ref() {
1977 scan_stats.segments_total += 1;
1978 scan_stats.segments_scanned += 1;
1979 let growing = if let Some(g) = growing_arc.try_read() {
1980 g
1981 } else {
1982 growing_arc.read()
1983 };
1984 results.extend(growing.iter().filter(|e| filter(e)).cloned());
1985 }
1986
1987 let sealed = self.sealed.read();
1989 let surviving: Vec<_> = sealed
1991 .iter()
1992 .filter(|seg_arc| {
1993 scan_stats.segments_total += 1;
1994 if zone_preds.is_empty() {
1995 scan_stats.segments_scanned += 1;
1996 return true;
1997 }
1998 let seg = seg_arc.read();
1999 if seg.can_skip_zone_preds(zone_preds) {
2000 scan_stats.segments_pruned += 1;
2001 false
2002 } else {
2003 scan_stats.segments_scanned += 1;
2004 true
2005 }
2006 })
2007 .collect();
2008
2009 let use_parallel = surviving.len() > 1 && crate::execution::should_parallelize();
2010
2011 if use_parallel {
2012 let filter_ref = &filter;
2013 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
2014 surviving
2015 .iter()
2016 .map(|segment| {
2017 s.spawn(move || {
2018 segment
2019 .read()
2020 .iter()
2021 .filter(|e| filter_ref(e))
2022 .cloned()
2023 .collect::<Vec<_>>()
2024 })
2025 })
2026 .collect::<Vec<_>>()
2027 .into_iter()
2028 .map(|handle| handle.join().unwrap_or_default())
2029 .collect()
2030 });
2031 for batch in segment_results {
2032 results.extend(batch);
2033 }
2034 } else {
2035 for segment_arc in surviving {
2036 let seg = segment_arc.read();
2037 results.extend(seg.iter().filter(|e| filter(e)).cloned());
2038 }
2039 }
2040
2041 (results, scan_stats)
2042 }
2043
2044 fn scan_entity_visible(snapshot: Option<&SnapshotContext>, entity: &UnifiedEntity) -> bool {
2050 match snapshot {
2051 Some(ctx) => entity_visible_with_context(Some(ctx), entity),
2052 None => {
2053 !crate::storage::visibility::entity_moderation_hidden(entity) && entity.xmax == 0
2054 }
2055 }
2056 }
2057
2058 pub fn scan<F>(&self, snapshot: Option<&SnapshotContext>, filter: F) -> Vec<UnifiedEntity>
2065 where
2066 F: Fn(&UnifiedEntity) -> bool + Sync,
2067 {
2068 self.query_all(|entity| Self::scan_entity_visible(snapshot, entity) && filter(entity))
2069 }
2070
2071 #[doc(hidden)]
2076 pub fn project_key_entity<T>(
2077 &self,
2078 key: &str,
2079 snapshot: Option<&SnapshotContext>,
2080 versioned: bool,
2081 mut project: impl FnMut(&UnifiedEntity) -> T,
2082 ) -> Option<T> {
2083 let mut probe = |segment: &GrowingSegment, best: &mut Option<(u64, T)>| {
2084 if let Some(entity) = segment.find_key_entity(key, versioned, &mut |entity| {
2085 !versioned || Self::scan_entity_visible(snapshot, entity)
2086 }) {
2087 if best.as_ref().is_none_or(|(xmin, _)| entity.xmin >= *xmin) {
2088 *best = Some((entity.xmin, project(entity)));
2089 }
2090 }
2091 };
2092 let mut best = None;
2093 if let Some(growing) = self.growing.read().as_ref() {
2094 probe(&growing.read(), &mut best);
2095 }
2096 for segment in self.sealed.read().iter() {
2097 if !versioned && best.is_some() {
2098 break;
2099 }
2100 probe(&segment.read(), &mut best);
2101 }
2102 best.map(|(_, projected)| projected)
2103 }
2104
2105 #[doc(hidden)]
2107 pub fn scan_graph_kind(
2108 &self,
2109 snapshot: Option<&SnapshotContext>,
2110 kind: GraphEntityKind,
2111 ) -> Vec<UnifiedEntity> {
2112 self.query_all_in_segments(
2113 |segment| segment.may_contain_graph_kind(kind),
2114 |entity| Self::scan_entity_visible(snapshot, entity) && kind.matches(&entity.kind),
2115 )
2116 }
2117
2118 #[doc(hidden)]
2123 pub fn scan_graph_candidates_for_each<F>(
2124 &self,
2125 snapshot: Option<&SnapshotContext>,
2126 kind: GraphEntityKind,
2127 before_segment: impl FnMut() -> bool,
2128 mut callback: F,
2129 ) where
2130 F: FnMut(&UnifiedEntity) -> bool,
2131 {
2132 self.for_each_entity_in_segments(
2133 |segment| segment.may_contain_graph_kind(kind),
2134 before_segment,
2135 |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
2136 );
2137 }
2138
2139 pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
2144 where
2145 F: FnMut(&UnifiedEntity) -> bool,
2146 {
2147 self.for_each_entity(|entity| {
2148 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2149 });
2150 }
2151
2152 #[doc(hidden)]
2153 pub fn physical_scan_cursor(&self) -> PhysicalScanCursor {
2154 let growing = self.growing.read();
2155 let sealed = self.sealed.read();
2156 PhysicalScanCursor {
2157 sources: growing
2158 .iter()
2159 .chain(sealed.iter())
2160 .map(|segment| (Arc::downgrade(segment), segment.read().scan_cursor()))
2161 .collect(),
2162 source: 0,
2163 }
2164 }
2165
2166 #[doc(hidden)]
2169 pub fn physical_scan_batch(
2170 &self,
2171 cursor: &mut PhysicalScanCursor,
2172 work_left: &mut usize,
2173 ) -> Vec<EntityId> {
2174 let mut ids = Vec::with_capacity(super::segment::SCAN_BATCH_SIZE.min(*work_left));
2175 while !cursor.finished() && *work_left > 0 {
2176 let (source, position) = &mut cursor.sources[cursor.source];
2177 if position.finished() {
2178 *work_left -= 1;
2179 cursor.source += 1;
2180 continue;
2181 }
2182 let Some(segment) = source.upgrade() else {
2183 *work_left -= 1;
2184 cursor.source += 1;
2185 continue;
2186 };
2187 segment.read().scan_batch(
2188 position,
2189 &mut || {
2190 if *work_left == 0 {
2191 return false;
2192 }
2193 *work_left -= 1;
2194 true
2195 },
2196 &mut |id| ids.push(id),
2197 );
2198 if position.finished() {
2199 cursor.source += 1;
2200 }
2201 if !ids.is_empty() {
2202 break;
2203 }
2204 }
2205 ids
2206 }
2207
2208 #[doc(hidden)]
2214 pub fn scan_batches(
2215 &self,
2216 snapshot: Option<&SnapshotContext>,
2217 mut before_work: impl FnMut() -> bool,
2218 mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
2219 ) -> bool {
2220 let segments = {
2221 let growing = self.growing.read();
2222 let sealed = self.sealed.read();
2223 let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
2224 for segment in growing.iter().chain(sealed.iter()) {
2225 if !before_work() {
2226 return false;
2227 }
2228 let cursor = segment.read().scan_cursor();
2229 segments.push((Arc::clone(segment), cursor));
2230 }
2231 segments
2232 };
2233 let mut ids = Vec::new();
2234 let mut batch = Vec::new();
2235 for (segment, mut cursor) in segments {
2236 while !cursor.finished() {
2237 ids.clear();
2238 batch.clear();
2239 let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
2240 ids.reserve_exact(batch_size);
2241 batch.reserve_exact(batch_size);
2242 {
2243 let segment = segment.read();
2244 if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
2245 return false;
2246 }
2247 }
2248 if !before_work() {
2251 return false;
2252 }
2253 self.for_each_id(&ids, |_, entity| {
2254 if Self::scan_entity_visible(snapshot, entity) {
2255 batch.push(entity.clone());
2256 }
2257 });
2258 if !before_work() {
2259 return false;
2260 }
2261 if !consume(&batch) {
2262 return false;
2263 }
2264 }
2265 }
2266 true
2267 }
2268
2269 pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
2271 &self,
2272 snapshot: Option<&SnapshotContext>,
2273 init: FInit,
2274 fold: FFold,
2275 reduce: FReduce,
2276 ) -> T
2277 where
2278 T: Send,
2279 FInit: Fn() -> T + Send + Sync,
2280 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
2281 FReduce: Fn(T, T) -> T + Send + Sync,
2282 {
2283 self.fold_entities_parallel(
2284 init,
2285 |acc, entity| {
2286 if Self::scan_entity_visible(snapshot, entity) {
2287 fold(acc, entity)
2288 } else {
2289 acc
2290 }
2291 },
2292 reduce,
2293 )
2294 }
2295
2296 pub fn scan_for_each_zoned_with_stats<F>(
2298 &self,
2299 snapshot: Option<&SnapshotContext>,
2300 zone_preds: &[(&str, ZoneColPred<'_>)],
2301 mut callback: F,
2302 ) -> SegmentScanStats
2303 where
2304 F: FnMut(&UnifiedEntity) -> bool,
2305 {
2306 self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
2307 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2308 })
2309 }
2310
2311 pub fn scan_zoned_with_stats<F>(
2313 &self,
2314 snapshot: Option<&SnapshotContext>,
2315 zone_preds: &[(&str, ZoneColPred<'_>)],
2316 filter: F,
2317 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
2318 where
2319 F: Fn(&UnifiedEntity) -> bool + Sync,
2320 {
2321 self.query_all_zoned_with_stats(zone_preds, |entity| {
2322 Self::scan_entity_visible(snapshot, entity) && filter(entity)
2323 })
2324 }
2325
2326 pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
2329 where
2330 F: Fn(&UnifiedEntity) -> bool + Sync,
2331 {
2332 let mut entities = self.query_all_in_segments(|_| true, filter);
2333 self.restore_owned_row_schemas(entities.iter_mut());
2334 entities
2335 }
2336
2337 fn query_all_in_segments<F>(
2338 &self,
2339 segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
2340 filter: F,
2341 ) -> Vec<UnifiedEntity>
2342 where
2343 F: Fn(&UnifiedEntity) -> bool + Sync,
2344 {
2345 let mut results = Vec::new();
2346
2347 if let Some(growing_arc) = self.growing.read().as_ref() {
2350 let growing = if let Some(g) = growing_arc.try_read() {
2351 g
2352 } else {
2353 growing_arc.read()
2354 };
2355 if segment_filter(&growing) {
2356 results.extend(growing.iter().filter(|e| filter(e)).cloned());
2357 }
2358 }
2359
2360 let sealed = self.sealed.read();
2362 let use_parallel = sealed.len() > 1 && crate::execution::should_parallelize();
2363 if use_parallel {
2364 let filter_ref = &filter;
2365 let segment_filter_ref = &segment_filter;
2366 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
2367 sealed
2368 .iter()
2369 .map(|segment| {
2370 s.spawn(move || {
2371 let segment = segment.read();
2372 if segment_filter_ref(&segment) {
2373 segment
2374 .iter()
2375 .filter(|e| filter_ref(e))
2376 .cloned()
2377 .collect::<Vec<_>>()
2378 } else {
2379 Vec::new()
2380 }
2381 })
2382 })
2383 .collect::<Vec<_>>()
2384 .into_iter()
2385 .map(|handle| handle.join().unwrap_or_default())
2386 .collect()
2387 });
2388 for batch in segment_results {
2389 results.extend(batch);
2390 }
2391 } else {
2392 for segment in sealed.iter() {
2393 let seg = segment.read();
2394 if segment_filter(&seg) {
2395 results.extend(seg.iter().filter(|e| filter(e)).cloned());
2396 }
2397 }
2398 }
2399
2400 results
2401 }
2402
2403 pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
2408 if let Some(growing_arc) = self.growing.read().as_ref() {
2409 let growing = growing_arc.read();
2410 if growing.may_contain_exact_key(key) {
2411 return true;
2412 }
2413 } else {
2414 return true;
2415 }
2416
2417 let sealed = self.sealed.read();
2418 for segment_arc in sealed.iter() {
2419 let segment = segment_arc.read();
2420 if segment.may_contain_exact_key(key) {
2421 return true;
2422 }
2423 }
2424
2425 false
2426 }
2427
2428 pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2430 let mut results = Vec::new();
2431
2432 if let Some(growing_arc) = self.growing.read().as_ref() {
2434 let growing = growing_arc.read();
2435 results.extend(growing.filter_metadata(filters));
2436 }
2437
2438 let sealed = self.sealed.read();
2440 for segment in sealed.iter() {
2441 results.extend(segment.filter_metadata(filters));
2442 }
2443
2444 results
2445 }
2446
2447 pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2449 let mut results = Vec::new();
2450
2451 if let Some(growing_arc) = self.growing.read().as_ref() {
2453 let growing = growing_arc.read();
2454 for entity in growing.iter_kind(kind) {
2455 results.push(entity.clone());
2456 }
2457 }
2458
2459 let sealed = self.sealed.read();
2464 for segment_arc in sealed.iter() {
2465 let segment = segment_arc.read();
2466 for entity in segment.iter_kind(kind) {
2467 results.push(entity.clone());
2468 }
2469 }
2470
2471 results
2472 }
2473
2474 pub fn count(&self) -> usize {
2476 self.total_entities_atomic.load(Ordering::Relaxed) as usize
2477 }
2478
2479 pub fn segment_ids(&self) -> Vec<SegmentId> {
2481 let mut ids = Vec::new();
2482
2483 if let Some(growing_arc) = self.growing.read().as_ref() {
2484 ids.push(growing_arc.read().id());
2485 }
2486
2487 let sealed = self.sealed.read();
2488 for segment in sealed.iter() {
2489 ids.push(segment.id());
2490 }
2491
2492 ids.extend(self.archived.read().iter().copied());
2493
2494 ids
2495 }
2496
2497 #[inline]
2510 #[allow(clippy::unused_self)]
2511 fn emit(&self, _event: LifecycleEvent) {}
2512
2513 pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2516 std::mem::take(&mut *self.events.write())
2517 }
2518
2519 pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2521 self.maybe_seal_growing()?;
2523
2524 if self.config.enable_consolidation {
2525 self.consolidation_tick();
2526 }
2527
2528 Ok(())
2529 }
2530
2531 fn consolidation_tick(&self) {
2542 let running = self.consolidation.read().is_some();
2545 if !running && !self.start_consolidation() {
2546 return;
2547 }
2548
2549 let mut guard = self.consolidation.write();
2552 let Some(run) = guard.as_mut() else {
2553 return;
2554 };
2555 self.copy_bounded(run);
2556 if run.cursor >= run.sources.len() {
2557 if let Some(run) = guard.take() {
2558 self.finish_consolidation(run);
2559 }
2560 }
2561 }
2562
2563 fn start_consolidation(&self) -> bool {
2567 let sources = self.select_candidates();
2568 self.start_consolidation_from_sources(sources)
2569 }
2570
2571 fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2572 if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2573 return false;
2574 }
2575
2576 let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2577 let merged = GrowingSegment::new(merged_id, &self.collection);
2578
2579 *self.consolidation.write() = Some(Consolidation {
2580 sources: sources
2581 .into_iter()
2582 .map(|id| SourceProgress {
2583 id,
2584 copied: Vec::new(),
2585 tombstones_at_open: None,
2586 })
2587 .collect(),
2588 merged_id,
2589 merged,
2590 cursor: 0,
2591 pending_ids: Vec::new(),
2592 pending_cursor: 0,
2593 entities_copied: 0,
2594 });
2595
2596 self.stats.write().consolidation.runs_started += 1;
2597 true
2598 }
2599
2600 pub fn pressure_consolidation_tick(&self) -> bool {
2605 if !self.config.enable_consolidation {
2606 return false;
2607 }
2608
2609 let running = self.consolidation.read().is_some();
2610 if !running {
2611 let sources = self.dirty_sealed_segments();
2612 if !self.start_consolidation_from_sources(sources) {
2613 return false;
2614 }
2615 }
2616
2617 self.consolidation_tick();
2618 true
2619 }
2620
2621 fn select_candidates(&self) -> Vec<SegmentId> {
2636 let sealed = self.sealed.read();
2637 if sealed.is_empty() {
2638 return Vec::new();
2639 }
2640
2641 if sealed.len() > self.config.max_sealed_segments {
2642 return sealed.iter().map(|segment| segment.read().id()).collect();
2643 }
2644
2645 let mut live = 0usize;
2646 let mut tombstones = 0usize;
2647 let mut held = 0u64;
2648 let mut reclaimable = 0u64;
2649 let mut dirty = Vec::new();
2650
2651 for segment_arc in sealed.iter() {
2652 let segment = segment_arc.read();
2653 live += segment.live_entity_count();
2654 tombstones += segment.tombstone_count();
2655 held += segment.resident_bytes();
2656 reclaimable += segment.reclaimable_bytes();
2657 if segment.tombstone_count() > 0 {
2658 dirty.push(segment.id());
2659 }
2660 }
2661
2662 let total = live + tombstones;
2663 let tombstone_ratio = if total == 0 {
2664 0.0
2665 } else {
2666 tombstones as f64 / total as f64
2667 };
2668 let fragmentation = if held == 0 {
2669 0.0
2670 } else {
2671 reclaimable as f64 / held as f64
2672 };
2673
2674 if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2675 || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2676 {
2677 return dirty;
2678 }
2679
2680 Vec::new()
2681 }
2682
2683 fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2684 self.sealed
2685 .read()
2686 .iter()
2687 .filter_map(|segment_arc| {
2688 let segment = segment_arc.read();
2689 (segment.reclaimable_bytes() > 0).then(|| segment.id())
2690 })
2691 .collect()
2692 }
2693
2694 fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2697 if sources.is_empty() {
2698 return false;
2699 }
2700 let sealed = self.sealed.read();
2701 sealed.iter().any(|segment_arc| {
2702 let segment = segment_arc.read();
2703 sources.contains(&segment.id()) && segment.tombstone_count() > 0
2704 })
2705 }
2706
2707 fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2708 let sealed = self.sealed.read();
2709 sealed
2710 .iter()
2711 .find(|segment| segment.read().id() == id)
2712 .map(Arc::clone)
2713 }
2714
2715 fn copy_bounded(&self, run: &mut Consolidation) {
2720 let mut budget = self.config.consolidation_entities_per_tick;
2721
2722 while budget > 0 && run.cursor < run.sources.len() {
2723 let source_id = run.sources[run.cursor].id;
2724 let Some(source_arc) = self.sealed_segment(source_id) else {
2725 run.cursor += 1;
2729 run.pending_ids.clear();
2730 run.pending_cursor = 0;
2731 continue;
2732 };
2733
2734 if run.sources[run.cursor].tombstones_at_open.is_none() {
2735 let source = source_arc.read();
2736 run.pending_ids = source.live_entity_ids();
2737 run.pending_cursor = 0;
2738 run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2739 }
2740
2741 {
2742 let source = source_arc.read();
2743 while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2744 let id = run.pending_ids[run.pending_cursor];
2745 run.pending_cursor += 1;
2746 budget -= 1;
2747
2748 let Some(entity) = source.get(id) else {
2751 continue;
2752 };
2753 let metadata = source.get_metadata(id);
2754 run.merged.adopt_entity(entity.clone(), metadata);
2755 run.sources[run.cursor].copied.push(id);
2756 run.entities_copied += 1;
2757 }
2758 }
2759
2760 if run.pending_cursor >= run.pending_ids.len() {
2761 run.cursor += 1;
2762 run.pending_ids.clear();
2763 run.pending_cursor = 0;
2764 }
2765 }
2766 }
2767
2768 fn finish_consolidation(&self, mut run: Consolidation) {
2774 let mut sealed = self.sealed.write();
2775
2776 let mut positions = Vec::with_capacity(run.sources.len());
2777 for source in &run.sources {
2778 if let Some(index) = sealed
2779 .iter()
2780 .position(|segment| segment.read().id() == source.id)
2781 {
2782 positions.push(index);
2783 }
2784 }
2785 if positions.is_empty() {
2786 return;
2787 }
2788 positions.sort_unstable();
2789
2790 let mut stale = Vec::new();
2794 for source in &run.sources {
2795 let Some(source_arc) = sealed
2796 .iter()
2797 .find(|segment| segment.read().id() == source.id)
2798 else {
2799 continue;
2800 };
2801 let segment = source_arc.read();
2802 if source.tombstones_at_open == Some(segment.tombstone_count()) {
2803 continue;
2804 }
2805 stale.extend(
2806 source
2807 .copied
2808 .iter()
2809 .copied()
2810 .filter(|id| !segment.contains(*id)),
2811 );
2812 }
2813 for id in stale {
2814 run.merged.evict_entity(id);
2815 }
2816
2817 let reclaimed_bytes: u64 = positions
2818 .iter()
2819 .map(|&index| sealed[index].read().resident_bytes())
2820 .sum();
2821 let reclaimed_tombstones: u64 = positions
2822 .iter()
2823 .map(|&index| sealed[index].read().tombstone_count() as u64)
2824 .sum();
2825
2826 if run.merged.seal().is_err() {
2827 return;
2828 }
2829 let merged_bytes = run.merged.resident_bytes();
2830 let merged_arc = Arc::new(RwLock::new(run.merged));
2831
2832 {
2833 let mut retired = self.retired.write();
2834 retired.retain(|source| source.strong_count() > 0);
2835 for &index in positions.iter().rev() {
2836 let source = sealed.remove(index);
2837 if Arc::strong_count(&source) > 1 {
2838 retired.push(Arc::downgrade(&source));
2839 }
2840 }
2841 }
2842 let insert_at = positions[0].min(sealed.len());
2843 sealed.insert(insert_at, Arc::clone(&merged_arc));
2844 drop(sealed);
2845
2846 let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
2848 {
2849 let mut entity_segment = self.entity_segment.write();
2850 for segment_id in entity_segment.values_mut() {
2851 if source_ids.contains(segment_id) {
2852 *segment_id = run.merged_id;
2853 }
2854 }
2855 }
2856
2857 {
2858 let mut stats = self.stats.write();
2859 stats.sealed_count = stats
2860 .sealed_count
2861 .saturating_sub(positions.len())
2862 .saturating_add(1);
2863 let consolidation = &mut stats.consolidation;
2864 consolidation.runs_completed += 1;
2865 consolidation.segments_merged += positions.len() as u64;
2866 consolidation.tombstones_reclaimed += reclaimed_tombstones;
2867 consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
2868 }
2869
2870 self.emit(LifecycleEvent::SegmentConsolidated {
2871 source: source_ids,
2872 target: run.merged_id,
2873 });
2874 }
2875
2876 #[cfg(test)]
2879 fn consolidation_progress(&self) -> Option<u64> {
2880 self.consolidation
2881 .read()
2882 .as_ref()
2883 .map(|run| run.entities_copied)
2884 }
2885}
2886
2887#[cfg(test)]
2888mod tests {
2889 use std::collections::HashSet;
2890
2891 use super::*;
2892 use crate::storage::entity::{EntityData, EntityKind, RowData};
2893 use crate::transaction::snapshot::{Snapshot, SnapshotManager};
2894 use reddb_types::Value;
2895
2896 #[test]
2897 fn physical_pages_keep_global_order_across_segments_and_deleted_slots() {
2898 let manager = SegmentManager::new("pages");
2899 for group in [[50, 10], [30, 20]] {
2900 for id in group {
2901 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "pages", id, vec![]);
2902 entity.data = EntityData::Row(RowData::with_names(
2903 vec![Value::Integer(id as i64)],
2904 vec!["id".into()],
2905 ));
2906 manager.insert(entity).unwrap();
2907 }
2908 manager.force_seal().unwrap();
2909 }
2910 manager
2911 .insert(UnifiedEntity::table_row(
2912 EntityId::new(40),
2913 "pages",
2914 40,
2915 vec![],
2916 ))
2917 .unwrap();
2918 let (total, page) = manager.page_in_id_order(1, 2);
2919 assert_eq!(total, 5);
2920 assert_eq!(
2921 page.iter()
2922 .map(|entity| entity.id.raw())
2923 .collect::<Vec<_>>(),
2924 [20, 30]
2925 );
2926 let EntityData::Row(row) = &page[0].data else {
2927 panic!("row")
2928 };
2929 assert_eq!(row.get_field("id"), Some(&Value::Integer(20)));
2930 manager.delete(EntityId::new(30)).unwrap();
2931 let (total, page) = manager.page_in_id_order(1, 2);
2932 assert_eq!(total, 4);
2933 assert_eq!(
2934 page.iter()
2935 .map(|entity| entity.id.raw())
2936 .collect::<Vec<_>>(),
2937 [20, 40]
2938 );
2939 for offset in [4, usize::MAX] {
2940 let (total, page) = manager.page_in_id_order(offset, 2);
2941 assert_eq!(total, 4);
2942 assert!(page.is_empty());
2943 }
2944 }
2945
2946 fn lookup_version(id: u64, key: &str, xmin: u64, xmax: u64) -> UnifiedEntity {
2947 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "keys", id, vec![]);
2948 entity.data = EntityData::Row(RowData::with_names(
2949 vec![Value::text(key)],
2950 vec!["key".into()],
2951 ));
2952 entity.set_xmin(xmin);
2953 entity.set_xmax(xmax);
2954 entity
2955 }
2956
2957 #[test]
2958 fn key_lookup_matches_snapshot_scan_across_segments() {
2959 let manager = Arc::new(SegmentManager::new("keys"));
2960 for entity in [
2961 lookup_version(1, "changed", 3, 12),
2962 lookup_version(2, "deleted", 3, 7),
2963 ] {
2964 manager.insert(entity).expect("old version");
2965 }
2966 manager.force_seal().expect("seal old versions");
2967 for entity in [
2968 lookup_version(3, "changed", 12, 0),
2969 lookup_version(4, "pending", 4, 0),
2970 lookup_version(5, "own", 15, 0),
2971 lookup_version(6, "live", 3, 0),
2972 ] {
2973 manager.insert(entity).expect("new version");
2974 }
2975 std::thread::spawn(move || {
2976 let snapshot = SnapshotContext {
2977 snapshot: Snapshot {
2978 xid: 10,
2979 in_progress: HashSet::from([4]),
2980 },
2981 manager: Arc::new(SnapshotManager::new()),
2982 own_xids: HashSet::from([15]),
2983 requires_index_fallback: false,
2984 serializable_reader: None,
2985 };
2986 for snapshot in [Some(&snapshot), None] {
2987 for key in ["changed", "deleted", "pending", "own", "live", "absent"] {
2988 let expected = manager
2989 .scan(snapshot, |entity| {
2990 crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity)
2991 == Some(key)
2992 })
2993 .into_iter()
2994 .max_by_key(|entity| entity.xmin)
2995 .map(|entity| entity.id);
2996 assert_eq!(
2997 manager.project_key_entity(key, snapshot, true, |entity| entity.id),
2998 expected,
2999 "key={key}"
3000 );
3001 }
3002 }
3003 assert_eq!(
3004 manager.project_key_entity("changed", Some(&snapshot), true, |entity| entity.id),
3005 Some(EntityId::new(1))
3006 );
3007 assert_eq!(
3008 manager.project_key_entity("changed", None, true, |entity| entity.id),
3009 Some(EntityId::new(3))
3010 );
3011 assert!(manager
3012 .project_key_entity("deleted", None, true, |entity| entity.id)
3013 .is_none());
3014 })
3015 .join()
3016 .expect("snapshot worker");
3017 }
3018
3019 #[test]
3020 fn key_lookup_survives_paced_consolidation() {
3021 let manager = consolidating_manager(1);
3022 manager
3023 .bulk_insert(
3024 (1..=10)
3025 .map(|id| lookup_version(id, &format!("key-{id}"), 1, 0))
3026 .collect(),
3027 )
3028 .expect("rows");
3029 manager.force_seal().expect("seal");
3030 for id in 1..=4 {
3031 manager.delete(EntityId::new(id)).expect("delete");
3032 }
3033 assert!(manager.pressure_consolidation_tick());
3034 drain_maintenance(&manager);
3035 for id in 1..=10 {
3036 assert_eq!(
3037 manager.project_key_entity(&format!("key-{id}"), None, false, |entity| entity.id),
3038 (id > 4).then_some(EntityId::new(id))
3039 );
3040 }
3041 }
3042
3043 #[test]
3044 #[ignore = "diagnostic of segment fragmentation and retained history; no timing assertions"]
3045 fn key_lookup_fragmentation_history_diagnostic() {
3046 use std::{hint::black_box, time::Instant};
3047 for segments in [1, 16, 64] {
3048 for versions in [1, 100, 1000] {
3049 let manager = SegmentManager::new("keys");
3050 let mut id = 1;
3051 for segment in 0..segments {
3052 let mut rows: Vec<_> = (0..128)
3053 .map(|_| {
3054 let entity = lookup_version(id, &format!("other-{id}"), 1, 0);
3055 id += 1;
3056 entity
3057 })
3058 .collect();
3059 for version in (segment..versions).step_by(segments) {
3060 rows.push(lookup_version(
3061 id,
3062 "history",
3063 version as u64 + 1,
3064 if version + 1 == versions {
3065 0
3066 } else {
3067 version as u64 + 2
3068 },
3069 ));
3070 id += 1;
3071 }
3072 manager.bulk_insert(rows).expect("rows");
3073 manager.force_seal().expect("seal");
3074 }
3075 let started = Instant::now();
3076 for _ in 0..2000 {
3077 let xmin =
3078 manager.project_key_entity(black_box("history"), None, true, |entity| {
3079 entity.xmin
3080 });
3081 assert_eq!(xmin, Some(versions as u64));
3082 }
3083 eprintln!("segments={segments} versions={versions} reads=2000 elapsed_us={} resident_bytes={}", started.elapsed().as_micros(), manager.resident_bytes());
3084 }
3085 }
3086 }
3087
3088 #[test]
3089 fn ordered_fingerprint_scan_matches_legacy_sort_without_cloning_rows() {
3090 let manager = SegmentManager::new("fingerprint");
3091 let row = |id, label: &str| {
3092 UnifiedEntity::table_row(
3093 EntityId::new(id),
3094 "fingerprint",
3095 id,
3096 vec![Value::text(label)],
3097 )
3098 };
3099 manager
3100 .bulk_insert(vec![row(8, "eight"), row(2, "sealed-two"), row(5, "five")])
3101 .expect("sealed rows");
3102 manager.force_seal().expect("seal");
3103 manager
3104 .bulk_insert(vec![row(7, "seven"), row(2, "growing-two"), row(1, "one")])
3105 .expect("growing rows");
3106
3107 let mut expected = manager.query_all(|_| true);
3108 expected.sort_by_key(|entity| entity.id.raw());
3109 let expected: Vec<_> = expected
3110 .iter()
3111 .map(|entity| format!("{entity:?}"))
3112 .collect();
3113 let mut stored_addresses = HashSet::new();
3114 manager.for_each_entity(|entity| {
3115 stored_addresses.insert(entity as *const UnifiedEntity as usize);
3116 true
3117 });
3118
3119 let mut actual = Vec::new();
3120 manager.for_each_entity_in_id_order(|entity| {
3121 assert!(stored_addresses.contains(&(entity as *const UnifiedEntity as usize)));
3122 assert!(
3123 manager.growing.try_write().is_some(),
3124 "topology guard retained"
3125 );
3126 assert!(
3127 manager.sealed.try_write().is_some(),
3128 "topology guard retained"
3129 );
3130 actual.push(format!("{entity:?}"));
3131 true
3132 });
3133 assert_eq!(
3134 actual, expected,
3135 "equal IDs retain their original segment order"
3136 );
3137 }
3138
3139 #[test]
3140 fn ordered_fingerprint_scan_stops_early_and_releases_sources() {
3141 let manager = SegmentManager::new("fingerprint");
3142 for id in [3, 1, 2] {
3143 manager
3144 .insert(UnifiedEntity::table_row(
3145 EntityId::new(id),
3146 "fingerprint",
3147 id,
3148 vec![Value::Integer(id as i64)],
3149 ))
3150 .expect("row");
3151 }
3152 let source = manager.growing.read().as_ref().expect("source").clone();
3153 let handles = Arc::strong_count(&source);
3154 let mut seen = Vec::new();
3155 manager.for_each_entity_in_id_order(|entity| {
3156 seen.push(entity.id.raw());
3157 false
3158 });
3159 assert_eq!(seen, vec![1]);
3160 assert_eq!(Arc::strong_count(&source), handles);
3161 assert!(
3162 source.try_write().is_some(),
3163 "segment guard retained after early stop"
3164 );
3165 }
3166
3167 #[test]
3168 fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
3169 let manager = SegmentManager::with_config(
3170 "vectors",
3171 ManagerConfig {
3172 max_sealed_segments: 1,
3173 consolidation_entities_per_tick: 10_000,
3174 ..Default::default()
3175 },
3176 );
3177 for part in 0..3 {
3178 manager
3179 .bulk_insert(
3180 (1..=300)
3181 .map(|offset| {
3182 UnifiedEntity::vector(
3183 EntityId::new(part * 300 + offset),
3184 "vectors",
3185 vec![1.0, 0.0],
3186 )
3187 })
3188 .collect(),
3189 )
3190 .expect("batch");
3191 if part < 2 {
3192 manager.force_seal().expect("seal");
3193 }
3194 }
3195 let original_growing = manager.growing.read().as_ref().expect("growing").clone();
3196 let mut changed = false;
3197 let mut seen = Vec::new();
3198 assert!(manager.scan_batches(
3199 None,
3200 || true,
3201 |batch| {
3202 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3203 assert!(
3204 manager.growing.try_write().is_some(),
3205 "consumer holds no topology guard"
3206 );
3207 assert!(
3208 original_growing.try_write().is_some(),
3209 "consumer holds no segment guard"
3210 );
3211 for entity in batch {
3212 assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
3213 seen.push(entity.id.raw());
3214 }
3215 if !changed {
3216 changed = true;
3217 manager.force_seal().expect("seal during scan");
3218 manager
3219 .insert(UnifiedEntity::vector(
3220 EntityId::new(5000),
3221 "vectors",
3222 vec![1.0, 0.0],
3223 ))
3224 .expect("new write");
3225 drain_maintenance(&manager);
3226 assert!(manager.stats().consolidation.runs_completed > 0);
3227 assert!(manager
3228 .delete(EntityId::new(1))
3229 .expect("delete after source retirement"));
3230 assert!(manager.get(EntityId::new(1)).is_none());
3231 }
3232 true
3233 }
3234 ));
3235 seen.sort_unstable();
3236 assert_eq!(
3237 seen,
3238 (2..=900).collect::<Vec<_>>(),
3239 "no duplicate, missing or late candidates"
3240 );
3241 let mut after = Vec::new();
3242 assert!(manager.scan_batches(
3243 None,
3244 || true,
3245 |batch| {
3246 after.extend(batch.iter().map(|entity| entity.id.raw()));
3247 true
3248 }
3249 ));
3250 after.sort_unstable();
3251 let mut expected: Vec<_> = (2..=900).collect();
3252 expected.push(5000);
3253 assert_eq!(after, expected, "adopted entities have scan positions");
3254 }
3255
3256 #[test]
3257 fn bounded_scan_applies_snapshot_to_relocated_versions() {
3258 for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
3259 let manager = SegmentManager::with_config(
3260 "vectors",
3261 ManagerConfig {
3262 max_sealed_segments: 1,
3263 ..Default::default()
3264 },
3265 );
3266 for ids in [[1, 2], [3, 4]] {
3267 let entities = ids.map(|id| {
3268 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3269 entity.set_xmin(3);
3270 entity
3271 });
3272 if bulk {
3273 manager
3274 .bulk_insert(entities.into())
3275 .expect("initial versions");
3276 } else {
3277 for entity in entities {
3278 manager.insert(entity).expect("initial version");
3279 }
3280 }
3281 manager.force_seal().expect("seal source");
3282 }
3283 let snapshot = SnapshotContext {
3284 snapshot: Snapshot {
3285 xid: 10,
3286 in_progress: HashSet::new(),
3287 },
3288 manager: Arc::new(SnapshotManager::new()),
3289 own_xids: HashSet::new(),
3290 requires_index_fallback: false,
3291 serializable_reader: None,
3292 };
3293 let mut changed = false;
3294 let mut seen = Vec::new();
3295 assert!(manager.scan_batches(
3296 Some(&snapshot),
3297 || true,
3298 |batch| {
3299 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3300 if !changed {
3301 changed = true;
3302 if consolidate {
3303 drain_maintenance(&manager);
3304 assert!(manager.stats().consolidation.runs_completed > 0);
3305 }
3306 for (id, xmax) in [(3, 8), (4, 12)] {
3307 let mut entity =
3308 manager.get(EntityId::new(id)).expect("retained version");
3309 entity.set_xmax(xmax);
3310 manager
3311 .update(entity)
3312 .expect("update after source retirement");
3313 }
3314 }
3315 true
3316 }
3317 ));
3318 seen.sort_unstable();
3319 assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
3320 let mut again = Vec::new();
3321 assert!(manager.scan_batches(
3322 Some(&snapshot),
3323 || true,
3324 |batch| {
3325 again.extend(batch.iter().map(|entity| entity.id.raw()));
3326 true
3327 }
3328 ));
3329 again.sort_unstable();
3330 assert_eq!(
3331 again,
3332 vec![1, 2, 4],
3333 "old slots do not duplicate relocated versions"
3334 );
3335 let mut current = Vec::new();
3336 assert!(manager.scan_batches(
3337 None,
3338 || true,
3339 |batch| {
3340 current.extend(batch.iter().map(|entity| entity.id.raw()));
3341 true
3342 }
3343 ));
3344 current.sort_unstable();
3345 assert_eq!(
3346 current,
3347 vec![1, 2],
3348 "current view excludes both deleted versions"
3349 );
3350 }
3351 }
3352
3353 #[test]
3354 fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
3355 let manager = SegmentManager::new("mixed");
3356 let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
3357 manager
3358 .bulk_insert((1..=300).map(vector).collect())
3359 .expect("flat");
3360 manager.insert(vector(500)).expect("hash insert");
3361 manager
3362 .bulk_insert(vec![vector(502), vector(505)])
3363 .expect("gapped bulk");
3364 manager.delete(EntityId::new(2)).expect("flat delete");
3365 manager.delete(EntityId::new(505)).expect("hash delete");
3366 let mut changed = false;
3367 let mut seen = Vec::new();
3368 assert!(manager.scan_batches(
3369 None,
3370 || true,
3371 |batch| {
3372 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3373 if !changed {
3374 changed = true;
3375 manager
3376 .insert(vector(700))
3377 .expect("append after cursor boundary");
3378 manager
3379 .update(UnifiedEntity::graph_node(
3380 EntityId::new(500),
3381 "mixed",
3382 "Node",
3383 HashMap::new(),
3384 ))
3385 .expect("structural update");
3386 }
3387 true
3388 }
3389 ));
3390 seen.sort_unstable();
3391 let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
3392 expected.extend([500, 502]);
3393 assert_eq!(seen, expected);
3394 }
3395
3396 #[test]
3397 fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
3398 let manager = SegmentManager::new("vectors");
3399 let mut entities = Vec::new();
3400 for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
3401 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3402 entity.set_xmin(xmin);
3403 entity.set_xmax(xmax);
3404 entities.push(entity);
3405 }
3406 manager.bulk_insert(entities).expect("versions");
3407 manager.force_seal().expect("seal");
3408 let snapshot = SnapshotContext {
3409 snapshot: Snapshot {
3410 xid: 10,
3411 in_progress: HashSet::from([4]),
3412 },
3413 manager: Arc::new(SnapshotManager::new()),
3414 own_xids: HashSet::new(),
3415 requires_index_fallback: false,
3416 serializable_reader: None,
3417 };
3418 let mut work = 0;
3419 let mut callbacks = 0;
3420 assert!(!manager.scan_batches(
3421 Some(&snapshot),
3422 || {
3423 work += 1;
3424 work <= 2
3425 },
3426 |_| {
3427 callbacks += 1;
3428 true
3429 }
3430 ));
3431 assert_eq!(callbacks, 0, "a stopped batch is not consumed");
3432 for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
3433 let mut seen = Vec::new();
3434 assert!(manager.scan_batches(
3435 view,
3436 || true,
3437 |batch| {
3438 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3439 true
3440 }
3441 ));
3442 seen.sort_unstable();
3443 assert_eq!(seen, expected);
3444 }
3445 }
3446
3447 #[test]
3448 fn test_manager_basic() {
3449 let manager = SegmentManager::new("test_collection");
3450
3451 let entity = UnifiedEntity::table_row(
3452 manager.next_entity_id(),
3453 "users",
3454 1,
3455 vec![Value::text("Alice".to_string())],
3456 );
3457
3458 let id = manager.insert(entity).unwrap();
3459 assert!(manager.get(id).is_some());
3460 assert_eq!(manager.count(), 1);
3461 }
3462
3463 #[test]
3464 fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
3465 let manager = Arc::new(SegmentManager::new("mixed"));
3466 manager
3467 .bulk_insert(
3468 (1..=128)
3469 .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
3470 .collect(),
3471 )
3472 .expect("vector-only segment");
3473 manager.force_seal().expect("seal vectors");
3474 let mut old =
3475 UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
3476 old.set_xmin(3);
3477 old.set_xmax(12);
3478 let mut new =
3479 UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
3480 new.set_xmin(12);
3481 let edge = UnifiedEntity::graph_edge(
3482 EntityId::new(202),
3483 "mixed",
3484 "a",
3485 "b",
3486 1.0,
3487 Default::default(),
3488 );
3489 manager
3490 .bulk_insert(vec![old, new, edge])
3491 .expect("physical graph versions");
3492 manager.force_seal().expect("seal graph");
3493 manager
3494 .insert(UnifiedEntity::graph_node(
3495 EntityId::new(203),
3496 "mixed",
3497 "person",
3498 Default::default(),
3499 ))
3500 .expect("growing node");
3501 let snapshot = SnapshotContext {
3502 snapshot: Snapshot {
3503 xid: 10,
3504 in_progress: HashSet::new(),
3505 },
3506 manager: Arc::new(SnapshotManager::new()),
3507 own_xids: HashSet::new(),
3508 requires_index_fallback: false,
3509 serializable_reader: None,
3510 };
3511 std::thread::spawn(move || {
3512 for view in [None, Some(&snapshot)] {
3513 for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
3514 let expected: HashSet<_> = manager
3515 .scan(view, |item| kind.matches(&item.kind))
3516 .into_iter()
3517 .map(|item| item.id)
3518 .collect();
3519 let actual: HashSet<_> = manager
3520 .scan_graph_kind(view, kind)
3521 .into_iter()
3522 .map(|item| item.id)
3523 .collect();
3524 assert_eq!(actual, expected);
3525 let mut visited = 0;
3526 let mut candidates = HashSet::new();
3527 manager.scan_graph_candidates_for_each(
3528 view,
3529 kind,
3530 || true,
3531 |item| {
3532 visited += 1;
3533 if kind.matches(&item.kind) {
3534 candidates.insert(item.id);
3535 }
3536 true
3537 },
3538 );
3539 assert_eq!(candidates, expected);
3540 assert_eq!(
3541 visited,
3542 if matches!(kind, GraphEntityKind::Node) {
3543 3
3544 } else {
3545 2
3546 }
3547 );
3548 }
3549 }
3550 let ids: HashSet<_> = manager
3551 .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
3552 .into_iter()
3553 .map(|item| item.id.raw())
3554 .collect();
3555 assert_eq!(ids, HashSet::from([200, 203]));
3556 let ids: HashSet<_> = manager
3557 .scan_graph_kind(None, GraphEntityKind::Node)
3558 .into_iter()
3559 .map(|item| item.id.raw())
3560 .collect();
3561 assert_eq!(ids, HashSet::from([201, 203]));
3562 let mut visits = 0;
3563 manager.scan_graph_candidates_for_each(
3564 None,
3565 GraphEntityKind::Node,
3566 || true,
3567 |_| {
3568 visits += 1;
3569 false
3570 },
3571 );
3572 assert_eq!(
3573 visits, 1,
3574 "early termination must also stop subsequent segments"
3575 );
3576 })
3577 .join()
3578 .expect("snapshot worker");
3579 }
3580
3581 #[test]
3582 fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
3583 let manager = Arc::new(SegmentManager::new("accounts"));
3584 let cases = [
3585 ("legacy row", 0, 0, true),
3586 ("committed creator", 3, 0, true),
3587 ("future creator", 11, 0, false),
3588 ("in-progress creator", 4, 0, false),
3589 ("committed deleter", 3, 7, false),
3590 ("future deleter", 3, 11, true),
3591 ("in-progress deleter", 3, 4, true),
3592 ];
3593
3594 for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
3595 let row_id = index as u64 + 1;
3596 let mut entity = UnifiedEntity::table_row(
3597 EntityId::new(row_id),
3598 "accounts",
3599 row_id,
3600 vec![Value::Integer(row_id as i64)],
3601 );
3602 entity.set_xmin(*xmin);
3603 entity.set_xmax(*xmax);
3604 manager.insert(entity).unwrap();
3605 }
3606
3607 let snapshot = SnapshotContext {
3608 snapshot: Snapshot {
3609 xid: 10,
3610 in_progress: HashSet::from([4]),
3611 },
3612 manager: Arc::new(SnapshotManager::new()),
3613 own_xids: HashSet::new(),
3614 requires_index_fallback: false,
3615 serializable_reader: None,
3616 };
3617
3618 let raw_visible_row_ids: HashSet<u64> = manager
3619 .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
3620 .into_iter()
3621 .filter_map(|entity| match entity.kind {
3622 EntityKind::TableRow { row_id, .. } => Some(row_id),
3623 _ => None,
3624 })
3625 .collect();
3626
3627 let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3628 .join()
3629 .unwrap();
3630 let visible_row_ids: HashSet<u64> = scanned
3631 .into_iter()
3632 .filter_map(|entity| match entity.kind {
3633 EntityKind::TableRow { row_id, .. } => Some(row_id),
3634 _ => None,
3635 })
3636 .collect();
3637 assert_eq!(visible_row_ids, raw_visible_row_ids);
3638
3639 for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3640 let row_id = index as u64 + 1;
3641 assert_eq!(
3642 visible_row_ids.contains(&row_id),
3643 *expected_visible,
3644 "{name} visibility mismatch"
3645 );
3646 }
3647 }
3648
3649 #[test]
3650 fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3651 let manager = SegmentManager::new("accounts");
3655 let mut live =
3656 UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3657 live.set_xmin(2);
3658 live.set_xmax(0);
3659 manager.insert(live).unwrap();
3660
3661 let mut superseded =
3662 UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3663 superseded.set_xmin(2);
3664 superseded.set_xmax(9);
3665 manager.insert(superseded).unwrap();
3666
3667 let rows: Vec<u64> = manager
3668 .scan(None, |_| true)
3669 .into_iter()
3670 .filter_map(|entity| match entity.kind {
3671 EntityKind::TableRow { row_id, .. } => Some(row_id),
3672 _ => None,
3673 })
3674 .collect();
3675 assert_eq!(
3676 rows,
3677 vec![1],
3678 "frameless scan must hide superseded versions"
3679 );
3680 }
3681
3682 #[test]
3683 fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3684 let manager = SegmentManager::new("test_collection");
3685
3686 let entity = UnifiedEntity::table_row(
3687 manager.next_entity_id(),
3688 "users",
3689 1,
3690 vec![Value::text("Alice".to_string())],
3691 );
3692
3693 let id = manager.insert(entity).unwrap();
3694
3695 assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3696 assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
3697 assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
3698
3699 assert!(manager.delete(id).unwrap());
3700 assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3701 }
3702
3703 #[test]
3704 fn zoned_scan_reports_pruned_segments_and_preserves_results() {
3705 let config = ManagerConfig {
3706 segment_config: SegmentConfig {
3707 max_entities: 2,
3708 ..Default::default()
3709 },
3710 ..Default::default()
3711 };
3712 let manager = SegmentManager::with_config("events", config);
3713
3714 for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
3715 let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
3716 let entity = UnifiedEntity::new(
3717 manager.next_entity_id(),
3718 EntityKind::TableRow {
3719 table: "events".into(),
3720 row_id: (idx + 1) as u64,
3721 },
3722 EntityData::Row(row),
3723 );
3724 manager.insert(entity).unwrap();
3725 }
3726
3727 let full = manager.query_all(|entity| match &entity.data {
3728 EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
3729 _ => false,
3730 });
3731 let probe = Value::Integer(100);
3732 let (pruned, stats) =
3733 manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
3734 match &entity.data {
3735 EntityData::Row(row) => {
3736 matches!(row.get_field("ts"), Some(Value::Integer(100)))
3737 }
3738 _ => false,
3739 }
3740 });
3741
3742 assert_eq!(pruned.len(), full.len());
3743 assert_eq!(pruned[0].id, full[0].id);
3744 assert_eq!(stats.segments_total, 3);
3745 assert_eq!(stats.segments_scanned, 2);
3746 assert_eq!(stats.segments_pruned, 1);
3747 }
3748
3749 #[test]
3750 fn test_manager_auto_seal() {
3751 let config = ManagerConfig {
3752 segment_config: SegmentConfig {
3753 max_entities: 2,
3754 ..Default::default()
3755 },
3756 ..Default::default()
3757 };
3758
3759 let manager = SegmentManager::with_config("test", config);
3760
3761 manager
3763 .insert(UnifiedEntity::vector(
3764 manager.next_entity_id(),
3765 "v",
3766 vec![0.1],
3767 ))
3768 .unwrap();
3769
3770 manager
3772 .insert(UnifiedEntity::vector(
3773 manager.next_entity_id(),
3774 "v",
3775 vec![0.2],
3776 ))
3777 .unwrap();
3778
3779 manager
3781 .insert(UnifiedEntity::vector(
3782 manager.next_entity_id(),
3783 "v",
3784 vec![0.3],
3785 ))
3786 .unwrap();
3787
3788 let stats = manager.stats();
3789 assert_eq!(stats.total_entities, 3);
3790 }
3791
3792 #[test]
3793 fn test_manager_delete() {
3794 let manager = SegmentManager::new("test");
3795
3796 let id = manager
3797 .insert(UnifiedEntity::vector(
3798 manager.next_entity_id(),
3799 "v",
3800 vec![0.1],
3801 ))
3802 .unwrap();
3803
3804 assert!(manager.get(id).is_some());
3805 assert!(manager.delete(id).unwrap());
3806 assert!(manager.get(id).is_none());
3807 }
3808
3809 #[test]
3810 fn test_manager_metadata() {
3811 let manager = SegmentManager::new("test");
3812
3813 let id = manager
3814 .insert(UnifiedEntity::table_row(
3815 manager.next_entity_id(),
3816 "hosts",
3817 1,
3818 vec![Value::text("192.168.1.1".to_string())],
3819 ))
3820 .unwrap();
3821
3822 let mut meta = Metadata::new();
3823 meta.set(
3824 "os",
3825 super::super::metadata::MetadataValue::String("linux".to_string()),
3826 );
3827
3828 manager.set_metadata(id, meta).unwrap();
3829
3830 let retrieved = manager.get_metadata(id).unwrap();
3831 assert!(retrieved.has("os"));
3832 }
3833
3834 #[test]
3835 fn test_manager_query_by_kind() {
3836 let manager = SegmentManager::new("test");
3837
3838 manager
3839 .insert(UnifiedEntity::table_row(
3840 manager.next_entity_id(),
3841 "hosts",
3842 1,
3843 vec![],
3844 ))
3845 .unwrap();
3846
3847 manager
3848 .insert(UnifiedEntity::vector(
3849 manager.next_entity_id(),
3850 "embeddings",
3851 vec![0.1],
3852 ))
3853 .unwrap();
3854
3855 manager
3856 .insert(UnifiedEntity::table_row(
3857 manager.next_entity_id(),
3858 "hosts",
3859 2,
3860 vec![],
3861 ))
3862 .unwrap();
3863
3864 let rows = manager.get_by_kind("table");
3865 assert_eq!(rows.len(), 2);
3866
3867 let vectors = manager.get_by_kind("vector");
3868 assert_eq!(vectors.len(), 1);
3869 }
3870
3871 fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
3878 SegmentManager::with_config(
3879 "test",
3880 ManagerConfig {
3881 max_sealed_segments: 1_000,
3882 consolidation_entities_per_tick: entities_per_tick,
3883 ..Default::default()
3884 },
3885 )
3886 }
3887
3888 fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
3892 let entities: Vec<UnifiedEntity> = (0..count)
3893 .map(|i| {
3894 UnifiedEntity::table_row(
3895 manager.next_entity_id(),
3896 "users",
3897 i as u64,
3898 vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
3899 )
3900 })
3901 .collect();
3902 let ids = manager.bulk_insert(entities).expect("bulk insert");
3903 manager.force_seal().expect("seal");
3904 ids
3905 }
3906
3907 fn live_ids(manager: &SegmentManager) -> Vec<u64> {
3908 let mut ids: Vec<u64> = manager
3909 .query_all(|_| true)
3910 .into_iter()
3911 .map(|entity| entity.id.raw())
3912 .collect();
3913 ids.sort_unstable();
3914 ids
3915 }
3916
3917 fn drain_maintenance(manager: &SegmentManager) -> usize {
3918 let mut ticks = 0;
3919 while manager.consolidation.read().is_some() || ticks == 0 {
3920 manager.run_maintenance().expect("maintenance");
3921 ticks += 1;
3922 assert!(ticks < 1_000, "consolidation failed to converge");
3923 }
3924 ticks
3925 }
3926
3927 #[test]
3928 fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
3929 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3930 let ids = manager
3931 .bulk_insert(
3932 (1..=1024)
3933 .map(|id| {
3934 UnifiedEntity::graph_edge(
3935 EntityId::new(id),
3936 "step",
3937 "2000",
3938 "3000",
3939 1.0,
3940 HashMap::new(),
3941 )
3942 })
3943 .collect(),
3944 )
3945 .expect("edges");
3946 manager.force_seal().expect("seal");
3947 for id in ids.iter().take(300) {
3948 manager.delete(*id).expect("fragment source");
3949 }
3950 let source = Arc::downgrade(&manager.sealed.read()[0]);
3951 let mut seen = Vec::new();
3952 let mut changed = false;
3953 let mut metadata_bytes = 0;
3954 assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
3955 || true,
3956 |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
3957 |batch| {
3958 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3959 assert!(manager.growing.try_write().is_some(), "no growing topology guard");
3960 assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
3961 assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
3962 if !changed {
3963 changed = true;
3964 drain_maintenance(&manager);
3965 assert!(manager.stats().consolidation.runs_completed > 0);
3966 assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
3967 let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
3968 if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
3969 manager.update(updated).expect("update authoritative merged copy");
3970 }
3971 for id in batch {
3972 let entity = manager.get(*id).expect("hydrate current manager");
3973 if id.raw() == 1024 {
3974 assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
3975 }
3976 seen.push(id.raw());
3977 }
3978 Ok(true)
3979 }).expect("cursor"));
3980 assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
3981 assert!(
3982 source.upgrade().is_none(),
3983 "finished cursor releases retired source"
3984 );
3985 assert!(
3986 metadata_bytes < 4096,
3987 "fixed ID buffer plus source handles: {metadata_bytes}"
3988 );
3989 }
3990
3991 #[test]
3992 fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
3993 let manager = SegmentManager::new("graph");
3994 manager
3995 .bulk_insert(
3996 (1..=1024)
3997 .map(|id| {
3998 UnifiedEntity::graph_edge(
3999 EntityId::new(id),
4000 "step",
4001 "2000",
4002 "3000",
4003 1.0,
4004 HashMap::new(),
4005 )
4006 })
4007 .collect(),
4008 )
4009 .expect("edges");
4010 let mut delivered = 0;
4011 let denied = manager.visit_graph_index_batches(
4012 GraphEntityKind::Edge,
4013 &[EntityId::new(2000)],
4014 || true,
4015 |_| Err("out of memory"),
4016 |ids| {
4017 delivered += ids.len();
4018 Ok(true)
4019 },
4020 );
4021 assert_eq!(denied, Err("out of memory"));
4022 assert_eq!(delivered, 0);
4023 let mut work = 0;
4024 assert!(!manager
4025 .visit_graph_index_batches(
4026 GraphEntityKind::Edge,
4027 &[EntityId::new(2000)],
4028 || {
4029 work += 1;
4030 work < 100
4031 },
4032 |_| Ok::<_, ()>(()),
4033 |ids| {
4034 delivered += ids.len();
4035 Ok(true)
4036 }
4037 )
4038 .expect("cancelled cursor"));
4039 assert_eq!(
4040 delivered, 0,
4041 "cancelled partial batch cannot reach consumer"
4042 );
4043 assert!(!manager
4044 .visit_graph_index_batches(
4045 GraphEntityKind::Edge,
4046 &[EntityId::new(2000)],
4047 || true,
4048 |_| Ok::<_, ()>(()),
4049 |ids| {
4050 delivered += ids.len();
4051 Ok(false)
4052 }
4053 )
4054 .expect("consumer stop"));
4055 assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
4056 }
4057
4058 #[test]
4059 fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
4060 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4061 let ids = seal_bulk_segment(&manager, 100);
4062 for id in ids.iter().take(40) {
4063 manager.delete(*id).expect("delete before scan");
4064 }
4065 let source_bytes = manager.resident_bytes();
4066 let source = Arc::downgrade(&manager.sealed.read()[0]);
4067 std::thread::scope(|scope| {
4068 let (ready, waiting) = std::sync::mpsc::channel();
4069 let (release_first, first_wait) = std::sync::mpsc::channel();
4070 let (release_last, last_wait) = std::sync::mpsc::channel();
4071 let reader = |resume: std::sync::mpsc::Receiver<()>,
4072 ready: std::sync::mpsc::Sender<()>| {
4073 assert!(manager.scan_batches(
4074 None,
4075 || true,
4076 |_| {
4077 ready.send(()).expect("signal captured cursor");
4078 let _ = resume.recv();
4079 true
4080 }
4081 ));
4082 };
4083 let ready_first = ready.clone();
4084 let first = scope.spawn(move || reader(first_wait, ready_first));
4085 let last = scope.spawn(move || reader(last_wait, ready));
4086 for _ in 0..2 {
4087 waiting
4088 .recv_timeout(std::time::Duration::from_secs(10))
4089 .expect("reader ready");
4090 }
4091 drain_maintenance(&manager);
4092 assert!(
4093 source.upgrade().is_some(),
4094 "both cursors pin the retired source"
4095 );
4096 let active_bytes = manager.sealed.read()[0].read().resident_bytes();
4097 let held = manager.resident_bytes();
4098 assert!(
4099 held >= active_bytes + source_bytes,
4100 "retired source remains resident"
4101 );
4102 release_first.send(()).expect("release first reader");
4103 first.join().expect("first reader");
4104 assert_eq!(
4105 manager.resident_bytes(),
4106 held,
4107 "one reader still pins the source"
4108 );
4109 release_last.send(()).expect("release last reader");
4110 last.join().expect("last reader");
4111 assert!(
4112 source.upgrade().is_none(),
4113 "accounting must not retain payloads"
4114 );
4115 assert_eq!(held - manager.resident_bytes(), source_bytes);
4116 assert_eq!(
4117 manager.stats().total_memory_bytes as u64,
4118 manager.resident_bytes()
4119 );
4120 });
4121 }
4122
4123 #[test]
4124 fn retained_memory_includes_an_inflight_consolidation_copy() {
4125 let manager = consolidating_manager(1);
4126 let ids = seal_bulk_segment(&manager, 100);
4127 for id in ids.iter().take(40) {
4128 manager.delete(*id).expect("delete");
4129 }
4130 let before = manager.resident_bytes();
4131 assert!(manager.pressure_consolidation_tick());
4132 let copied = manager
4133 .consolidation
4134 .read()
4135 .as_ref()
4136 .expect("paced merge")
4137 .merged
4138 .resident_bytes();
4139 assert!(copied > 0);
4140 assert!(
4141 manager.resident_bytes() >= before + copied,
4142 "unpublished copy consumes memory"
4143 );
4144 drain_maintenance(&manager);
4145 assert!(
4146 manager.resident_bytes() < before,
4147 "completed merge releases the old storage"
4148 );
4149 }
4150
4151 #[test]
4152 fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
4153 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4154 let ids = seal_bulk_segment(&manager, 100);
4155
4156 let bytes_before_delete = manager.resident_bytes();
4157
4158 for id in ids.iter().take(40) {
4160 assert!(manager.delete(*id).expect("delete"));
4161 }
4162
4163 let bytes_with_tombstones = manager.resident_bytes();
4166 assert!(bytes_with_tombstones > bytes_before_delete);
4167
4168 let survivors = live_ids(&manager);
4169 assert_eq!(survivors.len(), 60);
4170
4171 drain_maintenance(&manager);
4172
4173 let stats = manager.stats();
4174 assert_eq!(stats.consolidation.runs_started, 1);
4175 assert_eq!(stats.consolidation.runs_completed, 1);
4176 assert_eq!(stats.consolidation.segments_merged, 1);
4177 assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
4178 assert!(stats.consolidation.bytes_reclaimed > 0);
4179
4180 let bytes_after = manager.resident_bytes();
4182 assert!(
4183 bytes_after <= bytes_before_delete,
4184 "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
4185 );
4186 assert_eq!(
4187 bytes_with_tombstones - bytes_after,
4188 stats.consolidation.bytes_reclaimed
4189 );
4190 assert_eq!(stats.total_memory_bytes, bytes_after as usize);
4191
4192 assert_eq!(live_ids(&manager), survivors);
4193 assert_eq!(manager.sealed.read().len(), 1);
4194 assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
4195 }
4196
4197 #[test]
4198 fn query_results_are_identical_before_during_and_after_consolidation() {
4199 let manager = consolidating_manager(3);
4200 let ids = seal_bulk_segment(&manager, 20);
4201 seal_bulk_segment(&manager, 20);
4202 for id in ids.iter().take(10) {
4203 assert!(manager.delete(*id).expect("delete"));
4204 }
4205
4206 let before = live_ids(&manager);
4207 let survivor = *ids.last().expect("at least one row");
4208 let tombstoned = ids[0];
4209
4210 let mut mid_tick_observations = 0;
4213 while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
4214 manager.run_maintenance().expect("maintenance");
4215 assert_eq!(live_ids(&manager), before);
4216 assert!(manager.get(survivor).is_some());
4217 assert!(manager.get(tombstoned).is_none());
4218 assert_eq!(manager.get_by_kind("table").len(), before.len());
4219 mid_tick_observations += 1;
4220 assert!(mid_tick_observations < 1_000);
4221 }
4222
4223 assert!(
4224 mid_tick_observations > 2,
4225 "expected the run to span several ticks, saw {mid_tick_observations}"
4226 );
4227 assert_eq!(live_ids(&manager), before);
4228 assert!(manager.get(survivor).is_some());
4229 assert!(manager.get(tombstoned).is_none());
4230 assert_eq!(manager.get_by_kind("table").len(), before.len());
4231 }
4232
4233 #[test]
4234 fn get_by_kind_reaches_sealed_segments() {
4235 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4236 let ids = seal_bulk_segment(&manager, 5);
4237 assert!(manager.delete(ids[0]).expect("delete"));
4238
4239 assert_eq!(manager.get_by_kind("table").len(), 4);
4243 assert_eq!(manager.get_by_kind("vector").len(), 0);
4244 }
4245
4246 #[test]
4247 fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4248 const PER_TICK: usize = 4;
4249 let manager = consolidating_manager(PER_TICK);
4250 let ids = seal_bulk_segment(&manager, 60);
4251 for id in ids.iter().take(30) {
4252 assert!(manager.delete(*id).expect("delete"));
4253 }
4254
4255 manager.run_maintenance().expect("maintenance");
4257 assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4258
4259 let mut previous = PER_TICK as u64;
4262 let mut ticks = 1;
4263 while let Some(copied) = manager.consolidation_progress() {
4264 assert!(
4265 copied - previous <= PER_TICK as u64,
4266 "tick copied {} entities, bound is {PER_TICK}",
4267 copied - previous
4268 );
4269 previous = copied;
4270 manager.run_maintenance().expect("maintenance");
4271 ticks += 1;
4272 assert!(ticks < 100);
4273 }
4274
4275 assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4278 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4279 assert_eq!(live_ids(&manager).len(), 30);
4280 }
4281
4282 #[test]
4283 fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4284 let manager = SegmentManager::with_config(
4285 "test",
4286 ManagerConfig {
4287 max_sealed_segments: 3,
4288 ..Default::default()
4289 },
4290 );
4291 for _ in 0..4 {
4292 seal_bulk_segment(&manager, 5);
4293 }
4294 assert_eq!(manager.sealed.read().len(), 4);
4295
4296 let before = live_ids(&manager);
4297 drain_maintenance(&manager);
4298
4299 assert_eq!(manager.sealed.read().len(), 1);
4302 assert_eq!(manager.stats().consolidation.segments_merged, 4);
4303 assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4304 assert_eq!(live_ids(&manager), before);
4305 }
4306
4307 #[test]
4308 fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4309 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4310 seal_bulk_segment(&manager, 10);
4311 seal_bulk_segment(&manager, 10);
4312
4313 for _ in 0..5 {
4314 manager.run_maintenance().expect("maintenance");
4315 }
4316
4317 assert_eq!(manager.stats().consolidation.runs_started, 0);
4318 assert_eq!(manager.sealed.read().len(), 2);
4319 }
4320
4321 #[test]
4322 fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4323 use std::sync::atomic::AtomicBool;
4324
4325 let manager = Arc::new(consolidating_manager(2));
4326 let ids = seal_bulk_segment(&manager, 40);
4327 seal_bulk_segment(&manager, 40);
4328 for id in ids.iter().take(20) {
4329 assert!(manager.delete(*id).expect("delete"));
4330 }
4331
4332 let expected = live_ids(&manager);
4333 let stop = Arc::new(AtomicBool::new(false));
4334
4335 let readers: Vec<_> = (0..4)
4336 .map(|_| {
4337 let manager = Arc::clone(&manager);
4338 let stop = Arc::clone(&stop);
4339 let expected = expected.clone();
4340 std::thread::spawn(move || {
4341 let mut scans = 0u64;
4342 loop {
4345 let mut seen: Vec<u64> = manager
4346 .query_all(|_| true)
4347 .into_iter()
4348 .map(|entity| entity.id.raw())
4349 .collect();
4350 seen.sort_unstable();
4351 assert_eq!(seen, expected, "reader saw a torn sealed set");
4354 scans += 1;
4355 if stop.load(Ordering::Relaxed) {
4356 break;
4357 }
4358 }
4359 scans
4360 })
4361 })
4362 .collect();
4363
4364 drain_maintenance(&manager);
4365 stop.store(true, Ordering::Relaxed);
4366
4367 for reader in readers {
4368 let scans = reader.join().expect("reader thread");
4369 assert!(scans > 0, "reader never got to scan");
4370 }
4371
4372 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4373 assert_eq!(live_ids(&manager), expected);
4374 }
4375
4376 #[test]
4377 fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4378 let manager = consolidating_manager(4);
4379 let ids = seal_bulk_segment(&manager, 20);
4380 for id in ids.iter().take(6) {
4381 assert!(manager.delete(*id).expect("delete"));
4382 }
4383
4384 manager.run_maintenance().expect("maintenance");
4386 assert_eq!(manager.consolidation_progress(), Some(4));
4387 let doomed = ids[6];
4388 assert!(manager.delete(doomed).expect("delete"));
4389
4390 let expected = live_ids(&manager);
4391 drain_maintenance(&manager);
4392
4393 assert!(manager.get(doomed).is_none(), "deleted row came back");
4394 assert_eq!(live_ids(&manager), expected);
4395 }
4396
4397 #[test]
4398 fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
4399 let manager = consolidating_manager(3);
4400 let ids = seal_bulk_segment(&manager, 30);
4401 for id in ids.iter().take(12) {
4402 assert!(manager.delete(*id).expect("delete"));
4403 }
4404
4405 let before = live_ids(&manager);
4406 let sealed_before: Vec<SegmentId> = manager
4407 .sealed
4408 .read()
4409 .iter()
4410 .map(|segment| segment.read().id())
4411 .collect();
4412
4413 manager.run_maintenance().expect("maintenance");
4414 manager.run_maintenance().expect("maintenance");
4415 assert!(manager.consolidation.read().is_some());
4416
4417 *manager.consolidation.write() = None;
4421
4422 assert_eq!(live_ids(&manager), before);
4423 let sealed_after: Vec<SegmentId> = manager
4424 .sealed
4425 .read()
4426 .iter()
4427 .map(|segment| segment.read().id())
4428 .collect();
4429 assert_eq!(sealed_after, sealed_before);
4430 assert_eq!(manager.stats().consolidation.runs_completed, 0);
4431
4432 drain_maintenance(&manager);
4433 assert_eq!(live_ids(&manager), before);
4434 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4435 }
4436
4437 #[test]
4438 fn consolidation_preserves_entity_payload_metadata_and_sequence() {
4439 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4440
4441 let entities: Vec<UnifiedEntity> = (0..10)
4445 .map(|i| {
4446 UnifiedEntity::table_row(
4447 manager.next_entity_id(),
4448 "users",
4449 i as u64,
4450 vec![Value::Integer(i as i64)],
4451 )
4452 })
4453 .collect();
4454 let ids = manager.bulk_insert(entities).expect("bulk insert");
4455
4456 let mut meta = Metadata::new();
4457 meta.set(
4458 "os",
4459 super::super::metadata::MetadataValue::String("linux".to_string()),
4460 );
4461 manager.set_metadata(ids[9], meta).expect("set metadata");
4462 manager.force_seal().expect("seal");
4463
4464 assert!(manager.delete(ids[0]).expect("delete"));
4465 assert!(manager.delete(ids[1]).expect("delete"));
4466 assert!(manager.delete(ids[2]).expect("delete"));
4467
4468 let before: Vec<UnifiedEntity> = {
4469 let mut entities = manager.query_all(|_| true);
4470 entities.sort_by_key(|entity| entity.id.raw());
4471 entities
4472 };
4473
4474 drain_maintenance(&manager);
4475
4476 let after: Vec<UnifiedEntity> = {
4477 let mut entities = manager.query_all(|_| true);
4478 entities.sort_by_key(|entity| entity.id.raw());
4479 entities
4480 };
4481
4482 let columns = |entity: &UnifiedEntity| match &entity.data {
4483 super::super::entity::EntityData::Row(row) => row.columns.clone(),
4484 _ => Vec::new(),
4485 };
4486
4487 assert_eq!(before.len(), after.len());
4488 for (old, new) in before.iter().zip(after.iter()) {
4489 assert_eq!(old.id, new.id);
4490 assert_eq!(columns(old), columns(new));
4491 assert_eq!(
4492 old.sequence_id, new.sequence_id,
4493 "merge must not re-issue sequence ids"
4494 );
4495 }
4496 assert!(manager
4497 .get_metadata(ids[9])
4498 .expect("metadata survives the merge")
4499 .has("os"));
4500 }
4501
4502 #[test]
4503 #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
4504 fn test_lifecycle_events() {
4505 let manager = SegmentManager::new("test");
4506
4507 manager
4508 .insert(UnifiedEntity::vector(
4509 manager.next_entity_id(),
4510 "v",
4511 vec![0.1],
4512 ))
4513 .unwrap();
4514
4515 let events = manager.drain_events();
4516
4517 assert!(events
4519 .iter()
4520 .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
4521 assert!(events
4522 .iter()
4523 .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
4524 }
4525}