1mod graph_index_merge;
16
17use parking_lot::RwLock;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Arc, Weak};
21
22use super::entity::{EntityId, EntityKind, UnifiedEntity};
23use super::metadata::{Metadata, MetadataFilter};
24use super::segment::{
25 GraphEntityKind, GrowingSegment, SegmentConfig, SegmentError, SegmentId, SegmentState,
26 SegmentStats, UnifiedSegment, ZoneColPred, ZoneColPredKind,
27};
28use super::visibility_map::VisibilityMap;
29use crate::runtime::mvcc::{entity_visible_with_context, SnapshotContext};
30
31pub const CONSOLIDATION_TOMBSTONE_RATIO: f64 = 0.20;
38
39pub const CONSOLIDATION_FRAGMENTATION_RATIO: f64 = 0.30;
44
45pub const CONSOLIDATION_ENTITIES_PER_TICK: usize = 4_096;
51
52#[derive(Debug, Clone)]
54pub struct ManagerConfig {
55 pub segment_config: SegmentConfig,
57 pub max_sealed_segments: usize,
59 pub idle_seal_secs: u64,
61 pub enable_consolidation: bool,
63 pub consolidation_entities_per_tick: usize,
65 pub enable_archival: bool,
67 pub archive_age_secs: u64,
69}
70
71impl Default for ManagerConfig {
72 fn default() -> Self {
73 Self {
74 segment_config: SegmentConfig::default(),
75 max_sealed_segments: 10,
76 idle_seal_secs: 300, enable_consolidation: true,
78 consolidation_entities_per_tick: CONSOLIDATION_ENTITIES_PER_TICK,
79 enable_archival: true,
80 archive_age_secs: 86400 * 7, }
82 }
83}
84
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct ConsolidationStats {
91 pub runs_started: u64,
93 pub runs_completed: u64,
95 pub segments_merged: u64,
97 pub tombstones_reclaimed: u64,
99 pub bytes_reclaimed: u64,
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct ManagerStats {
106 pub total_entities: usize,
108 pub growing_count: usize,
110 pub sealed_count: usize,
112 pub archived_count: usize,
114 pub total_memory_bytes: usize,
116 pub seal_ops: u64,
118 pub consolidation: ConsolidationStats,
120}
121
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
123pub struct SegmentScanStats {
124 pub segments_total: u64,
125 pub segments_scanned: u64,
126 pub segments_pruned: u64,
127}
128
129#[derive(Debug, Clone)]
131pub enum LifecycleEvent {
132 SegmentCreated(SegmentId),
133 SegmentSealed(SegmentId),
134 SegmentConsolidated {
135 source: Vec<SegmentId>,
136 target: SegmentId,
137 },
138 SegmentArchived(SegmentId),
139 EntityInserted(EntityId, SegmentId),
140 EntityDeleted(EntityId, SegmentId),
141}
142
143struct Consolidation {
150 sources: Vec<SourceProgress>,
152 merged_id: SegmentId,
154 merged: GrowingSegment,
156 cursor: usize,
158 pending_ids: Vec<EntityId>,
160 pending_cursor: usize,
162 entities_copied: u64,
164}
165
166struct SourceProgress {
168 id: SegmentId,
169 copied: Vec<EntityId>,
171 tombstones_at_open: Option<usize>,
180}
181
182pub struct SegmentManager {
184 row_constraint_lock: Arc<parking_lot::ReentrantMutex<()>>,
187 collection: String,
189 config: ManagerConfig,
191 next_segment_id: AtomicU64,
193 next_entity_id: AtomicU64,
195 next_row_id: AtomicU64,
197 total_entities_atomic: AtomicU64,
201 growing: RwLock<Option<Arc<RwLock<GrowingSegment>>>>,
203 sealed: RwLock<Vec<Arc<RwLock<GrowingSegment>>>>,
205 retired: RwLock<Vec<Weak<RwLock<GrowingSegment>>>>,
207 archived: RwLock<Vec<SegmentId>>,
209 entity_segment: RwLock<HashMap<EntityId, SegmentId>>,
213 column_schema: RwLock<Option<Arc<Vec<String>>>>,
216 stats: RwLock<ManagerStats>,
218 consolidation: RwLock<Option<Consolidation>>,
221 events: RwLock<Vec<LifecycleEvent>>,
223 visibility_map: VisibilityMap,
227}
228
229impl SegmentManager {
230 pub fn new(collection: impl Into<String>) -> Self {
232 Self::with_config(collection, ManagerConfig::default())
233 }
234
235 pub fn with_config(collection: impl Into<String>, config: ManagerConfig) -> Self {
237 Self {
238 row_constraint_lock: Arc::new(parking_lot::ReentrantMutex::new(())),
239 collection: collection.into(),
240 config,
241 next_segment_id: AtomicU64::new(1),
242 next_entity_id: AtomicU64::new(1),
243 next_row_id: AtomicU64::new(1),
244 total_entities_atomic: AtomicU64::new(0),
245 growing: RwLock::new(None),
246 sealed: RwLock::new(Vec::new()),
247 retired: RwLock::new(Vec::new()),
248 archived: RwLock::new(Vec::new()),
249 entity_segment: RwLock::new(HashMap::new()),
250 column_schema: RwLock::new(None),
251 stats: RwLock::new(ManagerStats::default()),
252 consolidation: RwLock::new(None),
253 events: RwLock::new(Vec::new()),
254 visibility_map: VisibilityMap::new(),
255 }
256 }
257
258 pub(crate) fn row_constraint_lock(&self) -> Arc<parking_lot::ReentrantMutex<()>> {
259 Arc::clone(&self.row_constraint_lock)
260 }
261
262 pub fn get_or_init_schema(
264 &self,
265 named: &HashMap<String, reddb_types::Value>,
266 ) -> Arc<Vec<String>> {
267 {
268 let schema = self.column_schema.read();
269 if let Some(ref s) = *schema {
270 return Arc::clone(s);
271 }
272 }
273 let cols: Vec<String> = named.keys().cloned().collect();
274 let arc = Arc::new(cols);
275 *self.column_schema.write() = Some(Arc::clone(&arc));
276 arc
277 }
278
279 pub fn column_schema(&self) -> Option<Arc<Vec<String>>> {
281 self.column_schema.read().clone()
282 }
283
284 pub(crate) fn set_column_schema_if_empty(&self, columns: Vec<String>) {
285 if columns.is_empty() {
286 return;
287 }
288 let mut schema = self.column_schema.write();
289 if schema.is_none() {
290 *schema = Some(Arc::new(columns));
291 }
292 }
293
294 pub fn collection(&self) -> &str {
296 &self.collection
297 }
298
299 pub fn config(&self) -> &ManagerConfig {
301 &self.config
302 }
303
304 pub fn stats(&self) -> ManagerStats {
307 let mut s = self.stats.read().clone();
308 s.total_entities = self.total_entities_atomic.load(Ordering::Relaxed) as usize;
309 s.total_memory_bytes = self.resident_bytes() as usize;
310 s
311 }
312
313 pub fn resident_bytes(&self) -> u64 {
316 self.memory_usage().1
317 }
318
319 pub fn memory_bytes(&self) -> u64 {
322 self.memory_usage().0
323 }
324
325 fn memory_usage(&self) -> (u64, u64) {
329 let consolidation = self.consolidation.read();
330 let growing = self.growing.read();
331 let sealed = self.sealed.read();
332 let mut retired = self.retired.write();
333 let mut payload = 0u64;
334 let mut resident = 0u64;
335 {
336 let mut add = |segment: &GrowingSegment| {
337 payload = payload.saturating_add(segment.memory_bytes());
338 resident = resident.saturating_add(segment.resident_bytes());
339 };
340 for segment in growing.iter().chain(sealed.iter()) {
341 add(&segment.read());
342 }
343 retired.retain(|source| {
344 if let Some(source) = source.upgrade() {
345 add(&source.read());
346 true
347 } else {
348 false
349 }
350 });
351 if let Some(run) = consolidation.as_ref() {
352 add(&run.merged);
353 }
354 }
355 let mut bookkeeping =
356 (retired.capacity() * std::mem::size_of::<Weak<RwLock<GrowingSegment>>>()) as u64;
357 if let Some(run) = consolidation.as_ref() {
358 bookkeeping = bookkeeping
359 .saturating_add(
360 (run.sources.capacity() * std::mem::size_of::<SourceProgress>()) as u64,
361 )
362 .saturating_add(
363 (run.pending_ids.capacity() * std::mem::size_of::<EntityId>()) as u64,
364 );
365 for source in &run.sources {
366 bookkeeping = bookkeeping.saturating_add(
367 (source.copied.capacity() * std::mem::size_of::<EntityId>()) as u64,
368 );
369 }
370 }
371 (
372 payload.saturating_add(bookkeeping),
373 resident.saturating_add(bookkeeping),
374 )
375 }
376
377 pub fn reclaimable_bytes(&self) -> u64 {
379 self.sealed
380 .read()
381 .iter()
382 .map(|segment| segment.read().reclaimable_bytes())
383 .fold(0, u64::saturating_add)
384 }
385
386 pub fn next_entity_id(&self) -> EntityId {
388 EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
389 }
390
391 pub fn next_row_id(&self) -> u64 {
393 self.next_row_id.fetch_add(1, Ordering::SeqCst)
394 }
395
396 pub fn reserve_row_ids(&self, n: u64) -> std::ops::Range<u64> {
400 let start = self.next_row_id.fetch_add(n, Ordering::SeqCst);
401 start..start + n
402 }
403
404 pub fn register_row_id(&self, id: u64) {
407 let candidate = id.saturating_add(1);
408 let mut current = self.next_row_id.load(Ordering::SeqCst);
409 while candidate > current {
410 match self.next_row_id.compare_exchange(
411 current,
412 candidate,
413 Ordering::SeqCst,
414 Ordering::SeqCst,
415 ) {
416 Ok(_) => break,
417 Err(updated) => current = updated,
418 }
419 }
420 }
421
422 fn get_or_create_growing(&self) -> Arc<RwLock<GrowingSegment>> {
429 {
431 let growing = self.growing.read();
432 if let Some(segment) = growing.as_ref() {
433 return Arc::clone(segment);
434 }
435 }
436
437 let mut growing = self.growing.write();
439 if let Some(segment) = growing.as_ref() {
441 return Arc::clone(segment);
442 }
443
444 let id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
445 let segment = GrowingSegment::new(id, &self.collection);
446 let segment_arc = Arc::new(RwLock::new(segment));
447 *growing = Some(Arc::clone(&segment_arc));
448
449 self.emit(LifecycleEvent::SegmentCreated(id));
450
451 self.stats.write().growing_count += 1;
454
455 segment_arc
456 }
457
458 pub fn insert(&self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
460 self.maybe_seal_growing()?;
462
463 let segment_arc = self.get_or_create_growing();
464 let mut segment = segment_arc.write();
465
466 if entity.id.raw() == 0 {
468 entity.id = self.next_entity_id();
469 }
470
471 let entity_id = entity.id;
472 let segment_id = segment.id();
473
474 segment.insert(entity)?;
475
476 self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
478
479 self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
488
489 Ok(entity_id)
490 }
491
492 pub fn insert_batch(
494 &self,
495 entities: Vec<UnifiedEntity>,
496 ) -> Result<Vec<EntityId>, SegmentError> {
497 let mut ids = Vec::with_capacity(entities.len());
498 for entity in entities {
499 ids.push(self.insert(entity)?);
500 }
501 Ok(ids)
502 }
503
504 pub fn bulk_insert(
507 &self,
508 mut entities: Vec<UnifiedEntity>,
509 ) -> Result<Vec<EntityId>, SegmentError> {
510 for entity in &mut entities {
512 if entity.id.raw() == 0 {
513 entity.id = self.next_entity_id();
514 }
515 if let super::entity::EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
516 if *row_id == 0 {
517 *row_id = self.next_row_id();
518 } else {
519 self.register_row_id(*row_id);
520 }
521 }
522 }
523
524 if let Some(first_row) = entities.first() {
527 if let super::entity::EntityData::Row(ref row) = first_row.data {
528 if let Some(ref named) = row.named {
529 let schema = self.get_or_init_schema(named);
530 for entity in &mut entities {
531 if let super::entity::EntityData::Row(ref mut row) = entity.data {
532 if let Some(named) = row.named.take() {
533 let mut cols = Vec::with_capacity(schema.len());
534 for col_name in schema.iter() {
535 cols.push(
536 named
537 .get(col_name)
538 .cloned()
539 .unwrap_or(reddb_types::Value::Null),
540 );
541 }
542 row.columns = cols;
543 row.schema = Some(Arc::clone(&schema));
544 }
545 }
546 }
547 }
548 }
549 }
550
551 let segment_arc = self.get_or_create_growing();
552 let mut segment = segment_arc.write();
553 let segment_id = segment.id();
554
555 let ids = segment.bulk_insert(entities)?;
557
558 self.total_entities_atomic
563 .fetch_add(ids.len() as u64, Ordering::Relaxed);
564
565 Ok(ids)
566 }
567
568 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
570 self.get_with(id, Clone::clone)
571 }
572
573 pub(crate) fn get_with<T>(
576 &self,
577 id: EntityId,
578 visit: impl FnOnce(&UnifiedEntity) -> T,
579 ) -> Option<T> {
580 if let Some(growing_arc) = self.growing.read().as_ref() {
582 let growing = growing_arc.read();
583 if let Some(entity) = growing.get(id) {
584 return Some(visit(entity));
585 }
586 }
587
588 let sealed = self.sealed.read();
590 for segment in sealed.iter() {
591 let seg = segment.read();
592 if let Some(entity) = seg.get(id) {
593 return Some(visit(entity));
594 }
595 }
596
597 None
598 }
599
600 pub(crate) fn graph_node_logical_id(
604 &self,
605 id: EntityId,
606 mut before_segment: impl FnMut() -> bool,
607 ) -> Option<EntityId> {
608 if let Some(growing_arc) = self.growing.read().as_ref() {
609 if !before_segment() {
610 return None;
611 }
612 let growing = growing_arc.read();
613 if let Some(entity) = growing.get(id) {
614 return matches!(entity.kind, EntityKind::GraphNode(_))
615 .then(|| entity.logical_id());
616 }
617 }
618 let sealed = self.sealed.read();
619 for segment in sealed.iter() {
620 if !before_segment() {
621 return None;
622 }
623 let segment = segment.read();
624 if let Some(entity) = segment.get(id) {
625 return matches!(entity.kind, EntityKind::GraphNode(_))
626 .then(|| entity.logical_id());
627 }
628 }
629 None
630 }
631
632 pub(crate) fn visit_graph_index_batches<E>(
637 &self,
638 kind: GraphEntityKind,
639 keys: &[EntityId],
640 mut before_work: impl FnMut() -> bool,
641 mut admit: impl FnMut(usize) -> Result<(), E>,
642 mut consume: impl FnMut(&[EntityId]) -> Result<bool, E>,
643 ) -> Result<bool, E> {
644 use super::segment::{GraphIndexCursor, SCAN_BATCH_SIZE};
645 admit(std::mem::size_of::<[EntityId; SCAN_BATCH_SIZE]>())?;
646 let mut admitted = 0;
647 let sources = loop {
648 let count = {
649 let growing = self.growing.read();
650 let sealed = self.sealed.read();
651 sealed.len() + usize::from(growing.is_some())
652 };
653 let bytes = count.saturating_mul(std::mem::size_of::<(
654 Arc<RwLock<GrowingSegment>>,
655 GraphIndexCursor,
656 )>());
657 admit(bytes.saturating_sub(admitted))?;
658 admitted = admitted.max(bytes);
659 let mut sources = Vec::with_capacity(count);
660 let growing = self.growing.read();
661 let sealed = self.sealed.read();
662 if sealed.len() + usize::from(growing.is_some()) > sources.capacity() {
663 continue;
664 }
665 for segment in growing.iter().chain(sealed.iter()) {
666 if !before_work() {
667 return Ok(false);
668 }
669 let view = segment.read();
670 if !view.may_contain_graph_kind(kind) {
671 continue;
672 }
673 let Some(cursor) = view.graph_index_cursor(kind, keys, &mut before_work) else {
674 return Ok(false);
675 };
676 sources.push((Arc::clone(segment), cursor));
677 }
678 break sources;
679 };
680 let mut ids = [EntityId::new(0); SCAN_BATCH_SIZE];
681 for (source, mut cursor) in sources {
682 while !cursor.finished(keys) {
683 let mut count = 0;
684 {
685 let segment = source.read();
686 if !segment.may_contain_graph_kind(kind) {
687 break;
688 }
689 if !segment.graph_index_batch(
690 kind,
691 keys,
692 &mut cursor,
693 &mut before_work,
694 &mut |id| {
695 ids[count] = id;
696 count += 1;
697 },
698 ) {
699 return Ok(false);
700 }
701 }
702 if !before_work() || !consume(&ids[..count])? {
703 return Ok(false);
704 }
705 }
706 }
707 Ok(true)
708 }
709
710 pub fn get_many(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
715 let mut out: Vec<Option<UnifiedEntity>> = vec![None; ids.len()];
716 let mut remaining: Vec<usize> = (0..ids.len()).collect(); if let Some(growing_arc) = self.growing.read().as_ref() {
721 let growing = if let Some(g) = growing_arc.try_read() {
722 g
723 } else {
724 growing_arc.read()
725 };
726 remaining.retain(|&i| {
727 if let Some(entity) = growing.get(ids[i]) {
728 out[i] = Some(entity.clone());
729 false } else {
731 true }
733 });
734 }
735
736 if remaining.is_empty() {
737 return out;
738 }
739
740 let sealed = self.sealed.read();
742 for segment in sealed.iter() {
743 if remaining.is_empty() {
744 break;
745 }
746 let seg = segment.read();
747 remaining.retain(|&i| {
748 if let Some(entity) = seg.get(ids[i]) {
749 out[i] = Some(entity.clone());
750 false
751 } else {
752 true
753 }
754 });
755 }
756
757 out
758 }
759
760 pub fn for_each_id<F>(&self, ids: &[EntityId], mut f: F)
773 where
774 F: FnMut(usize, &UnifiedEntity),
775 {
776 thread_local! {
785 static REMAINING_SCRATCH: std::cell::Cell<Vec<usize>> =
786 const { std::cell::Cell::new(Vec::new()) };
787 }
788
789 let mut remaining: Vec<usize> = REMAINING_SCRATCH.with(|cell| cell.take());
790 remaining.clear();
791
792 if let Some(growing_arc) = self.growing.read().as_ref() {
793 let growing = if let Some(g) = growing_arc.try_read() {
794 g
795 } else {
796 growing_arc.read()
797 };
798 for (i, id) in ids.iter().enumerate() {
799 if let Some(entity) = growing.get(*id) {
800 f(i, entity);
801 } else {
802 remaining.push(i);
803 }
804 }
805 } else {
806 remaining.reserve(ids.len());
807 remaining.extend(0..ids.len());
808 }
809
810 if !remaining.is_empty() {
811 let sealed = self.sealed.read();
812 for segment in sealed.iter() {
813 if remaining.is_empty() {
814 break;
815 }
816 let seg = segment.read();
817 remaining.retain(|&i| {
818 if let Some(entity) = seg.get(ids[i]) {
819 f(i, entity);
820 false
821 } else {
822 true
823 }
824 });
825 }
826 }
827
828 REMAINING_SCRATCH.with(|cell| cell.set(remaining));
829 }
830
831 fn scan_for_entity(&self, id: EntityId) -> Option<UnifiedEntity> {
833 if let Some(growing_arc) = self.growing.read().as_ref() {
835 let growing = growing_arc.read();
836 if let Some(entity) = growing.get(id) {
837 return Some(entity.clone());
838 }
839 }
840
841 let sealed = self.sealed.read();
843 for segment in sealed.iter() {
844 if let Some(entity) = segment.get(id) {
845 return Some(entity.clone());
846 }
847 }
848
849 None
850 }
851
852 fn find_sealed_segment_arc(&self, id: EntityId) -> Option<Arc<RwLock<GrowingSegment>>> {
853 let sealed = self.sealed.read();
854 sealed
855 .iter()
856 .find(|segment_arc| segment_arc.read().contains(id))
857 .map(Arc::clone)
858 }
859
860 fn rewrite_sealed_entity_into_growing(
861 &self,
862 entity: UnifiedEntity,
863 metadata: Option<&Metadata>,
864 ) -> Result<(), SegmentError> {
865 let entity_id = entity.id;
866 let sealed_arc = self
867 .find_sealed_segment_arc(entity_id)
868 .ok_or(SegmentError::NotFound(entity_id))?;
869
870 let metadata_to_apply = {
871 let mut sealed = sealed_arc.write();
872 let existing_metadata = sealed.get_metadata(entity_id);
873 if !sealed.force_delete(entity_id) {
874 return Err(SegmentError::NotFound(entity_id));
875 }
876 metadata.cloned().or(existing_metadata)
877 };
878
879 let growing_arc = self.get_or_create_growing();
880 let growing_id = {
881 let mut growing = growing_arc.write();
882 growing.insert(entity)?;
883 if let Some(metadata) = metadata_to_apply {
884 growing.set_metadata(entity_id, metadata)?;
885 }
886 growing.id()
887 };
888
889 self.entity_segment.write().insert(entity_id, growing_id);
890 Ok(())
891 }
892
893 pub fn update(&self, entity: UnifiedEntity) -> Result<(), SegmentError> {
895 let entity_id = entity.id;
896 let mut entity = Some(entity);
897
898 if let Some(growing_arc) = self.growing.read().as_ref() {
900 let mut growing = growing_arc.write();
901 if growing.contains(entity_id) && growing.state().is_writable() {
902 return growing.update(entity.take().expect("entity already moved"));
903 }
904 }
905
906 let segment_id = self.entity_segment.read().get(&entity_id).copied();
908 if let Some(seg_id) = segment_id {
909 if let Some(growing_arc) = self.growing.read().as_ref() {
910 let mut growing = growing_arc.write();
911 if growing.id() == seg_id && growing.state().is_writable() {
912 return growing.update(entity.take().expect("entity already moved"));
913 }
914 }
915 }
916
917 if let Some(entity) = entity.take() {
918 return self.rewrite_sealed_entity_into_growing(entity, None);
919 }
920
921 Err(SegmentError::NotFound(entity_id))
922 }
923
924 pub fn update_with_metadata(
927 &self,
928 entity: UnifiedEntity,
929 metadata: Option<&Metadata>,
930 ) -> Result<(), SegmentError> {
931 let entity_id = entity.id;
932 let mut entity = Some(entity);
933
934 if let Some(growing_arc) = self.growing.read().as_ref() {
936 let mut growing = growing_arc.write();
937 if growing.contains(entity_id) && growing.state().is_writable() {
938 growing.update(entity.take().expect("entity already moved"))?;
939 if let Some(metadata) = metadata {
940 growing.set_metadata(entity_id, metadata.clone())?;
941 }
942 return Ok(());
943 }
944 }
945
946 let segment_id = self.entity_segment.read().get(&entity_id).copied();
948 if let Some(seg_id) = segment_id {
949 if let Some(growing_arc) = self.growing.read().as_ref() {
950 let mut growing = growing_arc.write();
951 if growing.id() == seg_id && growing.state().is_writable() {
952 growing.update(entity.take().expect("entity already moved"))?;
953 if let Some(metadata) = metadata {
954 growing.set_metadata(entity_id, metadata.clone())?;
955 }
956 return Ok(());
957 }
958 }
959 }
960
961 if let Some(entity) = entity.take() {
962 return self.rewrite_sealed_entity_into_growing(entity, metadata);
963 }
964
965 Err(SegmentError::NotFound(entity_id))
966 }
967
968 pub fn update_hot(
972 &self,
973 entity: UnifiedEntity,
974 modified_columns: &[String],
975 ) -> Result<(), SegmentError> {
976 let entity_id = entity.id;
977 let mut entity = Some(entity);
978
979 if let Some(growing_arc) = self.growing.read().as_ref() {
980 let mut growing = growing_arc.write();
981 if growing.contains(entity_id) && growing.state().is_writable() {
982 return growing.update_hot(
983 entity.take().expect("entity already moved"),
984 modified_columns,
985 );
986 }
987 }
988
989 let segment_id = self.entity_segment.read().get(&entity_id).copied();
990 if let Some(seg_id) = segment_id {
991 if let Some(growing_arc) = self.growing.read().as_ref() {
992 let mut growing = growing_arc.write();
993 if growing.id() == seg_id && growing.state().is_writable() {
994 return growing.update_hot(
995 entity.take().expect("entity already moved"),
996 modified_columns,
997 );
998 }
999 }
1000 }
1001
1002 if let Some(entity) = entity.take() {
1003 return self.rewrite_sealed_entity_into_growing(entity, None);
1004 }
1005
1006 Err(SegmentError::NotFound(entity_id))
1007 }
1008
1009 pub fn update_hot_with_metadata(
1012 &self,
1013 entity: UnifiedEntity,
1014 modified_columns: &[String],
1015 metadata: Option<&Metadata>,
1016 ) -> Result<(), SegmentError> {
1017 let entity_id = entity.id;
1018 let mut entity = Some(entity);
1019
1020 if let Some(growing_arc) = self.growing.read().as_ref() {
1021 let mut growing = growing_arc.write();
1022 if growing.contains(entity_id) && growing.state().is_writable() {
1023 growing.update_hot(
1024 entity.take().expect("entity already moved"),
1025 modified_columns,
1026 )?;
1027 if let Some(metadata) = metadata {
1028 growing.set_metadata(entity_id, metadata.clone())?;
1029 }
1030 return Ok(());
1031 }
1032 }
1033
1034 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1035 if let Some(seg_id) = segment_id {
1036 if let Some(growing_arc) = self.growing.read().as_ref() {
1037 let mut growing = growing_arc.write();
1038 if growing.id() == seg_id && growing.state().is_writable() {
1039 growing.update_hot(
1040 entity.take().expect("entity already moved"),
1041 modified_columns,
1042 )?;
1043 if let Some(metadata) = metadata {
1044 growing.set_metadata(entity_id, metadata.clone())?;
1045 }
1046 return Ok(());
1047 }
1048 }
1049 }
1050
1051 if let Some(entity) = entity.take() {
1052 return self.rewrite_sealed_entity_into_growing(entity, metadata);
1053 }
1054
1055 Err(SegmentError::NotFound(entity_id))
1056 }
1057
1058 pub fn update_hot_batch_with_metadata<'a, I>(&self, items: I) -> Result<(), SegmentError>
1061 where
1062 I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
1063 {
1064 let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
1065 items.into_iter().collect();
1066 if items.is_empty() {
1067 return Ok(());
1068 }
1069
1070 if let Some(growing_arc) = self.growing.read().as_ref() {
1071 let mut growing = growing_arc.write();
1072 if growing.state().is_writable() {
1073 match growing.update_hot_batch_with_metadata(items.iter().copied()) {
1074 Ok(()) => return Ok(()),
1075 Err(SegmentError::NotFound(_)) => {}
1076 Err(other) => return Err(other),
1077 }
1078 }
1079 }
1080
1081 for (entity, modified_columns, metadata) in items {
1082 self.update_hot_with_metadata(entity.clone(), modified_columns, metadata)?;
1083 }
1084 Ok(())
1085 }
1086
1087 pub fn delete(&self, id: EntityId) -> Result<bool, SegmentError> {
1089 if let Some(growing_arc) = self.growing.read().as_ref() {
1092 let mut growing = growing_arc.write();
1093 if growing.contains(id) && growing.state().is_writable() {
1094 let seg_id = growing.id();
1095 let deleted = growing.delete(id)?;
1096 if deleted {
1097 self.entity_segment.write().remove(&id);
1098 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1099 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1100 }
1101 return Ok(deleted);
1102 }
1103 }
1104
1105 let segment_id = self.entity_segment.read().get(&id).copied();
1108
1109 if let Some(seg_id) = segment_id {
1110 if let Some(growing_arc) = self.growing.read().as_ref() {
1111 let mut growing = growing_arc.write();
1112 if growing.id() == seg_id && growing.state().is_writable() {
1113 let deleted = growing.delete(id)?;
1114 if deleted {
1115 self.entity_segment.write().remove(&id);
1116 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1117 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1118 }
1119 return Ok(deleted);
1120 }
1121 }
1122 }
1123
1124 {
1127 let sealed = self.sealed.read();
1128 for segment_arc in sealed.iter() {
1129 let mut seg = segment_arc.write();
1130 let seg_id = seg.id();
1131 if seg.contains(id) {
1132 let deleted = seg.force_delete(id);
1133 drop(seg);
1134 if deleted {
1135 self.entity_segment.write().remove(&id);
1136 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1137 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1138 }
1139 return Ok(deleted);
1140 }
1141 }
1142 }
1143
1144 Ok(false)
1145 }
1146
1147 pub fn delete_batch(&self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
1148 if ids.is_empty() {
1149 return Ok(Vec::new());
1150 }
1151
1152 let mut deleted_ids = Vec::with_capacity(ids.len());
1153
1154 if let Some(growing_arc) = self.growing.read().as_ref() {
1155 let mut growing = growing_arc.write();
1156 if growing.state().is_writable() {
1157 let seg_id = growing.id();
1158 let deleted = growing.delete_batch(ids)?;
1159 if !deleted.is_empty() {
1160 {
1161 let mut entity_segment = self.entity_segment.write();
1162 for id in &deleted {
1163 entity_segment.remove(id);
1164 }
1165 }
1166 self.total_entities_atomic
1167 .fetch_sub(deleted.len() as u64, Ordering::Relaxed);
1168 for id in &deleted {
1169 self.emit(LifecycleEvent::EntityDeleted(*id, seg_id));
1170 }
1171 deleted_ids.extend(deleted);
1172 }
1173 }
1174 }
1175
1176 if deleted_ids.len() == ids.len() {
1177 return Ok(deleted_ids);
1178 }
1179
1180 let deleted_set: std::collections::HashSet<EntityId> =
1181 deleted_ids.iter().copied().collect();
1182 for &id in ids {
1183 if deleted_set.contains(&id) {
1184 continue;
1185 }
1186 if self.delete(id)? {
1187 deleted_ids.push(id);
1188 }
1189 }
1190
1191 Ok(deleted_ids)
1192 }
1193
1194 pub fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1196 if let Some(growing_arc) = self.growing.read().as_ref() {
1198 let growing = growing_arc.read();
1199 if growing.contains(id) {
1200 return growing.get_metadata(id);
1201 }
1202 }
1203
1204 let segment_id = self.entity_segment.read().get(&id).copied();
1206
1207 if let Some(seg_id) = segment_id {
1208 if let Some(growing_arc) = self.growing.read().as_ref() {
1209 let growing = growing_arc.read();
1210 if growing.id() == seg_id {
1211 return growing.get_metadata(id);
1212 }
1213 }
1214
1215 let sealed = self.sealed.read();
1216 for segment in sealed.iter() {
1217 if segment.id() == seg_id {
1218 return segment.get_metadata(id);
1219 }
1220 }
1221 }
1222
1223 if let Some(segment_arc) = self.find_sealed_segment_arc(id) {
1224 return segment_arc.read().get_metadata(id);
1225 }
1226
1227 None
1228 }
1229
1230 pub fn set_metadata(&self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1232 if let Some(growing_arc) = self.growing.read().as_ref() {
1235 let mut growing = growing_arc.write();
1236 if growing.contains(id) && growing.state().is_writable() {
1237 return growing.set_metadata(id, metadata);
1238 }
1239 }
1240
1241 let segment_id = self.entity_segment.read().get(&id).copied();
1243
1244 if let Some(seg_id) = segment_id {
1245 if let Some(growing_arc) = self.growing.read().as_ref() {
1246 let mut growing = growing_arc.write();
1247 if growing.id() == seg_id && growing.state().is_writable() {
1248 return growing.set_metadata(id, metadata);
1249 }
1250 }
1251 }
1252
1253 if let Some(entity) = self.get(id) {
1254 return self.rewrite_sealed_entity_into_growing(entity, Some(&metadata));
1255 }
1256
1257 Err(SegmentError::NotFound(id))
1258 }
1259
1260 fn maybe_seal_growing(&self) -> Result<(), SegmentError> {
1262 let should_seal = {
1263 let growing_opt = self.growing.read();
1264 if let Some(growing_arc) = growing_opt.as_ref() {
1265 let growing = growing_arc.read();
1266 growing.should_seal(&self.config.segment_config)
1267 || growing.idle_secs() >= self.config.idle_seal_secs
1268 } else {
1269 false
1270 }
1271 };
1272
1273 if should_seal {
1274 self.seal_current()?;
1275 }
1276
1277 Ok(())
1278 }
1279
1280 pub fn seal_current(&self) -> Result<SegmentId, SegmentError> {
1282 let mut growing_slot = self.growing.write();
1285 if let Some(growing_arc) = growing_slot.take() {
1286 let mut growing = growing_arc.write();
1287 let seg_id = growing.id();
1288 let entity_count = growing.stats().entity_count as u64;
1289
1290 growing.seal()?;
1292
1293 drop(growing); self.sealed.write().push(growing_arc);
1299 drop(growing_slot);
1300
1301 self.mark_sealed_pages_visible(entity_count);
1303
1304 {
1306 let mut stats = self.stats.write();
1307 stats.growing_count = stats.growing_count.saturating_sub(1);
1308 stats.sealed_count += 1;
1309 stats.seal_ops += 1;
1310 }
1311
1312 self.emit(LifecycleEvent::SegmentSealed(seg_id));
1313
1314 return Ok(seg_id);
1315 }
1316
1317 Err(SegmentError::InvalidState(SegmentState::Sealed))
1318 }
1319
1320 pub fn force_seal(&self) -> Result<Option<SegmentId>, SegmentError> {
1322 let has_growing = self.growing.read().is_some();
1323 if has_growing {
1324 Ok(Some(self.seal_current()?))
1325 } else {
1326 Ok(None)
1327 }
1328 }
1329
1330 pub fn all_visible_fraction(&self) -> f64 {
1340 const ROWS_PER_PAGE: u32 = 256;
1341 let sealed = self.sealed.read();
1342 if sealed.is_empty() {
1343 return 0.0;
1344 }
1345 let mut total_pages: u64 = 0;
1346 for seg_arc in sealed.iter() {
1347 let seg = seg_arc.read();
1348 let entity_count = seg.stats().entity_count as u64;
1349 let pages = entity_count.div_ceil(ROWS_PER_PAGE as u64);
1350 total_pages += pages;
1351 }
1352 if total_pages == 0 {
1353 return 0.0;
1354 }
1355 let visible = self.visibility_map.all_visible_count();
1356 (visible as f64 / total_pages as f64).min(1.0)
1357 }
1358
1359 fn mark_sealed_pages_visible(&self, seg_entity_count: u64) {
1362 const ROWS_PER_PAGE: u32 = 256;
1363 let existing_visible = self.visibility_map.all_visible_count();
1364 let start_page = existing_visible as u32;
1366 let new_pages = seg_entity_count.div_ceil(ROWS_PER_PAGE as u64);
1367 let end_page = start_page + new_pages as u32;
1368 self.visibility_map.mark_range_visible(start_page, end_page);
1369 }
1370
1371 pub(crate) fn find_unique_key_conflict(
1375 &self,
1376 columns: &[Vec<String>],
1377 key_index: usize,
1378 signatures: &[String],
1379 exclude_id: Option<EntityId>,
1380 reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1381 ) -> Option<EntityId> {
1382 if let Some(growing) = self.growing.read().as_ref() {
1383 if let Some(id) = growing.read().find_unique_key_conflict(
1384 columns,
1385 key_index,
1386 signatures,
1387 exclude_id,
1388 reserves_key,
1389 ) {
1390 return Some(id);
1391 }
1392 }
1393 for segment in self.sealed.read().iter() {
1394 if let Some(id) = segment.read().find_unique_key_conflict(
1395 columns,
1396 key_index,
1397 signatures,
1398 exclude_id,
1399 reserves_key,
1400 ) {
1401 return Some(id);
1402 }
1403 }
1404 None
1405 }
1406
1407 pub fn for_each_entity<F>(&self, callback: F)
1413 where
1414 F: FnMut(&UnifiedEntity) -> bool,
1415 {
1416 self.for_each_entity_in_segments(|_| true, || true, callback);
1417 }
1418
1419 fn for_each_entity_in_segments<F>(
1420 &self,
1421 segment_filter: impl Fn(&GrowingSegment) -> bool,
1422 mut before_segment: impl FnMut() -> bool,
1423 mut callback: F,
1424 ) where
1425 F: FnMut(&UnifiedEntity) -> bool,
1426 {
1427 if let Some(growing_arc) = self.growing.read().as_ref() {
1431 if !before_segment() {
1432 return;
1433 }
1434 let growing = if let Some(g) = growing_arc.try_read() {
1435 g
1436 } else {
1437 growing_arc.read()
1438 };
1439 if segment_filter(&growing) && !growing.for_each_fast(&mut callback) {
1440 return;
1441 }
1442 }
1443
1444 let sealed = self.sealed.read();
1446 for segment_arc in sealed.iter() {
1447 if !before_segment() {
1448 return;
1449 }
1450 let segment = segment_arc.read();
1451 if segment_filter(&segment) && !segment.for_each_fast(&mut callback) {
1452 return;
1453 }
1454 }
1455 }
1456
1457 pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1473 &self,
1474 init: FInit,
1475 fold: FFold,
1476 reduce: FReduce,
1477 ) -> T
1478 where
1479 T: Send,
1480 FInit: Fn() -> T + Send + Sync,
1481 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1482 FReduce: Fn(T, T) -> T + Send + Sync,
1483 {
1484 use rayon::prelude::*;
1485
1486 let mut acc = init();
1489 if let Some(growing_arc) = self.growing.read().as_ref() {
1490 let growing = if let Some(g) = growing_arc.try_read() {
1491 g
1492 } else {
1493 growing_arc.read()
1494 };
1495 growing.for_each_fast(|entity| {
1496 acc = fold(std::mem::replace(&mut acc, init()), entity);
1497 true
1498 });
1499 }
1500
1501 let segments: Vec<_> = {
1505 let sealed = self.sealed.read();
1506 sealed.iter().cloned().collect()
1507 };
1508
1509 if segments.len() <= 1 {
1510 for seg_arc in &segments {
1511 let seg = seg_arc.read();
1512 seg.for_each_fast(|entity| {
1513 acc = fold(std::mem::replace(&mut acc, init()), entity);
1514 true
1515 });
1516 }
1517 return acc;
1518 }
1519
1520 let sealed_acc = segments
1521 .into_par_iter()
1522 .map(|seg_arc| {
1523 let mut local = init();
1524 let seg = seg_arc.read();
1525 seg.for_each_fast(|entity| {
1526 local = fold(std::mem::replace(&mut local, init()), entity);
1527 true
1528 });
1529 local
1530 })
1531 .reduce(&init, &reduce);
1532
1533 reduce(acc, sealed_acc)
1534 }
1535
1536 pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1545 where
1546 F: FnMut(&UnifiedEntity) -> bool,
1547 {
1548 let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1549 }
1550
1551 pub fn for_each_entity_zoned_with_stats<F>(
1552 &self,
1553 zone_preds: &[(&str, ZoneColPred<'_>)],
1554 mut callback: F,
1555 ) -> SegmentScanStats
1556 where
1557 F: FnMut(&UnifiedEntity) -> bool,
1558 {
1559 let mut scan_stats = SegmentScanStats::default();
1560 if let Some(growing_arc) = self.growing.read().as_ref() {
1566 scan_stats.segments_total += 1;
1567 scan_stats.segments_scanned += 1;
1568 let growing = if let Some(g) = growing_arc.try_read() {
1569 g
1570 } else {
1571 growing_arc.read()
1572 };
1573 if !growing.for_each_fast(&mut callback) {
1574 return scan_stats;
1575 }
1576 }
1577
1578 let sealed = self.sealed.read();
1580 for segment_arc in sealed.iter() {
1581 scan_stats.segments_total += 1;
1582 let segment = segment_arc.read();
1583 if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1584 scan_stats.segments_pruned += 1;
1585 continue; }
1587 scan_stats.segments_scanned += 1;
1588 if !segment.for_each_fast(&mut callback) {
1589 return scan_stats;
1590 }
1591 }
1592 scan_stats
1593 }
1594
1595 pub fn query_all_zoned<F>(
1605 &self,
1606 zone_preds: &[(&str, ZoneColPred<'_>)],
1607 filter: F,
1608 ) -> Vec<UnifiedEntity>
1609 where
1610 F: Fn(&UnifiedEntity) -> bool + Sync,
1611 {
1612 self.query_all_zoned_with_stats(zone_preds, filter).0
1613 }
1614
1615 pub fn query_all_zoned_with_stats<F>(
1616 &self,
1617 zone_preds: &[(&str, ZoneColPred<'_>)],
1618 filter: F,
1619 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1620 where
1621 F: Fn(&UnifiedEntity) -> bool + Sync,
1622 {
1623 let mut results = Vec::new();
1624 let mut scan_stats = SegmentScanStats::default();
1625
1626 if let Some(growing_arc) = self.growing.read().as_ref() {
1629 scan_stats.segments_total += 1;
1630 scan_stats.segments_scanned += 1;
1631 let growing = if let Some(g) = growing_arc.try_read() {
1632 g
1633 } else {
1634 growing_arc.read()
1635 };
1636 results.extend(growing.iter().filter(|e| filter(e)).cloned());
1637 }
1638
1639 let sealed = self.sealed.read();
1641 let surviving: Vec<_> = sealed
1643 .iter()
1644 .filter(|seg_arc| {
1645 scan_stats.segments_total += 1;
1646 if zone_preds.is_empty() {
1647 scan_stats.segments_scanned += 1;
1648 return true;
1649 }
1650 let seg = seg_arc.read();
1651 if seg.can_skip_zone_preds(zone_preds) {
1652 scan_stats.segments_pruned += 1;
1653 false
1654 } else {
1655 scan_stats.segments_scanned += 1;
1656 true
1657 }
1658 })
1659 .collect();
1660
1661 let use_parallel = surviving.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1662
1663 if use_parallel {
1664 let filter_ref = &filter;
1665 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1666 surviving
1667 .iter()
1668 .map(|segment| {
1669 s.spawn(move || {
1670 segment
1671 .read()
1672 .iter()
1673 .filter(|e| filter_ref(e))
1674 .cloned()
1675 .collect::<Vec<_>>()
1676 })
1677 })
1678 .collect::<Vec<_>>()
1679 .into_iter()
1680 .map(|handle| handle.join().unwrap_or_default())
1681 .collect()
1682 });
1683 for batch in segment_results {
1684 results.extend(batch);
1685 }
1686 } else {
1687 for segment_arc in surviving {
1688 let seg = segment_arc.read();
1689 results.extend(seg.iter().filter(|e| filter(e)).cloned());
1690 }
1691 }
1692
1693 (results, scan_stats)
1694 }
1695
1696 fn scan_entity_visible(snapshot: Option<&SnapshotContext>, entity: &UnifiedEntity) -> bool {
1702 match snapshot {
1703 Some(ctx) => entity_visible_with_context(Some(ctx), entity),
1704 None => {
1705 !crate::runtime::ai::moderation::entity_moderation_hidden(entity)
1706 && entity.xmax == 0
1707 }
1708 }
1709 }
1710
1711 pub fn scan<F>(&self, snapshot: Option<&SnapshotContext>, filter: F) -> Vec<UnifiedEntity>
1718 where
1719 F: Fn(&UnifiedEntity) -> bool + Sync,
1720 {
1721 self.query_all(|entity| Self::scan_entity_visible(snapshot, entity) && filter(entity))
1722 }
1723
1724 pub(crate) fn scan_graph_kind(
1726 &self,
1727 snapshot: Option<&SnapshotContext>,
1728 kind: GraphEntityKind,
1729 ) -> Vec<UnifiedEntity> {
1730 self.query_all_in_segments(
1731 |segment| segment.may_contain_graph_kind(kind),
1732 |entity| Self::scan_entity_visible(snapshot, entity) && kind.matches(&entity.kind),
1733 )
1734 }
1735
1736 pub(crate) fn scan_graph_candidates_for_each<F>(
1741 &self,
1742 snapshot: Option<&SnapshotContext>,
1743 kind: GraphEntityKind,
1744 before_segment: impl FnMut() -> bool,
1745 mut callback: F,
1746 ) where
1747 F: FnMut(&UnifiedEntity) -> bool,
1748 {
1749 self.for_each_entity_in_segments(
1750 |segment| segment.may_contain_graph_kind(kind),
1751 before_segment,
1752 |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
1753 );
1754 }
1755
1756 pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
1761 where
1762 F: FnMut(&UnifiedEntity) -> bool,
1763 {
1764 self.for_each_entity(|entity| {
1765 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
1766 });
1767 }
1768
1769 pub(crate) fn scan_batches(
1775 &self,
1776 snapshot: Option<&SnapshotContext>,
1777 mut before_work: impl FnMut() -> bool,
1778 mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
1779 ) -> bool {
1780 let segments = {
1781 let growing = self.growing.read();
1782 let sealed = self.sealed.read();
1783 let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
1784 for segment in growing.iter().chain(sealed.iter()) {
1785 if !before_work() {
1786 return false;
1787 }
1788 let cursor = segment.read().scan_cursor();
1789 segments.push((Arc::clone(segment), cursor));
1790 }
1791 segments
1792 };
1793 let mut ids = Vec::new();
1794 let mut batch = Vec::new();
1795 for (segment, mut cursor) in segments {
1796 while !cursor.finished() {
1797 ids.clear();
1798 batch.clear();
1799 let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
1800 ids.reserve_exact(batch_size);
1801 batch.reserve_exact(batch_size);
1802 {
1803 let segment = segment.read();
1804 if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
1805 return false;
1806 }
1807 }
1808 if !before_work() {
1811 return false;
1812 }
1813 self.for_each_id(&ids, |_, entity| {
1814 if Self::scan_entity_visible(snapshot, entity) {
1815 batch.push(entity.clone());
1816 }
1817 });
1818 if !before_work() {
1819 return false;
1820 }
1821 if !consume(&batch) {
1822 return false;
1823 }
1824 }
1825 }
1826 true
1827 }
1828
1829 pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
1831 &self,
1832 snapshot: Option<&SnapshotContext>,
1833 init: FInit,
1834 fold: FFold,
1835 reduce: FReduce,
1836 ) -> T
1837 where
1838 T: Send,
1839 FInit: Fn() -> T + Send + Sync,
1840 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1841 FReduce: Fn(T, T) -> T + Send + Sync,
1842 {
1843 self.fold_entities_parallel(
1844 init,
1845 |acc, entity| {
1846 if Self::scan_entity_visible(snapshot, entity) {
1847 fold(acc, entity)
1848 } else {
1849 acc
1850 }
1851 },
1852 reduce,
1853 )
1854 }
1855
1856 pub fn scan_for_each_zoned_with_stats<F>(
1858 &self,
1859 snapshot: Option<&SnapshotContext>,
1860 zone_preds: &[(&str, ZoneColPred<'_>)],
1861 mut callback: F,
1862 ) -> SegmentScanStats
1863 where
1864 F: FnMut(&UnifiedEntity) -> bool,
1865 {
1866 self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
1867 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
1868 })
1869 }
1870
1871 pub fn scan_zoned_with_stats<F>(
1873 &self,
1874 snapshot: Option<&SnapshotContext>,
1875 zone_preds: &[(&str, ZoneColPred<'_>)],
1876 filter: F,
1877 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1878 where
1879 F: Fn(&UnifiedEntity) -> bool + Sync,
1880 {
1881 self.query_all_zoned_with_stats(zone_preds, |entity| {
1882 Self::scan_entity_visible(snapshot, entity) && filter(entity)
1883 })
1884 }
1885
1886 pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
1889 where
1890 F: Fn(&UnifiedEntity) -> bool + Sync,
1891 {
1892 self.query_all_in_segments(|_| true, filter)
1893 }
1894
1895 fn query_all_in_segments<F>(
1896 &self,
1897 segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
1898 filter: F,
1899 ) -> Vec<UnifiedEntity>
1900 where
1901 F: Fn(&UnifiedEntity) -> bool + Sync,
1902 {
1903 let mut results = Vec::new();
1904
1905 if let Some(growing_arc) = self.growing.read().as_ref() {
1908 let growing = if let Some(g) = growing_arc.try_read() {
1909 g
1910 } else {
1911 growing_arc.read()
1912 };
1913 if segment_filter(&growing) {
1914 results.extend(growing.iter().filter(|e| filter(e)).cloned());
1915 }
1916 }
1917
1918 let sealed = self.sealed.read();
1920 let use_parallel = sealed.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1921 if use_parallel {
1922 let filter_ref = &filter;
1923 let segment_filter_ref = &segment_filter;
1924 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1925 sealed
1926 .iter()
1927 .map(|segment| {
1928 s.spawn(move || {
1929 let segment = segment.read();
1930 if segment_filter_ref(&segment) {
1931 segment
1932 .iter()
1933 .filter(|e| filter_ref(e))
1934 .cloned()
1935 .collect::<Vec<_>>()
1936 } else {
1937 Vec::new()
1938 }
1939 })
1940 })
1941 .collect::<Vec<_>>()
1942 .into_iter()
1943 .map(|handle| handle.join().unwrap_or_default())
1944 .collect()
1945 });
1946 for batch in segment_results {
1947 results.extend(batch);
1948 }
1949 } else {
1950 for segment in sealed.iter() {
1951 let seg = segment.read();
1952 if segment_filter(&seg) {
1953 results.extend(seg.iter().filter(|e| filter(e)).cloned());
1954 }
1955 }
1956 }
1957
1958 results
1959 }
1960
1961 pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
1966 if let Some(growing_arc) = self.growing.read().as_ref() {
1967 let growing = growing_arc.read();
1968 if growing.may_contain_exact_key(key) {
1969 return true;
1970 }
1971 } else {
1972 return true;
1973 }
1974
1975 let sealed = self.sealed.read();
1976 for segment_arc in sealed.iter() {
1977 let segment = segment_arc.read();
1978 if segment.may_contain_exact_key(key) {
1979 return true;
1980 }
1981 }
1982
1983 false
1984 }
1985
1986 pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
1988 let mut results = Vec::new();
1989
1990 if let Some(growing_arc) = self.growing.read().as_ref() {
1992 let growing = growing_arc.read();
1993 results.extend(growing.filter_metadata(filters));
1994 }
1995
1996 let sealed = self.sealed.read();
1998 for segment in sealed.iter() {
1999 results.extend(segment.filter_metadata(filters));
2000 }
2001
2002 results
2003 }
2004
2005 pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2007 let mut results = Vec::new();
2008
2009 if let Some(growing_arc) = self.growing.read().as_ref() {
2011 let growing = growing_arc.read();
2012 for entity in growing.iter_kind(kind) {
2013 results.push(entity.clone());
2014 }
2015 }
2016
2017 let sealed = self.sealed.read();
2022 for segment_arc in sealed.iter() {
2023 let segment = segment_arc.read();
2024 for entity in segment.iter_kind(kind) {
2025 results.push(entity.clone());
2026 }
2027 }
2028
2029 results
2030 }
2031
2032 pub fn count(&self) -> usize {
2034 self.total_entities_atomic.load(Ordering::Relaxed) as usize
2035 }
2036
2037 pub fn segment_ids(&self) -> Vec<SegmentId> {
2039 let mut ids = Vec::new();
2040
2041 if let Some(growing_arc) = self.growing.read().as_ref() {
2042 ids.push(growing_arc.read().id());
2043 }
2044
2045 let sealed = self.sealed.read();
2046 for segment in sealed.iter() {
2047 ids.push(segment.id());
2048 }
2049
2050 ids.extend(self.archived.read().iter().copied());
2051
2052 ids
2053 }
2054
2055 #[inline]
2068 #[allow(clippy::unused_self)]
2069 fn emit(&self, _event: LifecycleEvent) {}
2070
2071 pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2074 std::mem::take(&mut *self.events.write())
2075 }
2076
2077 pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2079 self.maybe_seal_growing()?;
2081
2082 if self.config.enable_consolidation {
2083 self.consolidation_tick();
2084 }
2085
2086 Ok(())
2087 }
2088
2089 fn consolidation_tick(&self) {
2100 let running = self.consolidation.read().is_some();
2103 if !running && !self.start_consolidation() {
2104 return;
2105 }
2106
2107 let mut guard = self.consolidation.write();
2110 let Some(run) = guard.as_mut() else {
2111 return;
2112 };
2113 self.copy_bounded(run);
2114 if run.cursor >= run.sources.len() {
2115 if let Some(run) = guard.take() {
2116 self.finish_consolidation(run);
2117 }
2118 }
2119 }
2120
2121 fn start_consolidation(&self) -> bool {
2125 let sources = self.select_candidates();
2126 self.start_consolidation_from_sources(sources)
2127 }
2128
2129 fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2130 if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2131 return false;
2132 }
2133
2134 let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2135 let merged = GrowingSegment::new(merged_id, &self.collection);
2136
2137 *self.consolidation.write() = Some(Consolidation {
2138 sources: sources
2139 .into_iter()
2140 .map(|id| SourceProgress {
2141 id,
2142 copied: Vec::new(),
2143 tombstones_at_open: None,
2144 })
2145 .collect(),
2146 merged_id,
2147 merged,
2148 cursor: 0,
2149 pending_ids: Vec::new(),
2150 pending_cursor: 0,
2151 entities_copied: 0,
2152 });
2153
2154 self.stats.write().consolidation.runs_started += 1;
2155 true
2156 }
2157
2158 pub fn pressure_consolidation_tick(&self) -> bool {
2163 if !self.config.enable_consolidation {
2164 return false;
2165 }
2166
2167 let running = self.consolidation.read().is_some();
2168 if !running {
2169 let sources = self.dirty_sealed_segments();
2170 if !self.start_consolidation_from_sources(sources) {
2171 return false;
2172 }
2173 }
2174
2175 self.consolidation_tick();
2176 true
2177 }
2178
2179 fn select_candidates(&self) -> Vec<SegmentId> {
2194 let sealed = self.sealed.read();
2195 if sealed.is_empty() {
2196 return Vec::new();
2197 }
2198
2199 if sealed.len() > self.config.max_sealed_segments {
2200 return sealed.iter().map(|segment| segment.read().id()).collect();
2201 }
2202
2203 let mut live = 0usize;
2204 let mut tombstones = 0usize;
2205 let mut held = 0u64;
2206 let mut reclaimable = 0u64;
2207 let mut dirty = Vec::new();
2208
2209 for segment_arc in sealed.iter() {
2210 let segment = segment_arc.read();
2211 live += segment.live_entity_count();
2212 tombstones += segment.tombstone_count();
2213 held += segment.resident_bytes();
2214 reclaimable += segment.reclaimable_bytes();
2215 if segment.tombstone_count() > 0 {
2216 dirty.push(segment.id());
2217 }
2218 }
2219
2220 let total = live + tombstones;
2221 let tombstone_ratio = if total == 0 {
2222 0.0
2223 } else {
2224 tombstones as f64 / total as f64
2225 };
2226 let fragmentation = if held == 0 {
2227 0.0
2228 } else {
2229 reclaimable as f64 / held as f64
2230 };
2231
2232 if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2233 || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2234 {
2235 return dirty;
2236 }
2237
2238 Vec::new()
2239 }
2240
2241 fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2242 self.sealed
2243 .read()
2244 .iter()
2245 .filter_map(|segment_arc| {
2246 let segment = segment_arc.read();
2247 (segment.reclaimable_bytes() > 0).then(|| segment.id())
2248 })
2249 .collect()
2250 }
2251
2252 fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2255 if sources.is_empty() {
2256 return false;
2257 }
2258 let sealed = self.sealed.read();
2259 sealed.iter().any(|segment_arc| {
2260 let segment = segment_arc.read();
2261 sources.contains(&segment.id()) && segment.tombstone_count() > 0
2262 })
2263 }
2264
2265 fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2266 let sealed = self.sealed.read();
2267 sealed
2268 .iter()
2269 .find(|segment| segment.read().id() == id)
2270 .map(Arc::clone)
2271 }
2272
2273 fn copy_bounded(&self, run: &mut Consolidation) {
2278 let mut budget = self.config.consolidation_entities_per_tick;
2279
2280 while budget > 0 && run.cursor < run.sources.len() {
2281 let source_id = run.sources[run.cursor].id;
2282 let Some(source_arc) = self.sealed_segment(source_id) else {
2283 run.cursor += 1;
2287 run.pending_ids.clear();
2288 run.pending_cursor = 0;
2289 continue;
2290 };
2291
2292 if run.sources[run.cursor].tombstones_at_open.is_none() {
2293 let source = source_arc.read();
2294 run.pending_ids = source.live_entity_ids();
2295 run.pending_cursor = 0;
2296 run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2297 }
2298
2299 {
2300 let source = source_arc.read();
2301 while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2302 let id = run.pending_ids[run.pending_cursor];
2303 run.pending_cursor += 1;
2304 budget -= 1;
2305
2306 let Some(entity) = source.get(id) else {
2309 continue;
2310 };
2311 let metadata = source.get_metadata(id);
2312 run.merged.adopt_entity(entity.clone(), metadata);
2313 run.sources[run.cursor].copied.push(id);
2314 run.entities_copied += 1;
2315 }
2316 }
2317
2318 if run.pending_cursor >= run.pending_ids.len() {
2319 run.cursor += 1;
2320 run.pending_ids.clear();
2321 run.pending_cursor = 0;
2322 }
2323 }
2324 }
2325
2326 fn finish_consolidation(&self, mut run: Consolidation) {
2332 let mut sealed = self.sealed.write();
2333
2334 let mut positions = Vec::with_capacity(run.sources.len());
2335 for source in &run.sources {
2336 if let Some(index) = sealed
2337 .iter()
2338 .position(|segment| segment.read().id() == source.id)
2339 {
2340 positions.push(index);
2341 }
2342 }
2343 if positions.is_empty() {
2344 return;
2345 }
2346 positions.sort_unstable();
2347
2348 let mut stale = Vec::new();
2352 for source in &run.sources {
2353 let Some(source_arc) = sealed
2354 .iter()
2355 .find(|segment| segment.read().id() == source.id)
2356 else {
2357 continue;
2358 };
2359 let segment = source_arc.read();
2360 if source.tombstones_at_open == Some(segment.tombstone_count()) {
2361 continue;
2362 }
2363 stale.extend(
2364 source
2365 .copied
2366 .iter()
2367 .copied()
2368 .filter(|id| !segment.contains(*id)),
2369 );
2370 }
2371 for id in stale {
2372 run.merged.evict_entity(id);
2373 }
2374
2375 let reclaimed_bytes: u64 = positions
2376 .iter()
2377 .map(|&index| sealed[index].read().resident_bytes())
2378 .sum();
2379 let reclaimed_tombstones: u64 = positions
2380 .iter()
2381 .map(|&index| sealed[index].read().tombstone_count() as u64)
2382 .sum();
2383
2384 if run.merged.seal().is_err() {
2385 return;
2386 }
2387 let merged_bytes = run.merged.resident_bytes();
2388 let merged_arc = Arc::new(RwLock::new(run.merged));
2389
2390 {
2391 let mut retired = self.retired.write();
2392 retired.retain(|source| source.strong_count() > 0);
2393 for &index in positions.iter().rev() {
2394 let source = sealed.remove(index);
2395 if Arc::strong_count(&source) > 1 {
2396 retired.push(Arc::downgrade(&source));
2397 }
2398 }
2399 }
2400 let insert_at = positions[0].min(sealed.len());
2401 sealed.insert(insert_at, Arc::clone(&merged_arc));
2402 drop(sealed);
2403
2404 let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
2406 {
2407 let mut entity_segment = self.entity_segment.write();
2408 for segment_id in entity_segment.values_mut() {
2409 if source_ids.contains(segment_id) {
2410 *segment_id = run.merged_id;
2411 }
2412 }
2413 }
2414
2415 {
2416 let mut stats = self.stats.write();
2417 stats.sealed_count = stats
2418 .sealed_count
2419 .saturating_sub(positions.len())
2420 .saturating_add(1);
2421 let consolidation = &mut stats.consolidation;
2422 consolidation.runs_completed += 1;
2423 consolidation.segments_merged += positions.len() as u64;
2424 consolidation.tombstones_reclaimed += reclaimed_tombstones;
2425 consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
2426 }
2427
2428 self.emit(LifecycleEvent::SegmentConsolidated {
2429 source: source_ids,
2430 target: run.merged_id,
2431 });
2432 }
2433
2434 #[cfg(test)]
2437 fn consolidation_progress(&self) -> Option<u64> {
2438 self.consolidation
2439 .read()
2440 .as_ref()
2441 .map(|run| run.entities_copied)
2442 }
2443}
2444
2445impl UnifiedSegment for Arc<RwLock<GrowingSegment>> {
2448 fn id(&self) -> SegmentId {
2449 self.read().id()
2450 }
2451
2452 fn state(&self) -> SegmentState {
2453 self.read().state()
2454 }
2455
2456 fn collection(&self) -> &str {
2457 "unknown"
2459 }
2460
2461 fn stats(&self) -> SegmentStats {
2462 self.read().stats()
2463 }
2464
2465 fn entity_count(&self) -> usize {
2466 self.read().entity_count()
2467 }
2468
2469 fn contains(&self, id: EntityId) -> bool {
2470 self.read().contains(id)
2471 }
2472
2473 fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2474 None
2477 }
2478
2479 fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2480 None
2481 }
2482
2483 fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2484 self.write().insert(entity)
2485 }
2486
2487 fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2488 self.write().update(entity)
2489 }
2490
2491 fn update_hot(
2492 &mut self,
2493 entity: UnifiedEntity,
2494 modified_columns: &[String],
2495 ) -> Result<(), SegmentError> {
2496 self.write().update_hot(entity, modified_columns)
2497 }
2498
2499 fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2500 self.write().delete(id)
2501 }
2502
2503 fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2504 self.read().get_metadata(id)
2505 }
2506
2507 fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2508 self.write().set_metadata(id, metadata)
2509 }
2510
2511 fn seal(&mut self) -> Result<(), SegmentError> {
2512 self.write().seal()
2513 }
2514
2515 fn should_seal(&self, config: &SegmentConfig) -> bool {
2516 self.read().should_seal(config)
2517 }
2518
2519 fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2520 Box::new(std::iter::empty())
2522 }
2523
2524 fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2525 Box::new(std::iter::empty())
2526 }
2527
2528 fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2529 self.read().filter_metadata(filters)
2530 }
2531}
2532
2533#[cfg(test)]
2534mod tests {
2535 use std::collections::HashSet;
2536
2537 use super::*;
2538 use crate::runtime::mvcc::SnapshotContext;
2539 use crate::storage::transaction::snapshot::{Snapshot, SnapshotManager};
2540 use crate::storage::unified::entity::{EntityData, EntityKind, RowData};
2541 use reddb_types::Value;
2542
2543 #[test]
2544 fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
2545 let manager = SegmentManager::with_config(
2546 "vectors",
2547 ManagerConfig {
2548 max_sealed_segments: 1,
2549 consolidation_entities_per_tick: 10_000,
2550 ..Default::default()
2551 },
2552 );
2553 for part in 0..3 {
2554 manager
2555 .bulk_insert(
2556 (1..=300)
2557 .map(|offset| {
2558 UnifiedEntity::vector(
2559 EntityId::new(part * 300 + offset),
2560 "vectors",
2561 vec![1.0, 0.0],
2562 )
2563 })
2564 .collect(),
2565 )
2566 .expect("batch");
2567 if part < 2 {
2568 manager.force_seal().expect("seal");
2569 }
2570 }
2571 let original_growing = manager.growing.read().as_ref().expect("growing").clone();
2572 let mut changed = false;
2573 let mut seen = Vec::new();
2574 assert!(manager.scan_batches(
2575 None,
2576 || true,
2577 |batch| {
2578 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
2579 assert!(
2580 manager.growing.try_write().is_some(),
2581 "consumer holds no topology guard"
2582 );
2583 assert!(
2584 original_growing.try_write().is_some(),
2585 "consumer holds no segment guard"
2586 );
2587 for entity in batch {
2588 assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
2589 seen.push(entity.id.raw());
2590 }
2591 if !changed {
2592 changed = true;
2593 manager.force_seal().expect("seal during scan");
2594 manager
2595 .insert(UnifiedEntity::vector(
2596 EntityId::new(5000),
2597 "vectors",
2598 vec![1.0, 0.0],
2599 ))
2600 .expect("new write");
2601 drain_maintenance(&manager);
2602 assert!(manager.stats().consolidation.runs_completed > 0);
2603 assert!(manager
2604 .delete(EntityId::new(1))
2605 .expect("delete after source retirement"));
2606 assert!(manager.get(EntityId::new(1)).is_none());
2607 }
2608 true
2609 }
2610 ));
2611 seen.sort_unstable();
2612 assert_eq!(
2613 seen,
2614 (2..=900).collect::<Vec<_>>(),
2615 "no duplicate, missing or late candidates"
2616 );
2617 let mut after = Vec::new();
2618 assert!(manager.scan_batches(
2619 None,
2620 || true,
2621 |batch| {
2622 after.extend(batch.iter().map(|entity| entity.id.raw()));
2623 true
2624 }
2625 ));
2626 after.sort_unstable();
2627 let mut expected: Vec<_> = (2..=900).collect();
2628 expected.push(5000);
2629 assert_eq!(after, expected, "adopted entities have scan positions");
2630 }
2631
2632 #[test]
2633 fn bounded_scan_applies_snapshot_to_relocated_versions() {
2634 for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
2635 let manager = SegmentManager::with_config(
2636 "vectors",
2637 ManagerConfig {
2638 max_sealed_segments: 1,
2639 ..Default::default()
2640 },
2641 );
2642 for ids in [[1, 2], [3, 4]] {
2643 let entities = ids.map(|id| {
2644 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
2645 entity.set_xmin(3);
2646 entity
2647 });
2648 if bulk {
2649 manager
2650 .bulk_insert(entities.into())
2651 .expect("initial versions");
2652 } else {
2653 for entity in entities {
2654 manager.insert(entity).expect("initial version");
2655 }
2656 }
2657 manager.force_seal().expect("seal source");
2658 }
2659 let snapshot = SnapshotContext {
2660 snapshot: Snapshot {
2661 xid: 10,
2662 in_progress: HashSet::new(),
2663 },
2664 manager: Arc::new(SnapshotManager::new()),
2665 own_xids: HashSet::new(),
2666 requires_index_fallback: false,
2667 serializable_reader: None,
2668 };
2669 let mut changed = false;
2670 let mut seen = Vec::new();
2671 assert!(manager.scan_batches(
2672 Some(&snapshot),
2673 || true,
2674 |batch| {
2675 seen.extend(batch.iter().map(|entity| entity.id.raw()));
2676 if !changed {
2677 changed = true;
2678 if consolidate {
2679 drain_maintenance(&manager);
2680 assert!(manager.stats().consolidation.runs_completed > 0);
2681 }
2682 for (id, xmax) in [(3, 8), (4, 12)] {
2683 let mut entity =
2684 manager.get(EntityId::new(id)).expect("retained version");
2685 entity.set_xmax(xmax);
2686 manager
2687 .update(entity)
2688 .expect("update after source retirement");
2689 }
2690 }
2691 true
2692 }
2693 ));
2694 seen.sort_unstable();
2695 assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
2696 let mut again = Vec::new();
2697 assert!(manager.scan_batches(
2698 Some(&snapshot),
2699 || true,
2700 |batch| {
2701 again.extend(batch.iter().map(|entity| entity.id.raw()));
2702 true
2703 }
2704 ));
2705 again.sort_unstable();
2706 assert_eq!(
2707 again,
2708 vec![1, 2, 4],
2709 "old slots do not duplicate relocated versions"
2710 );
2711 let mut current = Vec::new();
2712 assert!(manager.scan_batches(
2713 None,
2714 || true,
2715 |batch| {
2716 current.extend(batch.iter().map(|entity| entity.id.raw()));
2717 true
2718 }
2719 ));
2720 current.sort_unstable();
2721 assert_eq!(
2722 current,
2723 vec![1, 2],
2724 "current view excludes both deleted versions"
2725 );
2726 }
2727 }
2728
2729 #[test]
2730 fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
2731 let manager = SegmentManager::new("mixed");
2732 let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
2733 manager
2734 .bulk_insert((1..=300).map(vector).collect())
2735 .expect("flat");
2736 manager.insert(vector(500)).expect("hash insert");
2737 manager
2738 .bulk_insert(vec![vector(502), vector(505)])
2739 .expect("gapped bulk");
2740 manager.delete(EntityId::new(2)).expect("flat delete");
2741 manager.delete(EntityId::new(505)).expect("hash delete");
2742 let mut changed = false;
2743 let mut seen = Vec::new();
2744 assert!(manager.scan_batches(
2745 None,
2746 || true,
2747 |batch| {
2748 seen.extend(batch.iter().map(|entity| entity.id.raw()));
2749 if !changed {
2750 changed = true;
2751 manager
2752 .insert(vector(700))
2753 .expect("append after cursor boundary");
2754 manager
2755 .update(UnifiedEntity::graph_node(
2756 EntityId::new(500),
2757 "mixed",
2758 "Node",
2759 HashMap::new(),
2760 ))
2761 .expect("structural update");
2762 }
2763 true
2764 }
2765 ));
2766 seen.sort_unstable();
2767 let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
2768 expected.extend([500, 502]);
2769 assert_eq!(seen, expected);
2770 }
2771
2772 #[test]
2773 fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
2774 let manager = SegmentManager::new("vectors");
2775 let mut entities = Vec::new();
2776 for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
2777 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
2778 entity.set_xmin(xmin);
2779 entity.set_xmax(xmax);
2780 entities.push(entity);
2781 }
2782 manager.bulk_insert(entities).expect("versions");
2783 manager.force_seal().expect("seal");
2784 let snapshot = SnapshotContext {
2785 snapshot: Snapshot {
2786 xid: 10,
2787 in_progress: HashSet::from([4]),
2788 },
2789 manager: Arc::new(SnapshotManager::new()),
2790 own_xids: HashSet::new(),
2791 requires_index_fallback: false,
2792 serializable_reader: None,
2793 };
2794 let mut work = 0;
2795 let mut callbacks = 0;
2796 assert!(!manager.scan_batches(
2797 Some(&snapshot),
2798 || {
2799 work += 1;
2800 work <= 2
2801 },
2802 |_| {
2803 callbacks += 1;
2804 true
2805 }
2806 ));
2807 assert_eq!(callbacks, 0, "a stopped batch is not consumed");
2808 for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
2809 let mut seen = Vec::new();
2810 assert!(manager.scan_batches(
2811 view,
2812 || true,
2813 |batch| {
2814 seen.extend(batch.iter().map(|entity| entity.id.raw()));
2815 true
2816 }
2817 ));
2818 seen.sort_unstable();
2819 assert_eq!(seen, expected);
2820 }
2821 }
2822
2823 #[test]
2824 fn test_manager_basic() {
2825 let manager = SegmentManager::new("test_collection");
2826
2827 let entity = UnifiedEntity::table_row(
2828 manager.next_entity_id(),
2829 "users",
2830 1,
2831 vec![Value::text("Alice".to_string())],
2832 );
2833
2834 let id = manager.insert(entity).unwrap();
2835 assert!(manager.get(id).is_some());
2836 assert_eq!(manager.count(), 1);
2837 }
2838
2839 #[test]
2840 fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
2841 let manager = Arc::new(SegmentManager::new("mixed"));
2842 manager
2843 .bulk_insert(
2844 (1..=128)
2845 .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
2846 .collect(),
2847 )
2848 .expect("vector-only segment");
2849 manager.force_seal().expect("seal vectors");
2850 let mut old =
2851 UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
2852 old.set_xmin(3);
2853 old.set_xmax(12);
2854 let mut new =
2855 UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
2856 new.set_xmin(12);
2857 let edge = UnifiedEntity::graph_edge(
2858 EntityId::new(202),
2859 "mixed",
2860 "a",
2861 "b",
2862 1.0,
2863 Default::default(),
2864 );
2865 manager
2866 .bulk_insert(vec![old, new, edge])
2867 .expect("physical graph versions");
2868 manager.force_seal().expect("seal graph");
2869 manager
2870 .insert(UnifiedEntity::graph_node(
2871 EntityId::new(203),
2872 "mixed",
2873 "person",
2874 Default::default(),
2875 ))
2876 .expect("growing node");
2877 let snapshot = SnapshotContext {
2878 snapshot: Snapshot {
2879 xid: 10,
2880 in_progress: HashSet::new(),
2881 },
2882 manager: Arc::new(SnapshotManager::new()),
2883 own_xids: HashSet::new(),
2884 requires_index_fallback: false,
2885 serializable_reader: None,
2886 };
2887 std::thread::spawn(move || {
2888 for view in [None, Some(&snapshot)] {
2889 for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
2890 let expected: HashSet<_> = manager
2891 .scan(view, |item| kind.matches(&item.kind))
2892 .into_iter()
2893 .map(|item| item.id)
2894 .collect();
2895 let actual: HashSet<_> = manager
2896 .scan_graph_kind(view, kind)
2897 .into_iter()
2898 .map(|item| item.id)
2899 .collect();
2900 assert_eq!(actual, expected);
2901 let mut visited = 0;
2902 let mut candidates = HashSet::new();
2903 manager.scan_graph_candidates_for_each(
2904 view,
2905 kind,
2906 || true,
2907 |item| {
2908 visited += 1;
2909 if kind.matches(&item.kind) {
2910 candidates.insert(item.id);
2911 }
2912 true
2913 },
2914 );
2915 assert_eq!(candidates, expected);
2916 assert_eq!(
2917 visited,
2918 if matches!(kind, GraphEntityKind::Node) {
2919 3
2920 } else {
2921 2
2922 }
2923 );
2924 }
2925 }
2926 let ids: HashSet<_> = manager
2927 .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
2928 .into_iter()
2929 .map(|item| item.id.raw())
2930 .collect();
2931 assert_eq!(ids, HashSet::from([200, 203]));
2932 let ids: HashSet<_> = manager
2933 .scan_graph_kind(None, GraphEntityKind::Node)
2934 .into_iter()
2935 .map(|item| item.id.raw())
2936 .collect();
2937 assert_eq!(ids, HashSet::from([201, 203]));
2938 let mut visits = 0;
2939 manager.scan_graph_candidates_for_each(
2940 None,
2941 GraphEntityKind::Node,
2942 || true,
2943 |_| {
2944 visits += 1;
2945 false
2946 },
2947 );
2948 assert_eq!(
2949 visits, 1,
2950 "early termination must also stop subsequent segments"
2951 );
2952 })
2953 .join()
2954 .expect("snapshot worker");
2955 }
2956
2957 #[test]
2958 fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
2959 let manager = Arc::new(SegmentManager::new("accounts"));
2960 let cases = [
2961 ("legacy row", 0, 0, true),
2962 ("committed creator", 3, 0, true),
2963 ("future creator", 11, 0, false),
2964 ("in-progress creator", 4, 0, false),
2965 ("committed deleter", 3, 7, false),
2966 ("future deleter", 3, 11, true),
2967 ("in-progress deleter", 3, 4, true),
2968 ];
2969
2970 for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
2971 let row_id = index as u64 + 1;
2972 let mut entity = UnifiedEntity::table_row(
2973 EntityId::new(row_id),
2974 "accounts",
2975 row_id,
2976 vec![Value::Integer(row_id as i64)],
2977 );
2978 entity.set_xmin(*xmin);
2979 entity.set_xmax(*xmax);
2980 manager.insert(entity).unwrap();
2981 }
2982
2983 let snapshot = SnapshotContext {
2984 snapshot: Snapshot {
2985 xid: 10,
2986 in_progress: HashSet::from([4]),
2987 },
2988 manager: Arc::new(SnapshotManager::new()),
2989 own_xids: HashSet::new(),
2990 requires_index_fallback: false,
2991 serializable_reader: None,
2992 };
2993
2994 let raw_visible_row_ids: HashSet<u64> = manager
2995 .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
2996 .into_iter()
2997 .filter_map(|entity| match entity.kind {
2998 EntityKind::TableRow { row_id, .. } => Some(row_id),
2999 _ => None,
3000 })
3001 .collect();
3002
3003 let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3004 .join()
3005 .unwrap();
3006 let visible_row_ids: HashSet<u64> = scanned
3007 .into_iter()
3008 .filter_map(|entity| match entity.kind {
3009 EntityKind::TableRow { row_id, .. } => Some(row_id),
3010 _ => None,
3011 })
3012 .collect();
3013 assert_eq!(visible_row_ids, raw_visible_row_ids);
3014
3015 for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3016 let row_id = index as u64 + 1;
3017 assert_eq!(
3018 visible_row_ids.contains(&row_id),
3019 *expected_visible,
3020 "{name} visibility mismatch"
3021 );
3022 }
3023 }
3024
3025 #[test]
3026 fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3027 let manager = SegmentManager::new("accounts");
3031 let mut live =
3032 UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3033 live.set_xmin(2);
3034 live.set_xmax(0);
3035 manager.insert(live).unwrap();
3036
3037 let mut superseded =
3038 UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3039 superseded.set_xmin(2);
3040 superseded.set_xmax(9);
3041 manager.insert(superseded).unwrap();
3042
3043 let rows: Vec<u64> = manager
3044 .scan(None, |_| true)
3045 .into_iter()
3046 .filter_map(|entity| match entity.kind {
3047 EntityKind::TableRow { row_id, .. } => Some(row_id),
3048 _ => None,
3049 })
3050 .collect();
3051 assert_eq!(
3052 rows,
3053 vec![1],
3054 "frameless scan must hide superseded versions"
3055 );
3056 }
3057
3058 #[test]
3059 fn query_scan_callers_use_snapshot_api_for_visibility() {
3060 let table = include_str!("../../runtime/query_exec/table.rs");
3061 let aggregate = include_str!("../../runtime/query_exec/aggregate.rs");
3062
3063 assert_eq!(
3064 table.matches(".for_each_entity(|entity|").count(),
3065 1,
3066 "only the visibility-independent schema inspection may use the raw table iterator"
3067 );
3068 assert!(!table.contains(".query_all_zoned_with_stats("));
3069 assert!(!table.contains(".for_each_entity_zoned_with_stats("));
3070 assert!(!table.contains("TableRowMvccReadResolver::captured"));
3071
3072 assert!(!aggregate.contains("TableRowMvccReadResolver"));
3073 assert!(!aggregate.contains(".for_each_entity("));
3074 assert!(!aggregate.contains(".fold_entities_parallel("));
3075 }
3076
3077 #[test]
3078 fn remaining_visibility_scan_callers_use_snapshot_api() {
3079 let aggregate_pushdown =
3080 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs");
3081 let ask = include_str!("../../runtime/ask_pipeline.rs");
3082 let dml = include_str!("../../runtime/impl_dml.rs");
3083 let dml_targets = include_str!("../../runtime/dml_target_scan.rs");
3084 let events = include_str!("../../runtime/impl_events.rs");
3085 let graph_commands = include_str!("../../runtime/impl_graph_commands.rs");
3086 let graph_dsl = include_str!("../../runtime/graph_dsl.rs");
3087 let graph_tvf = include_str!("../../runtime/graph_tvf.rs");
3088 let kv = include_str!("../../runtime/impl_kv.rs");
3089 let queue = include_str!("../../runtime/impl_queue.rs");
3090 let primary_queue = include_str!("../../runtime/primary_queue_store.rs");
3091 let record_search = include_str!("../../runtime/record_search.rs");
3092 let search = include_str!("../../runtime/impl_search.rs");
3093 let vector = include_str!("../../runtime/query_exec/vector.rs");
3094
3095 assert!(!aggregate_pushdown.contains("TableRowMvccReadResolver"));
3096 assert!(!aggregate_pushdown.contains(".for_each_entity("));
3097 assert!(!ask.contains("for entity in manager.query_all(|_| true)"));
3098 assert!(!dml.contains("let entities = manager.query_all(|_| true);"));
3099 assert_eq!(
3104 dml_targets
3105 .matches("self.candidate_visible(snapshot.as_ref(), entity)")
3106 .count(),
3107 2,
3108 "both raw DML iterations must route through candidate_visible"
3109 );
3110 assert_eq!(
3113 events.matches("manager.query_all(").count(),
3114 1,
3115 "only the visibility-free backfill dedup may use query_all"
3116 );
3117 assert!(!graph_commands.contains("TableRowMvccReadResolver"));
3118 assert!(!graph_dsl.contains("entity_visible_with_context(snap_ctx.as_ref(), e)"));
3119 assert!(!graph_tvf.contains("entity_visible_with_context(snap_ctx.as_ref(), &entity)"));
3120 assert!(!kv.contains("TableRowMvccReadResolver"));
3121 assert!(!queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3122 assert!(!primary_queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3123 assert!(!record_search.contains("TableRowMvccReadResolver"));
3124 assert!(!search.contains("for entity in manager.query_all(|_| true)"));
3125 assert!(!vector.contains("manager.query_all("));
3126 }
3127
3128 fn production_source(src: &str) -> String {
3133 let mut kept: Vec<&str> = Vec::new();
3134 let lines: Vec<&str> = src.lines().collect();
3135 let mut idx = 0;
3136 while idx < lines.len() {
3137 let starts_test_mod = lines[idx].starts_with("#[cfg(test)]")
3138 && lines
3139 .get(idx + 1)
3140 .is_some_and(|next| next.starts_with("mod "));
3141 if !starts_test_mod {
3142 kept.push(lines[idx]);
3143 idx += 1;
3144 continue;
3145 }
3146 idx += 1;
3147 let mut depth: isize = 0;
3148 while idx < lines.len() {
3149 depth += lines[idx].matches('{').count() as isize;
3150 depth -= lines[idx].matches('}').count() as isize;
3151 idx += 1;
3152 if depth <= 0 {
3153 break;
3154 }
3155 }
3156 }
3157 kept.join("\n")
3158 }
3159
3160 fn raw_iteration_sites(src: &str) -> usize {
3162 src.matches("query_all(").count()
3163 + src.matches(".for_each_entity").count()
3164 + src.matches("fold_entities_parallel(").count()
3165 }
3166
3167 #[test]
3168 fn raw_iteration_confined_to_maintenance_allowlist() {
3169 let allowlist: &[(&str, &str, usize, &str)] = &[
3177 (
3178 "runtime/query_exec/table.rs",
3179 include_str!("../../runtime/query_exec/table.rs"),
3180 1,
3181 "documented exception: schema inspection (does the collection ever \
3182 observe these fields?) is visibility-independent",
3183 ),
3184 (
3185 "runtime/query_exec/aggregate.rs",
3186 include_str!("../../runtime/query_exec/aggregate.rs"),
3187 0,
3188 "fully migrated by batch 1",
3189 ),
3190 (
3191 "runtime/query_exec/aggregate_pushdown_dispatch.rs",
3192 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs"),
3193 0,
3194 "fully migrated by batch 1",
3195 ),
3196 (
3197 "runtime/query_exec/vector.rs",
3198 include_str!("../../runtime/query_exec/vector.rs"),
3199 0,
3200 "fully migrated by batch 2",
3201 ),
3202 (
3203 "runtime/ask_pipeline.rs",
3204 include_str!("../../runtime/ask_pipeline.rs"),
3205 0,
3206 "fully migrated by batch 2",
3207 ),
3208 (
3209 "runtime/impl_dml.rs",
3210 include_str!("../../runtime/impl_dml.rs"),
3211 1,
3212 "documented exception: ON CONFLICT resolves a unique key against \
3213 physical versions on purpose. A snapshot scan cannot see a row an \
3214 uncommitted concurrent writer already holds the key for, which is \
3215 exactly the case that must raise a serialization conflict rather \
3216 than insert a duplicate; the same pass reaps index entries left by \
3217 aborted writers. \
3218 TODO(#2263 follow-up): narrow the candidates through the unique \
3219 index instead of walking the collection",
3220 ),
3221 (
3222 "runtime/dml_target_scan.rs",
3223 include_str!("../../runtime/dml_target_scan.rs"),
3224 2,
3225 "documented exception: kind-conditional visibility the uniform scan \
3226 API cannot express; both iterations gate on candidate_visible",
3227 ),
3228 (
3229 "runtime/impl_events.rs",
3230 include_str!("../../runtime/impl_events.rs"),
3231 1,
3232 "documented exception: backfill dedup must include consumed messages, \
3233 so it is deliberately visibility-free",
3234 ),
3235 (
3236 "runtime/impl_graph_commands.rs",
3237 include_str!("../../runtime/impl_graph_commands.rs"),
3238 0,
3239 "native node properties resolve one snapshot-visible, RLS-admitted version",
3240 ),
3241 (
3242 "runtime/graph_dsl.rs",
3243 include_str!("../../runtime/graph_dsl.rs"),
3244 1,
3245 "remaining legacy debt: unused edge-property helper; search expansion \
3246 now uses snapshot visitors and RLS-gated hydration",
3247 ),
3248 (
3249 "runtime/graph_tvf.rs",
3250 include_str!("../../runtime/graph_tvf.rs"),
3251 0,
3252 "fully migrated by batch 2",
3253 ),
3254 (
3255 "runtime/impl_kv.rs",
3256 include_str!("../../runtime/impl_kv.rs"),
3257 4,
3258 "two non-versioned-collection fallbacks (the versioned arms scan under \
3259 the captured snapshot) plus the two vault-version readers, which exist \
3260 to expose every physical version. \
3261 TODO(#2138 follow-up): vault_versions/latest_vault_entries never \
3262 capture a snapshot",
3263 ),
3264 (
3265 "runtime/impl_queue.rs",
3266 include_str!("../../runtime/impl_queue.rs"),
3267 10,
3268 "maintenance: red_queue_meta bookkeeping (config, groups, acks, \
3269 pending, dedup TTL reaper, gauge counts) — queue message bodies are \
3270 read through scan. \
3271 TODO(#2138 follow-up): QUEUE PENDING projects meta rows to clients",
3272 ),
3273 (
3274 "runtime/primary_queue_store.rs",
3275 include_str!("../../runtime/primary_queue_store.rs"),
3276 3,
3277 "maintenance: generic red_queue_meta read/delete helpers and the \
3278 next-position allocator, which must not collide with rows invisible \
3279 to the writer's snapshot",
3280 ),
3281 (
3282 "runtime/record_search.rs",
3283 include_str!("../../runtime/record_search.rs"),
3284 0,
3285 "fully migrated by batch 2",
3286 ),
3287 (
3288 "runtime/search_graph.rs",
3289 include_str!("../../runtime/search_graph.rs"),
3290 0,
3291 "search graph preparation uses snapshot visitors and gated hydration",
3292 ),
3293 (
3294 "runtime/impl_search.rs",
3295 include_str!("../../runtime/impl_search.rs"),
3296 1,
3297 "maintenance: ask-audit retention sweep must see every physical row",
3298 ),
3299 ];
3300
3301 for (path, src, allowed, reason) in allowlist {
3302 let found = raw_iteration_sites(&production_source(src));
3303 assert_eq!(
3304 found, *allowed,
3305 "{path}: {found} raw-iteration sites, allowlist permits {allowed} \
3306 ({reason}). Raw iteration bypasses MVCC visibility — use \
3307 SegmentManager::scan / scan_for_each / scan_fold_parallel / \
3308 scan_zoned_with_stats, or extend the allowlist with a maintenance \
3309 justification."
3310 );
3311 }
3312 }
3313
3314 #[test]
3315 fn explicit_snapshot_scan_ignores_ambient_thread_local() {
3316 let manager = Arc::new(SegmentManager::new("accounts"));
3317 let versions = [(0, 0), (3, 0), (11, 0), (4, 0), (3, 7), (3, 11), (3, 4)];
3319 for (index, (xmin, xmax)) in versions.iter().enumerate() {
3320 let row_id = index as u64 + 1;
3321 let mut entity = UnifiedEntity::table_row(
3322 EntityId::new(row_id),
3323 "accounts",
3324 row_id,
3325 vec![Value::Integer(row_id as i64)],
3326 );
3327 entity.set_xmin(*xmin);
3328 entity.set_xmax(*xmax);
3329 manager.insert(entity).unwrap();
3330 }
3331
3332 let explicit = SnapshotContext {
3333 snapshot: Snapshot {
3334 xid: 10,
3335 in_progress: HashSet::from([4]),
3336 },
3337 manager: Arc::new(SnapshotManager::new()),
3338 own_xids: HashSet::new(),
3339 requires_index_fallback: false,
3340 serializable_reader: None,
3341 };
3342 let decoy = SnapshotContext {
3345 snapshot: Snapshot {
3346 xid: 1,
3347 in_progress: HashSet::new(),
3348 },
3349 manager: Arc::new(SnapshotManager::new()),
3350 own_xids: HashSet::new(),
3351 requires_index_fallback: false,
3352 serializable_reader: None,
3353 };
3354
3355 let row_ids = |entities: Vec<UnifiedEntity>| -> HashSet<u64> {
3356 entities
3357 .into_iter()
3358 .filter_map(|entity| match entity.kind {
3359 EntityKind::TableRow { row_id, .. } => Some(row_id),
3360 _ => None,
3361 })
3362 .collect()
3363 };
3364
3365 let guard = crate::runtime::execution_context::CurrentSnapshotGuard::install(decoy);
3366 let on_installing_thread = row_ids(manager.scan(Some(&explicit), |_| true));
3367 drop(guard);
3368
3369 let spawned_manager = Arc::clone(&manager);
3370 let spawned_snapshot = explicit.clone();
3371 let on_fresh_thread = std::thread::spawn(move || {
3372 row_ids(spawned_manager.scan(Some(&spawned_snapshot), |_| true))
3373 })
3374 .join()
3375 .unwrap();
3376
3377 assert_eq!(
3378 on_installing_thread, on_fresh_thread,
3379 "an explicit snapshot must produce the same visible set on any thread"
3380 );
3381 assert_eq!(on_installing_thread, HashSet::from([1, 2, 6, 7]));
3382
3383 let raw_manager = Arc::clone(&manager);
3386 let raw_count = std::thread::spawn(move || raw_manager.query_all(|_| true).len())
3387 .join()
3388 .unwrap();
3389 assert_eq!(
3390 raw_count,
3391 versions.len(),
3392 "raw iteration must expose every physical version"
3393 );
3394 }
3395
3396 #[test]
3397 fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3398 let manager = SegmentManager::new("test_collection");
3399
3400 let entity = UnifiedEntity::table_row(
3401 manager.next_entity_id(),
3402 "users",
3403 1,
3404 vec![Value::text("Alice".to_string())],
3405 );
3406
3407 let id = manager.insert(entity).unwrap();
3408
3409 assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3410 assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
3411 assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
3412
3413 assert!(manager.delete(id).unwrap());
3414 assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3415 }
3416
3417 #[test]
3418 fn zoned_scan_reports_pruned_segments_and_preserves_results() {
3419 let config = ManagerConfig {
3420 segment_config: SegmentConfig {
3421 max_entities: 2,
3422 ..Default::default()
3423 },
3424 ..Default::default()
3425 };
3426 let manager = SegmentManager::with_config("events", config);
3427
3428 for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
3429 let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
3430 let entity = UnifiedEntity::new(
3431 manager.next_entity_id(),
3432 EntityKind::TableRow {
3433 table: "events".into(),
3434 row_id: (idx + 1) as u64,
3435 },
3436 EntityData::Row(row),
3437 );
3438 manager.insert(entity).unwrap();
3439 }
3440
3441 let full = manager.query_all(|entity| match &entity.data {
3442 EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
3443 _ => false,
3444 });
3445 let probe = Value::Integer(100);
3446 let (pruned, stats) =
3447 manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
3448 match &entity.data {
3449 EntityData::Row(row) => {
3450 matches!(row.get_field("ts"), Some(Value::Integer(100)))
3451 }
3452 _ => false,
3453 }
3454 });
3455
3456 assert_eq!(pruned.len(), full.len());
3457 assert_eq!(pruned[0].id, full[0].id);
3458 assert_eq!(stats.segments_total, 3);
3459 assert_eq!(stats.segments_scanned, 2);
3460 assert_eq!(stats.segments_pruned, 1);
3461 }
3462
3463 #[test]
3464 fn test_manager_auto_seal() {
3465 let config = ManagerConfig {
3466 segment_config: SegmentConfig {
3467 max_entities: 2,
3468 ..Default::default()
3469 },
3470 ..Default::default()
3471 };
3472
3473 let manager = SegmentManager::with_config("test", config);
3474
3475 manager
3477 .insert(UnifiedEntity::vector(
3478 manager.next_entity_id(),
3479 "v",
3480 vec![0.1],
3481 ))
3482 .unwrap();
3483
3484 manager
3486 .insert(UnifiedEntity::vector(
3487 manager.next_entity_id(),
3488 "v",
3489 vec![0.2],
3490 ))
3491 .unwrap();
3492
3493 manager
3495 .insert(UnifiedEntity::vector(
3496 manager.next_entity_id(),
3497 "v",
3498 vec![0.3],
3499 ))
3500 .unwrap();
3501
3502 let stats = manager.stats();
3503 assert_eq!(stats.total_entities, 3);
3504 }
3505
3506 #[test]
3507 fn test_manager_delete() {
3508 let manager = SegmentManager::new("test");
3509
3510 let id = manager
3511 .insert(UnifiedEntity::vector(
3512 manager.next_entity_id(),
3513 "v",
3514 vec![0.1],
3515 ))
3516 .unwrap();
3517
3518 assert!(manager.get(id).is_some());
3519 assert!(manager.delete(id).unwrap());
3520 assert!(manager.get(id).is_none());
3521 }
3522
3523 #[test]
3524 fn test_manager_metadata() {
3525 let manager = SegmentManager::new("test");
3526
3527 let id = manager
3528 .insert(UnifiedEntity::table_row(
3529 manager.next_entity_id(),
3530 "hosts",
3531 1,
3532 vec![Value::text("192.168.1.1".to_string())],
3533 ))
3534 .unwrap();
3535
3536 let mut meta = Metadata::new();
3537 meta.set(
3538 "os",
3539 super::super::metadata::MetadataValue::String("linux".to_string()),
3540 );
3541
3542 manager.set_metadata(id, meta).unwrap();
3543
3544 let retrieved = manager.get_metadata(id).unwrap();
3545 assert!(retrieved.has("os"));
3546 }
3547
3548 #[test]
3549 fn test_manager_query_by_kind() {
3550 let manager = SegmentManager::new("test");
3551
3552 manager
3553 .insert(UnifiedEntity::table_row(
3554 manager.next_entity_id(),
3555 "hosts",
3556 1,
3557 vec![],
3558 ))
3559 .unwrap();
3560
3561 manager
3562 .insert(UnifiedEntity::vector(
3563 manager.next_entity_id(),
3564 "embeddings",
3565 vec![0.1],
3566 ))
3567 .unwrap();
3568
3569 manager
3570 .insert(UnifiedEntity::table_row(
3571 manager.next_entity_id(),
3572 "hosts",
3573 2,
3574 vec![],
3575 ))
3576 .unwrap();
3577
3578 let rows = manager.get_by_kind("table");
3579 assert_eq!(rows.len(), 2);
3580
3581 let vectors = manager.get_by_kind("vector");
3582 assert_eq!(vectors.len(), 1);
3583 }
3584
3585 fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
3592 SegmentManager::with_config(
3593 "test",
3594 ManagerConfig {
3595 max_sealed_segments: 1_000,
3596 consolidation_entities_per_tick: entities_per_tick,
3597 ..Default::default()
3598 },
3599 )
3600 }
3601
3602 fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
3606 let entities: Vec<UnifiedEntity> = (0..count)
3607 .map(|i| {
3608 UnifiedEntity::table_row(
3609 manager.next_entity_id(),
3610 "users",
3611 i as u64,
3612 vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
3613 )
3614 })
3615 .collect();
3616 let ids = manager.bulk_insert(entities).expect("bulk insert");
3617 manager.force_seal().expect("seal");
3618 ids
3619 }
3620
3621 fn live_ids(manager: &SegmentManager) -> Vec<u64> {
3622 let mut ids: Vec<u64> = manager
3623 .query_all(|_| true)
3624 .into_iter()
3625 .map(|entity| entity.id.raw())
3626 .collect();
3627 ids.sort_unstable();
3628 ids
3629 }
3630
3631 fn drain_maintenance(manager: &SegmentManager) -> usize {
3632 let mut ticks = 0;
3633 while manager.consolidation.read().is_some() || ticks == 0 {
3634 manager.run_maintenance().expect("maintenance");
3635 ticks += 1;
3636 assert!(ticks < 1_000, "consolidation failed to converge");
3637 }
3638 ticks
3639 }
3640
3641 #[test]
3642 fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
3643 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3644 let ids = manager
3645 .bulk_insert(
3646 (1..=1024)
3647 .map(|id| {
3648 UnifiedEntity::graph_edge(
3649 EntityId::new(id),
3650 "step",
3651 "2000",
3652 "3000",
3653 1.0,
3654 HashMap::new(),
3655 )
3656 })
3657 .collect(),
3658 )
3659 .expect("edges");
3660 manager.force_seal().expect("seal");
3661 for id in ids.iter().take(300) {
3662 manager.delete(*id).expect("fragment source");
3663 }
3664 let source = Arc::downgrade(&manager.sealed.read()[0]);
3665 let mut seen = Vec::new();
3666 let mut changed = false;
3667 let mut metadata_bytes = 0;
3668 assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
3669 || true,
3670 |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
3671 |batch| {
3672 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3673 assert!(manager.growing.try_write().is_some(), "no growing topology guard");
3674 assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
3675 assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
3676 if !changed {
3677 changed = true;
3678 drain_maintenance(&manager);
3679 assert!(manager.stats().consolidation.runs_completed > 0);
3680 assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
3681 let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
3682 if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
3683 manager.update(updated).expect("update authoritative merged copy");
3684 }
3685 for id in batch {
3686 let entity = manager.get(*id).expect("hydrate current manager");
3687 if id.raw() == 1024 {
3688 assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
3689 }
3690 seen.push(id.raw());
3691 }
3692 Ok(true)
3693 }).expect("cursor"));
3694 assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
3695 assert!(
3696 source.upgrade().is_none(),
3697 "finished cursor releases retired source"
3698 );
3699 assert!(
3700 metadata_bytes < 4096,
3701 "fixed ID buffer plus source handles: {metadata_bytes}"
3702 );
3703 }
3704
3705 #[test]
3706 fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
3707 let manager = SegmentManager::new("graph");
3708 manager
3709 .bulk_insert(
3710 (1..=1024)
3711 .map(|id| {
3712 UnifiedEntity::graph_edge(
3713 EntityId::new(id),
3714 "step",
3715 "2000",
3716 "3000",
3717 1.0,
3718 HashMap::new(),
3719 )
3720 })
3721 .collect(),
3722 )
3723 .expect("edges");
3724 let mut delivered = 0;
3725 let denied = manager.visit_graph_index_batches(
3726 GraphEntityKind::Edge,
3727 &[EntityId::new(2000)],
3728 || true,
3729 |_| Err("out of memory"),
3730 |ids| {
3731 delivered += ids.len();
3732 Ok(true)
3733 },
3734 );
3735 assert_eq!(denied, Err("out of memory"));
3736 assert_eq!(delivered, 0);
3737 let mut work = 0;
3738 assert!(!manager
3739 .visit_graph_index_batches(
3740 GraphEntityKind::Edge,
3741 &[EntityId::new(2000)],
3742 || {
3743 work += 1;
3744 work < 100
3745 },
3746 |_| Ok::<_, ()>(()),
3747 |ids| {
3748 delivered += ids.len();
3749 Ok(true)
3750 }
3751 )
3752 .expect("cancelled cursor"));
3753 assert_eq!(
3754 delivered, 0,
3755 "cancelled partial batch cannot reach consumer"
3756 );
3757 assert!(!manager
3758 .visit_graph_index_batches(
3759 GraphEntityKind::Edge,
3760 &[EntityId::new(2000)],
3761 || true,
3762 |_| Ok::<_, ()>(()),
3763 |ids| {
3764 delivered += ids.len();
3765 Ok(false)
3766 }
3767 )
3768 .expect("consumer stop"));
3769 assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
3770 }
3771
3772 #[test]
3773 fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
3774 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3775 let ids = seal_bulk_segment(&manager, 100);
3776 for id in ids.iter().take(40) {
3777 manager.delete(*id).expect("delete before scan");
3778 }
3779 let source_bytes = manager.resident_bytes();
3780 let source = Arc::downgrade(&manager.sealed.read()[0]);
3781 std::thread::scope(|scope| {
3782 let (ready, waiting) = std::sync::mpsc::channel();
3783 let (release_first, first_wait) = std::sync::mpsc::channel();
3784 let (release_last, last_wait) = std::sync::mpsc::channel();
3785 let reader = |resume: std::sync::mpsc::Receiver<()>,
3786 ready: std::sync::mpsc::Sender<()>| {
3787 assert!(manager.scan_batches(
3788 None,
3789 || true,
3790 |_| {
3791 ready.send(()).expect("signal captured cursor");
3792 let _ = resume.recv();
3793 true
3794 }
3795 ));
3796 };
3797 let ready_first = ready.clone();
3798 let first = scope.spawn(move || reader(first_wait, ready_first));
3799 let last = scope.spawn(move || reader(last_wait, ready));
3800 for _ in 0..2 {
3801 waiting
3802 .recv_timeout(std::time::Duration::from_secs(10))
3803 .expect("reader ready");
3804 }
3805 drain_maintenance(&manager);
3806 assert!(
3807 source.upgrade().is_some(),
3808 "both cursors pin the retired source"
3809 );
3810 let active_bytes = manager.sealed.read()[0].read().resident_bytes();
3811 let held = manager.resident_bytes();
3812 assert!(
3813 held >= active_bytes + source_bytes,
3814 "retired source remains resident"
3815 );
3816 release_first.send(()).expect("release first reader");
3817 first.join().expect("first reader");
3818 assert_eq!(
3819 manager.resident_bytes(),
3820 held,
3821 "one reader still pins the source"
3822 );
3823 release_last.send(()).expect("release last reader");
3824 last.join().expect("last reader");
3825 assert!(
3826 source.upgrade().is_none(),
3827 "accounting must not retain payloads"
3828 );
3829 assert_eq!(held - manager.resident_bytes(), source_bytes);
3830 assert_eq!(
3831 manager.stats().total_memory_bytes as u64,
3832 manager.resident_bytes()
3833 );
3834 });
3835 }
3836
3837 #[test]
3838 fn retained_memory_gates_runtime_growth_until_the_cursor_is_cancelled() {
3839 use crate::storage::memory_pools::MemoryPool;
3840 use crate::{RedDBOptions, RedDBRuntime};
3841
3842 let budget = 128 * 1024;
3843 let runtime =
3844 RedDBRuntime::with_options(RedDBOptions::in_memory().with_memory_budget(budget))
3845 .expect("runtime");
3846 runtime
3847 .db()
3848 .store()
3849 .create_collection("test")
3850 .expect("collection");
3851 let manager = runtime
3852 .db()
3853 .store()
3854 .get_collection("test")
3855 .expect("manager");
3856 let ids = seal_bulk_segment(&manager, 100);
3857 for id in ids.iter().take(40) {
3858 manager.delete(*id).expect("delete before scan");
3859 }
3860 let source_bytes = manager.resident_bytes();
3861 let mut growth = 0;
3862 let mut held_used = 0;
3863 assert!(!manager.scan_batches(
3864 None,
3865 || true,
3866 |_| {
3867 drain_maintenance(&manager);
3868 runtime.refresh_memory_accounting();
3869 held_used = runtime.memory_accounting().total_used_bytes();
3872 growth = budget
3873 .checked_sub(held_used)
3874 .expect("fixture fits while the cursor is open")
3875 + source_bytes / 2;
3876 assert!(growth > 0);
3877 assert!(
3878 runtime
3879 .admit_non_evictable_growth(
3880 MemoryPool::SegmentArena,
3881 "growth with pinned source",
3882 growth,
3883 )
3884 .is_err(),
3885 "retained source must participate in runtime admission"
3886 );
3887 false
3888 }
3889 ));
3890 runtime.refresh_memory_accounting();
3891 assert_eq!(
3892 held_used - runtime.memory_accounting().total_used_bytes(),
3893 source_bytes
3894 );
3895 let _reservation = runtime
3896 .admit_non_evictable_growth(
3897 MemoryPool::SegmentArena,
3898 "growth after cursor cancellation",
3899 growth,
3900 )
3901 .expect("the same growth fits once the cursor releases its source");
3902 }
3903
3904 #[test]
3905 fn retained_memory_includes_an_inflight_consolidation_copy() {
3906 let manager = consolidating_manager(1);
3907 let ids = seal_bulk_segment(&manager, 100);
3908 for id in ids.iter().take(40) {
3909 manager.delete(*id).expect("delete");
3910 }
3911 let before = manager.resident_bytes();
3912 assert!(manager.pressure_consolidation_tick());
3913 let copied = manager
3914 .consolidation
3915 .read()
3916 .as_ref()
3917 .expect("paced merge")
3918 .merged
3919 .resident_bytes();
3920 assert!(copied > 0);
3921 assert!(
3922 manager.resident_bytes() >= before + copied,
3923 "unpublished copy consumes memory"
3924 );
3925 drain_maintenance(&manager);
3926 assert!(
3927 manager.resident_bytes() < before,
3928 "completed merge releases the old storage"
3929 );
3930 }
3931
3932 #[test]
3933 fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
3934 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3935 let ids = seal_bulk_segment(&manager, 100);
3936
3937 let bytes_before_delete = manager.resident_bytes();
3938
3939 for id in ids.iter().take(40) {
3941 assert!(manager.delete(*id).expect("delete"));
3942 }
3943
3944 let bytes_with_tombstones = manager.resident_bytes();
3947 assert!(bytes_with_tombstones > bytes_before_delete);
3948
3949 let survivors = live_ids(&manager);
3950 assert_eq!(survivors.len(), 60);
3951
3952 drain_maintenance(&manager);
3953
3954 let stats = manager.stats();
3955 assert_eq!(stats.consolidation.runs_started, 1);
3956 assert_eq!(stats.consolidation.runs_completed, 1);
3957 assert_eq!(stats.consolidation.segments_merged, 1);
3958 assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
3959 assert!(stats.consolidation.bytes_reclaimed > 0);
3960
3961 let bytes_after = manager.resident_bytes();
3963 assert!(
3964 bytes_after <= bytes_before_delete,
3965 "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
3966 );
3967 assert_eq!(
3968 bytes_with_tombstones - bytes_after,
3969 stats.consolidation.bytes_reclaimed
3970 );
3971 assert_eq!(stats.total_memory_bytes, bytes_after as usize);
3972
3973 assert_eq!(live_ids(&manager), survivors);
3974 assert_eq!(manager.sealed.read().len(), 1);
3975 assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
3976 }
3977
3978 #[test]
3979 fn query_results_are_identical_before_during_and_after_consolidation() {
3980 let manager = consolidating_manager(3);
3981 let ids = seal_bulk_segment(&manager, 20);
3982 seal_bulk_segment(&manager, 20);
3983 for id in ids.iter().take(10) {
3984 assert!(manager.delete(*id).expect("delete"));
3985 }
3986
3987 let before = live_ids(&manager);
3988 let survivor = *ids.last().expect("at least one row");
3989 let tombstoned = ids[0];
3990
3991 let mut mid_tick_observations = 0;
3994 while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
3995 manager.run_maintenance().expect("maintenance");
3996 assert_eq!(live_ids(&manager), before);
3997 assert!(manager.get(survivor).is_some());
3998 assert!(manager.get(tombstoned).is_none());
3999 assert_eq!(manager.get_by_kind("table").len(), before.len());
4000 mid_tick_observations += 1;
4001 assert!(mid_tick_observations < 1_000);
4002 }
4003
4004 assert!(
4005 mid_tick_observations > 2,
4006 "expected the run to span several ticks, saw {mid_tick_observations}"
4007 );
4008 assert_eq!(live_ids(&manager), before);
4009 assert!(manager.get(survivor).is_some());
4010 assert!(manager.get(tombstoned).is_none());
4011 assert_eq!(manager.get_by_kind("table").len(), before.len());
4012 }
4013
4014 #[test]
4015 fn get_by_kind_reaches_sealed_segments() {
4016 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4017 let ids = seal_bulk_segment(&manager, 5);
4018 assert!(manager.delete(ids[0]).expect("delete"));
4019
4020 assert_eq!(manager.get_by_kind("table").len(), 4);
4024 assert_eq!(manager.get_by_kind("vector").len(), 0);
4025 }
4026
4027 #[test]
4028 fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4029 const PER_TICK: usize = 4;
4030 let manager = consolidating_manager(PER_TICK);
4031 let ids = seal_bulk_segment(&manager, 60);
4032 for id in ids.iter().take(30) {
4033 assert!(manager.delete(*id).expect("delete"));
4034 }
4035
4036 manager.run_maintenance().expect("maintenance");
4038 assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4039
4040 let mut previous = PER_TICK as u64;
4043 let mut ticks = 1;
4044 while let Some(copied) = manager.consolidation_progress() {
4045 assert!(
4046 copied - previous <= PER_TICK as u64,
4047 "tick copied {} entities, bound is {PER_TICK}",
4048 copied - previous
4049 );
4050 previous = copied;
4051 manager.run_maintenance().expect("maintenance");
4052 ticks += 1;
4053 assert!(ticks < 100);
4054 }
4055
4056 assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4059 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4060 assert_eq!(live_ids(&manager).len(), 30);
4061 }
4062
4063 #[test]
4064 fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4065 let manager = SegmentManager::with_config(
4066 "test",
4067 ManagerConfig {
4068 max_sealed_segments: 3,
4069 ..Default::default()
4070 },
4071 );
4072 for _ in 0..4 {
4073 seal_bulk_segment(&manager, 5);
4074 }
4075 assert_eq!(manager.sealed.read().len(), 4);
4076
4077 let before = live_ids(&manager);
4078 drain_maintenance(&manager);
4079
4080 assert_eq!(manager.sealed.read().len(), 1);
4083 assert_eq!(manager.stats().consolidation.segments_merged, 4);
4084 assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4085 assert_eq!(live_ids(&manager), before);
4086 }
4087
4088 #[test]
4089 fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4090 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4091 seal_bulk_segment(&manager, 10);
4092 seal_bulk_segment(&manager, 10);
4093
4094 for _ in 0..5 {
4095 manager.run_maintenance().expect("maintenance");
4096 }
4097
4098 assert_eq!(manager.stats().consolidation.runs_started, 0);
4099 assert_eq!(manager.sealed.read().len(), 2);
4100 }
4101
4102 #[test]
4103 fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4104 use std::sync::atomic::AtomicBool;
4105
4106 let manager = Arc::new(consolidating_manager(2));
4107 let ids = seal_bulk_segment(&manager, 40);
4108 seal_bulk_segment(&manager, 40);
4109 for id in ids.iter().take(20) {
4110 assert!(manager.delete(*id).expect("delete"));
4111 }
4112
4113 let expected = live_ids(&manager);
4114 let stop = Arc::new(AtomicBool::new(false));
4115
4116 let readers: Vec<_> = (0..4)
4117 .map(|_| {
4118 let manager = Arc::clone(&manager);
4119 let stop = Arc::clone(&stop);
4120 let expected = expected.clone();
4121 std::thread::spawn(move || {
4122 let mut scans = 0u64;
4123 loop {
4126 let mut seen: Vec<u64> = manager
4127 .query_all(|_| true)
4128 .into_iter()
4129 .map(|entity| entity.id.raw())
4130 .collect();
4131 seen.sort_unstable();
4132 assert_eq!(seen, expected, "reader saw a torn sealed set");
4135 scans += 1;
4136 if stop.load(Ordering::Relaxed) {
4137 break;
4138 }
4139 }
4140 scans
4141 })
4142 })
4143 .collect();
4144
4145 drain_maintenance(&manager);
4146 stop.store(true, Ordering::Relaxed);
4147
4148 for reader in readers {
4149 let scans = reader.join().expect("reader thread");
4150 assert!(scans > 0, "reader never got to scan");
4151 }
4152
4153 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4154 assert_eq!(live_ids(&manager), expected);
4155 }
4156
4157 #[test]
4158 fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4159 let manager = consolidating_manager(4);
4160 let ids = seal_bulk_segment(&manager, 20);
4161 for id in ids.iter().take(6) {
4162 assert!(manager.delete(*id).expect("delete"));
4163 }
4164
4165 manager.run_maintenance().expect("maintenance");
4167 assert_eq!(manager.consolidation_progress(), Some(4));
4168 let doomed = ids[6];
4169 assert!(manager.delete(doomed).expect("delete"));
4170
4171 let expected = live_ids(&manager);
4172 drain_maintenance(&manager);
4173
4174 assert!(manager.get(doomed).is_none(), "deleted row came back");
4175 assert_eq!(live_ids(&manager), expected);
4176 }
4177
4178 #[test]
4179 fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
4180 let manager = consolidating_manager(3);
4181 let ids = seal_bulk_segment(&manager, 30);
4182 for id in ids.iter().take(12) {
4183 assert!(manager.delete(*id).expect("delete"));
4184 }
4185
4186 let before = live_ids(&manager);
4187 let sealed_before: Vec<SegmentId> = manager
4188 .sealed
4189 .read()
4190 .iter()
4191 .map(|segment| segment.read().id())
4192 .collect();
4193
4194 manager.run_maintenance().expect("maintenance");
4195 manager.run_maintenance().expect("maintenance");
4196 assert!(manager.consolidation.read().is_some());
4197
4198 *manager.consolidation.write() = None;
4202
4203 assert_eq!(live_ids(&manager), before);
4204 let sealed_after: Vec<SegmentId> = manager
4205 .sealed
4206 .read()
4207 .iter()
4208 .map(|segment| segment.read().id())
4209 .collect();
4210 assert_eq!(sealed_after, sealed_before);
4211 assert_eq!(manager.stats().consolidation.runs_completed, 0);
4212
4213 drain_maintenance(&manager);
4214 assert_eq!(live_ids(&manager), before);
4215 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4216 }
4217
4218 #[test]
4219 fn consolidation_preserves_entity_payload_metadata_and_sequence() {
4220 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4221
4222 let entities: Vec<UnifiedEntity> = (0..10)
4226 .map(|i| {
4227 UnifiedEntity::table_row(
4228 manager.next_entity_id(),
4229 "users",
4230 i as u64,
4231 vec![Value::Integer(i as i64)],
4232 )
4233 })
4234 .collect();
4235 let ids = manager.bulk_insert(entities).expect("bulk insert");
4236
4237 let mut meta = Metadata::new();
4238 meta.set(
4239 "os",
4240 super::super::metadata::MetadataValue::String("linux".to_string()),
4241 );
4242 manager.set_metadata(ids[9], meta).expect("set metadata");
4243 manager.force_seal().expect("seal");
4244
4245 assert!(manager.delete(ids[0]).expect("delete"));
4246 assert!(manager.delete(ids[1]).expect("delete"));
4247 assert!(manager.delete(ids[2]).expect("delete"));
4248
4249 let before: Vec<UnifiedEntity> = {
4250 let mut entities = manager.query_all(|_| true);
4251 entities.sort_by_key(|entity| entity.id.raw());
4252 entities
4253 };
4254
4255 drain_maintenance(&manager);
4256
4257 let after: Vec<UnifiedEntity> = {
4258 let mut entities = manager.query_all(|_| true);
4259 entities.sort_by_key(|entity| entity.id.raw());
4260 entities
4261 };
4262
4263 let columns = |entity: &UnifiedEntity| match &entity.data {
4264 super::super::entity::EntityData::Row(row) => row.columns.clone(),
4265 _ => Vec::new(),
4266 };
4267
4268 assert_eq!(before.len(), after.len());
4269 for (old, new) in before.iter().zip(after.iter()) {
4270 assert_eq!(old.id, new.id);
4271 assert_eq!(columns(old), columns(new));
4272 assert_eq!(
4273 old.sequence_id, new.sequence_id,
4274 "merge must not re-issue sequence ids"
4275 );
4276 }
4277 assert!(manager
4278 .get_metadata(ids[9])
4279 .expect("metadata survives the merge")
4280 .has("os"));
4281 }
4282
4283 #[test]
4284 #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
4285 fn test_lifecycle_events() {
4286 let manager = SegmentManager::new("test");
4287
4288 manager
4289 .insert(UnifiedEntity::vector(
4290 manager.next_entity_id(),
4291 "v",
4292 vec![0.1],
4293 ))
4294 .unwrap();
4295
4296 let events = manager.drain_events();
4297
4298 assert!(events
4300 .iter()
4301 .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
4302 assert!(events
4303 .iter()
4304 .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
4305 }
4306}