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, SegmentScanCursor,
26 SegmentState, 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
182struct PhysicalRootCacheEntry {
183 segments: Vec<(u64, u64)>,
184 seed: u64,
185 root: u64,
186}
187
188pub(crate) struct PhysicalScanCursor {
191 sources: Vec<(Weak<RwLock<GrowingSegment>>, SegmentScanCursor)>,
192 source: usize,
193}
194
195impl PhysicalScanCursor {
196 pub(crate) fn finished(&self) -> bool {
197 self.source == self.sources.len()
198 }
199}
200
201pub struct SegmentManager {
203 row_constraint_lock: Arc<parking_lot::ReentrantMutex<()>>,
206 collection: String,
208 config: ManagerConfig,
210 next_segment_id: AtomicU64,
212 next_entity_id: AtomicU64,
214 next_row_id: AtomicU64,
216 total_entities_atomic: AtomicU64,
220 growing: RwLock<Option<Arc<RwLock<GrowingSegment>>>>,
222 sealed: RwLock<Vec<Arc<RwLock<GrowingSegment>>>>,
224 retired: RwLock<Vec<Weak<RwLock<GrowingSegment>>>>,
226 archived: RwLock<Vec<SegmentId>>,
228 entity_segment: RwLock<HashMap<EntityId, SegmentId>>,
232 column_schema: RwLock<Option<Arc<Vec<String>>>>,
235 stats: RwLock<ManagerStats>,
237 consolidation: RwLock<Option<Consolidation>>,
240 events: RwLock<Vec<LifecycleEvent>>,
242 visibility_map: VisibilityMap,
246 physical_root_cache: parking_lot::Mutex<Option<PhysicalRootCacheEntry>>,
247}
248
249impl SegmentManager {
250 pub fn new(collection: impl Into<String>) -> Self {
252 Self::with_config(collection, ManagerConfig::default())
253 }
254
255 pub fn with_config(collection: impl Into<String>, config: ManagerConfig) -> Self {
257 Self {
258 row_constraint_lock: Arc::new(parking_lot::ReentrantMutex::new(())),
259 physical_root_cache: parking_lot::Mutex::new(None),
260 collection: collection.into(),
261 config,
262 next_segment_id: AtomicU64::new(1),
263 next_entity_id: AtomicU64::new(1),
264 next_row_id: AtomicU64::new(1),
265 total_entities_atomic: AtomicU64::new(0),
266 growing: RwLock::new(None),
267 sealed: RwLock::new(Vec::new()),
268 retired: RwLock::new(Vec::new()),
269 archived: RwLock::new(Vec::new()),
270 entity_segment: RwLock::new(HashMap::new()),
271 column_schema: RwLock::new(None),
272 stats: RwLock::new(ManagerStats::default()),
273 consolidation: RwLock::new(None),
274 events: RwLock::new(Vec::new()),
275 visibility_map: VisibilityMap::new(),
276 }
277 }
278
279 pub(crate) fn row_constraint_lock(&self) -> Arc<parking_lot::ReentrantMutex<()>> {
280 Arc::clone(&self.row_constraint_lock)
281 }
282
283 pub fn get_or_init_schema(
285 &self,
286 named: &HashMap<String, reddb_types::Value>,
287 ) -> Arc<Vec<String>> {
288 {
289 let schema = self.column_schema.read();
290 if let Some(ref s) = *schema {
291 return Arc::clone(s);
292 }
293 }
294 let cols: Vec<String> = named.keys().cloned().collect();
295 let arc = Arc::new(cols);
296 *self.column_schema.write() = Some(Arc::clone(&arc));
297 arc
298 }
299
300 pub fn column_schema(&self) -> Option<Arc<Vec<String>>> {
302 self.column_schema.read().clone()
303 }
304
305 fn restore_owned_row_schemas<'a>(&self, entities: impl Iterator<Item = &'a mut UnifiedEntity>) {
309 let schema = self.column_schema();
310 for entity in entities {
311 if let super::entity::EntityData::Row(row) = &mut entity.data {
312 if row.named.is_none() && row.schema.is_none() {
313 row.schema = schema.clone();
314 }
315 }
316 }
317 }
318
319 pub(crate) fn set_column_schema_if_empty(&self, columns: Vec<String>) {
320 if columns.is_empty() {
321 return;
322 }
323 let mut schema = self.column_schema.write();
324 if schema.is_none() {
325 *schema = Some(Arc::new(columns));
326 }
327 }
328
329 pub fn collection(&self) -> &str {
331 &self.collection
332 }
333
334 pub fn config(&self) -> &ManagerConfig {
336 &self.config
337 }
338
339 pub fn stats(&self) -> ManagerStats {
342 let mut s = self.stats.read().clone();
343 s.total_entities = self.total_entities_atomic.load(Ordering::Relaxed) as usize;
344 s.total_memory_bytes = self.resident_bytes() as usize;
345 s
346 }
347
348 pub fn resident_bytes(&self) -> u64 {
351 self.memory_usage().1
352 }
353
354 pub fn memory_bytes(&self) -> u64 {
357 self.memory_usage().0
358 }
359
360 fn memory_usage(&self) -> (u64, u64) {
364 let consolidation = self.consolidation.read();
365 let growing = self.growing.read();
366 let sealed = self.sealed.read();
367 let mut retired = self.retired.write();
368 let mut payload = 0u64;
369 let mut resident = 0u64;
370 {
371 let mut add = |segment: &GrowingSegment| {
372 payload = payload.saturating_add(segment.memory_bytes());
373 resident = resident.saturating_add(segment.resident_bytes());
374 };
375 for segment in growing.iter().chain(sealed.iter()) {
376 add(&segment.read());
377 }
378 retired.retain(|source| {
379 if let Some(source) = source.upgrade() {
380 add(&source.read());
381 true
382 } else {
383 false
384 }
385 });
386 if let Some(run) = consolidation.as_ref() {
387 add(&run.merged);
388 }
389 }
390 let mut bookkeeping =
391 (retired.capacity() * std::mem::size_of::<Weak<RwLock<GrowingSegment>>>()) as u64;
392 if let Some(run) = consolidation.as_ref() {
393 bookkeeping = bookkeeping
394 .saturating_add(
395 (run.sources.capacity() * std::mem::size_of::<SourceProgress>()) as u64,
396 )
397 .saturating_add(
398 (run.pending_ids.capacity() * std::mem::size_of::<EntityId>()) as u64,
399 );
400 for source in &run.sources {
401 bookkeeping = bookkeeping.saturating_add(
402 (source.copied.capacity() * std::mem::size_of::<EntityId>()) as u64,
403 );
404 }
405 }
406 (
407 payload.saturating_add(bookkeeping),
408 resident.saturating_add(bookkeeping),
409 )
410 }
411
412 pub fn reclaimable_bytes(&self) -> u64 {
414 self.sealed
415 .read()
416 .iter()
417 .map(|segment| segment.read().reclaimable_bytes())
418 .fold(0, u64::saturating_add)
419 }
420
421 pub fn next_entity_id(&self) -> EntityId {
423 EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
424 }
425
426 pub fn next_row_id(&self) -> u64 {
428 self.next_row_id.fetch_add(1, Ordering::SeqCst)
429 }
430
431 pub fn reserve_row_ids(&self, n: u64) -> std::ops::Range<u64> {
435 let start = self.next_row_id.fetch_add(n, Ordering::SeqCst);
436 start..start + n
437 }
438
439 pub fn register_row_id(&self, id: u64) {
442 let candidate = id.saturating_add(1);
443 let mut current = self.next_row_id.load(Ordering::SeqCst);
444 while candidate > current {
445 match self.next_row_id.compare_exchange(
446 current,
447 candidate,
448 Ordering::SeqCst,
449 Ordering::SeqCst,
450 ) {
451 Ok(_) => break,
452 Err(updated) => current = updated,
453 }
454 }
455 }
456
457 fn get_or_create_growing(&self) -> Arc<RwLock<GrowingSegment>> {
464 {
466 let growing = self.growing.read();
467 if let Some(segment) = growing.as_ref() {
468 return Arc::clone(segment);
469 }
470 }
471
472 let mut growing = self.growing.write();
474 if let Some(segment) = growing.as_ref() {
476 return Arc::clone(segment);
477 }
478
479 let id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
480 let segment = GrowingSegment::new(id, &self.collection);
481 let segment_arc = Arc::new(RwLock::new(segment));
482 *growing = Some(Arc::clone(&segment_arc));
483
484 self.emit(LifecycleEvent::SegmentCreated(id));
485
486 self.stats.write().growing_count += 1;
489
490 segment_arc
491 }
492
493 pub fn insert(&self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
495 self.maybe_seal_growing()?;
497
498 let segment_arc = self.get_or_create_growing();
499 let mut segment = segment_arc.write();
500
501 if entity.id.raw() == 0 {
503 entity.id = self.next_entity_id();
504 }
505
506 let entity_id = entity.id;
507 let segment_id = segment.id();
508
509 segment.insert(entity)?;
510
511 self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
513
514 self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
523
524 Ok(entity_id)
525 }
526
527 pub(crate) fn restore_snapshot_entity(
529 &self,
530 entity: UnifiedEntity,
531 ) -> Result<EntityId, SegmentError> {
532 self.maybe_seal_growing()?;
533 if self.get(entity.id).is_some() {
534 return Err(SegmentError::AlreadyExists(entity.id));
535 }
536 let segment_arc = self.get_or_create_growing();
537 let mut segment = segment_arc.write();
538 let entity_id = entity.id;
539 let segment_id = segment.id();
540 segment.adopt_entity(entity, None);
543 self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
544 self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
545 Ok(entity_id)
546 }
547
548 pub fn insert_batch(
550 &self,
551 entities: Vec<UnifiedEntity>,
552 ) -> Result<Vec<EntityId>, SegmentError> {
553 let mut ids = Vec::with_capacity(entities.len());
554 for entity in entities {
555 ids.push(self.insert(entity)?);
556 }
557 Ok(ids)
558 }
559
560 pub fn bulk_insert(
563 &self,
564 mut entities: Vec<UnifiedEntity>,
565 ) -> Result<Vec<EntityId>, SegmentError> {
566 for entity in &mut entities {
568 if entity.id.raw() == 0 {
569 entity.id = self.next_entity_id();
570 }
571 if let super::entity::EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
572 if *row_id == 0 {
573 *row_id = self.next_row_id();
574 } else {
575 self.register_row_id(*row_id);
576 }
577 }
578 }
579
580 if let Some(first_row) = entities.first() {
583 if let super::entity::EntityData::Row(ref row) = first_row.data {
584 if let Some(ref named) = row.named {
585 let schema = self.get_or_init_schema(named);
586 for entity in &mut entities {
587 if let super::entity::EntityData::Row(ref mut row) = entity.data {
588 if let Some(named) = row.named.take() {
589 let mut cols = Vec::with_capacity(schema.len());
590 for col_name in schema.iter() {
591 cols.push(
592 named
593 .get(col_name)
594 .cloned()
595 .unwrap_or(reddb_types::Value::Null),
596 );
597 }
598 row.columns = cols;
599 row.schema = Some(Arc::clone(&schema));
600 }
601 }
602 }
603 }
604 }
605 }
606
607 let segment_arc = self.get_or_create_growing();
608 let mut segment = segment_arc.write();
609 let segment_id = segment.id();
610
611 let ids = segment.bulk_insert(entities)?;
613
614 self.total_entities_atomic
619 .fetch_add(ids.len() as u64, Ordering::Relaxed);
620
621 Ok(ids)
622 }
623
624 pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
626 let mut entity = self.get_with(id, Clone::clone)?;
627 self.restore_owned_row_schemas(std::iter::once(&mut entity));
628 Some(entity)
629 }
630
631 pub(crate) fn get_with<T>(
634 &self,
635 id: EntityId,
636 visit: impl FnOnce(&UnifiedEntity) -> T,
637 ) -> Option<T> {
638 if let Some(growing_arc) = self.growing.read().as_ref() {
640 let growing = growing_arc.read();
641 if let Some(entity) = growing.get(id) {
642 return Some(visit(entity));
643 }
644 }
645
646 let sealed = self.sealed.read();
648 for segment in sealed.iter() {
649 let seg = segment.read();
650 if let Some(entity) = seg.get(id) {
651 return Some(visit(entity));
652 }
653 }
654
655 None
656 }
657
658 pub(crate) fn graph_node_logical_id(
662 &self,
663 id: EntityId,
664 mut before_segment: impl FnMut() -> bool,
665 ) -> Option<EntityId> {
666 if let Some(growing_arc) = self.growing.read().as_ref() {
667 if !before_segment() {
668 return None;
669 }
670 let growing = growing_arc.read();
671 if let Some(entity) = growing.get(id) {
672 return matches!(entity.kind, EntityKind::GraphNode(_))
673 .then(|| entity.logical_id());
674 }
675 }
676 let sealed = self.sealed.read();
677 for segment in sealed.iter() {
678 if !before_segment() {
679 return None;
680 }
681 let segment = segment.read();
682 if let Some(entity) = segment.get(id) {
683 return matches!(entity.kind, EntityKind::GraphNode(_))
684 .then(|| entity.logical_id());
685 }
686 }
687 None
688 }
689
690 pub(crate) fn visit_graph_index_batches<E>(
695 &self,
696 kind: GraphEntityKind,
697 keys: &[EntityId],
698 mut before_work: impl FnMut() -> bool,
699 mut admit: impl FnMut(usize) -> Result<(), E>,
700 mut consume: impl FnMut(&[EntityId]) -> Result<bool, E>,
701 ) -> Result<bool, E> {
702 use super::segment::{GraphIndexCursor, SCAN_BATCH_SIZE};
703 admit(std::mem::size_of::<[EntityId; SCAN_BATCH_SIZE]>())?;
704 let mut admitted = 0;
705 let sources = loop {
706 let count = {
707 let growing = self.growing.read();
708 let sealed = self.sealed.read();
709 sealed.len() + usize::from(growing.is_some())
710 };
711 let bytes = count.saturating_mul(std::mem::size_of::<(
712 Arc<RwLock<GrowingSegment>>,
713 GraphIndexCursor,
714 )>());
715 admit(bytes.saturating_sub(admitted))?;
716 admitted = admitted.max(bytes);
717 let mut sources = Vec::with_capacity(count);
718 let growing = self.growing.read();
719 let sealed = self.sealed.read();
720 if sealed.len() + usize::from(growing.is_some()) > sources.capacity() {
721 continue;
722 }
723 for segment in growing.iter().chain(sealed.iter()) {
724 if !before_work() {
725 return Ok(false);
726 }
727 let view = segment.read();
728 if !view.may_contain_graph_kind(kind) {
729 continue;
730 }
731 let Some(cursor) = view.graph_index_cursor(kind, keys, &mut before_work) else {
732 return Ok(false);
733 };
734 sources.push((Arc::clone(segment), cursor));
735 }
736 break sources;
737 };
738 let mut ids = [EntityId::new(0); SCAN_BATCH_SIZE];
739 for (source, mut cursor) in sources {
740 while !cursor.finished(keys) {
741 let mut count = 0;
742 {
743 let segment = source.read();
744 if !segment.may_contain_graph_kind(kind) {
745 break;
746 }
747 if !segment.graph_index_batch(
748 kind,
749 keys,
750 &mut cursor,
751 &mut before_work,
752 &mut |id| {
753 ids[count] = id;
754 count += 1;
755 },
756 ) {
757 return Ok(false);
758 }
759 }
760 if !before_work() || !consume(&ids[..count])? {
761 return Ok(false);
762 }
763 }
764 }
765 Ok(true)
766 }
767
768 pub fn get_many(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
773 let schema = self.column_schema();
774 let clone_row = |entity: &UnifiedEntity| {
775 let mut owned = entity.clone();
776 if let super::entity::EntityData::Row(row) = &mut owned.data {
777 if row.schema.is_none() {
778 row.schema = schema.clone();
779 }
780 }
781 owned
782 };
783 let mut out: Vec<Option<UnifiedEntity>> = vec![None; ids.len()];
784 let mut remaining: Vec<usize> = (0..ids.len()).collect(); if let Some(growing_arc) = self.growing.read().as_ref() {
789 let growing = if let Some(g) = growing_arc.try_read() {
790 g
791 } else {
792 growing_arc.read()
793 };
794 remaining.retain(|&i| {
795 if let Some(entity) = growing.get(ids[i]) {
796 out[i] = Some(clone_row(entity));
797 false } else {
799 true }
801 });
802 }
803
804 if remaining.is_empty() {
805 self.restore_owned_row_schemas(out.iter_mut().flatten());
806 return out;
807 }
808
809 let sealed = self.sealed.read();
811 for segment in sealed.iter() {
812 if remaining.is_empty() {
813 break;
814 }
815 let seg = segment.read();
816 remaining.retain(|&i| {
817 if let Some(entity) = seg.get(ids[i]) {
818 out[i] = Some(clone_row(entity));
819 false
820 } else {
821 true
822 }
823 });
824 }
825
826 drop(sealed);
827 self.restore_owned_row_schemas(out.iter_mut().flatten());
828 out
829 }
830
831 pub fn for_each_id<F>(&self, ids: &[EntityId], mut f: F)
844 where
845 F: FnMut(usize, &UnifiedEntity),
846 {
847 thread_local! {
856 static REMAINING_SCRATCH: std::cell::Cell<Vec<usize>> =
857 const { std::cell::Cell::new(Vec::new()) };
858 }
859
860 let mut remaining: Vec<usize> = REMAINING_SCRATCH.with(|cell| cell.take());
861 remaining.clear();
862
863 if let Some(growing_arc) = self.growing.read().as_ref() {
864 let growing = if let Some(g) = growing_arc.try_read() {
865 g
866 } else {
867 growing_arc.read()
868 };
869 for (i, id) in ids.iter().enumerate() {
870 if let Some(entity) = growing.get(*id) {
871 f(i, entity);
872 } else {
873 remaining.push(i);
874 }
875 }
876 } else {
877 remaining.reserve(ids.len());
878 remaining.extend(0..ids.len());
879 }
880
881 if !remaining.is_empty() {
882 let sealed = self.sealed.read();
883 for segment in sealed.iter() {
884 if remaining.is_empty() {
885 break;
886 }
887 let seg = segment.read();
888 remaining.retain(|&i| {
889 if let Some(entity) = seg.get(ids[i]) {
890 f(i, entity);
891 false
892 } else {
893 true
894 }
895 });
896 }
897 }
898
899 REMAINING_SCRATCH.with(|cell| cell.set(remaining));
900 }
901
902 fn scan_for_entity(&self, id: EntityId) -> Option<UnifiedEntity> {
904 if let Some(growing_arc) = self.growing.read().as_ref() {
906 let growing = growing_arc.read();
907 if let Some(entity) = growing.get(id) {
908 return Some(entity.clone());
909 }
910 }
911
912 let sealed = self.sealed.read();
914 for segment in sealed.iter() {
915 if let Some(entity) = segment.get(id) {
916 return Some(entity.clone());
917 }
918 }
919
920 None
921 }
922
923 fn find_sealed_segment_arc(&self, id: EntityId) -> Option<Arc<RwLock<GrowingSegment>>> {
924 let sealed = self.sealed.read();
925 sealed
926 .iter()
927 .find(|segment_arc| segment_arc.read().contains(id))
928 .map(Arc::clone)
929 }
930
931 fn rewrite_sealed_entity_into_growing(
932 &self,
933 entity: UnifiedEntity,
934 metadata: Option<&Metadata>,
935 ) -> Result<(), SegmentError> {
936 let entity_id = entity.id;
937 let sealed_arc = self
938 .find_sealed_segment_arc(entity_id)
939 .ok_or(SegmentError::NotFound(entity_id))?;
940
941 let metadata_to_apply = {
942 let mut sealed = sealed_arc.write();
943 let existing_metadata = sealed.get_metadata(entity_id);
944 if !sealed.force_delete(entity_id) {
945 return Err(SegmentError::NotFound(entity_id));
946 }
947 metadata.cloned().or(existing_metadata)
948 };
949
950 let growing_arc = self.get_or_create_growing();
951 let growing_id = {
952 let mut growing = growing_arc.write();
953 growing.insert(entity)?;
954 if let Some(metadata) = metadata_to_apply {
955 growing.set_metadata(entity_id, metadata)?;
956 }
957 growing.id()
958 };
959
960 self.entity_segment.write().insert(entity_id, growing_id);
961 Ok(())
962 }
963
964 pub fn update(&self, entity: UnifiedEntity) -> Result<(), SegmentError> {
966 let entity_id = entity.id;
967 let mut entity = Some(entity);
968
969 if let Some(growing_arc) = self.growing.read().as_ref() {
971 let mut growing = growing_arc.write();
972 if growing.contains(entity_id) && growing.state().is_writable() {
973 return growing.update(entity.take().expect("entity already moved"));
974 }
975 }
976
977 let segment_id = self.entity_segment.read().get(&entity_id).copied();
979 if let Some(seg_id) = segment_id {
980 if let Some(growing_arc) = self.growing.read().as_ref() {
981 let mut growing = growing_arc.write();
982 if growing.id() == seg_id && growing.state().is_writable() {
983 return growing.update(entity.take().expect("entity already moved"));
984 }
985 }
986 }
987
988 if let Some(entity) = entity.take() {
989 return self.rewrite_sealed_entity_into_growing(entity, None);
990 }
991
992 Err(SegmentError::NotFound(entity_id))
993 }
994
995 pub fn update_with_metadata(
998 &self,
999 entity: UnifiedEntity,
1000 metadata: Option<&Metadata>,
1001 ) -> Result<(), SegmentError> {
1002 let entity_id = entity.id;
1003 let mut entity = Some(entity);
1004
1005 if let Some(growing_arc) = self.growing.read().as_ref() {
1007 let mut growing = growing_arc.write();
1008 if growing.contains(entity_id) && growing.state().is_writable() {
1009 growing.update(entity.take().expect("entity already moved"))?;
1010 if let Some(metadata) = metadata {
1011 growing.set_metadata(entity_id, metadata.clone())?;
1012 }
1013 return Ok(());
1014 }
1015 }
1016
1017 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1019 if let Some(seg_id) = segment_id {
1020 if let Some(growing_arc) = self.growing.read().as_ref() {
1021 let mut growing = growing_arc.write();
1022 if growing.id() == seg_id && growing.state().is_writable() {
1023 growing.update(entity.take().expect("entity already moved"))?;
1024 if let Some(metadata) = metadata {
1025 growing.set_metadata(entity_id, metadata.clone())?;
1026 }
1027 return Ok(());
1028 }
1029 }
1030 }
1031
1032 if let Some(entity) = entity.take() {
1033 return self.rewrite_sealed_entity_into_growing(entity, metadata);
1034 }
1035
1036 Err(SegmentError::NotFound(entity_id))
1037 }
1038
1039 pub fn update_hot(
1043 &self,
1044 entity: UnifiedEntity,
1045 modified_columns: &[String],
1046 ) -> Result<(), SegmentError> {
1047 let entity_id = entity.id;
1048 let mut entity = Some(entity);
1049
1050 if let Some(growing_arc) = self.growing.read().as_ref() {
1051 let mut growing = growing_arc.write();
1052 if growing.contains(entity_id) && growing.state().is_writable() {
1053 return growing.update_hot(
1054 entity.take().expect("entity already moved"),
1055 modified_columns,
1056 );
1057 }
1058 }
1059
1060 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1061 if let Some(seg_id) = segment_id {
1062 if let Some(growing_arc) = self.growing.read().as_ref() {
1063 let mut growing = growing_arc.write();
1064 if growing.id() == seg_id && growing.state().is_writable() {
1065 return growing.update_hot(
1066 entity.take().expect("entity already moved"),
1067 modified_columns,
1068 );
1069 }
1070 }
1071 }
1072
1073 if let Some(entity) = entity.take() {
1074 return self.rewrite_sealed_entity_into_growing(entity, None);
1075 }
1076
1077 Err(SegmentError::NotFound(entity_id))
1078 }
1079
1080 pub fn update_hot_with_metadata(
1083 &self,
1084 entity: UnifiedEntity,
1085 modified_columns: &[String],
1086 metadata: Option<&Metadata>,
1087 ) -> Result<(), SegmentError> {
1088 let entity_id = entity.id;
1089 let mut entity = Some(entity);
1090
1091 if let Some(growing_arc) = self.growing.read().as_ref() {
1092 let mut growing = growing_arc.write();
1093 if growing.contains(entity_id) && growing.state().is_writable() {
1094 growing.update_hot(
1095 entity.take().expect("entity already moved"),
1096 modified_columns,
1097 )?;
1098 if let Some(metadata) = metadata {
1099 growing.set_metadata(entity_id, metadata.clone())?;
1100 }
1101 return Ok(());
1102 }
1103 }
1104
1105 let segment_id = self.entity_segment.read().get(&entity_id).copied();
1106 if let Some(seg_id) = segment_id {
1107 if let Some(growing_arc) = self.growing.read().as_ref() {
1108 let mut growing = growing_arc.write();
1109 if growing.id() == seg_id && growing.state().is_writable() {
1110 growing.update_hot(
1111 entity.take().expect("entity already moved"),
1112 modified_columns,
1113 )?;
1114 if let Some(metadata) = metadata {
1115 growing.set_metadata(entity_id, metadata.clone())?;
1116 }
1117 return Ok(());
1118 }
1119 }
1120 }
1121
1122 if let Some(entity) = entity.take() {
1123 return self.rewrite_sealed_entity_into_growing(entity, metadata);
1124 }
1125
1126 Err(SegmentError::NotFound(entity_id))
1127 }
1128
1129 pub fn update_hot_batch_with_metadata<'a, I>(&self, items: I) -> Result<(), SegmentError>
1132 where
1133 I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
1134 {
1135 let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
1136 items.into_iter().collect();
1137 if items.is_empty() {
1138 return Ok(());
1139 }
1140
1141 if let Some(growing_arc) = self.growing.read().as_ref() {
1142 let mut growing = growing_arc.write();
1143 if growing.state().is_writable() {
1144 match growing.update_hot_batch_with_metadata(items.iter().copied()) {
1145 Ok(()) => return Ok(()),
1146 Err(SegmentError::NotFound(_)) => {}
1147 Err(other) => return Err(other),
1148 }
1149 }
1150 }
1151
1152 for (entity, modified_columns, metadata) in items {
1153 self.update_hot_with_metadata(entity.clone(), modified_columns, metadata)?;
1154 }
1155 Ok(())
1156 }
1157
1158 pub fn delete(&self, id: EntityId) -> Result<bool, SegmentError> {
1160 if let Some(growing_arc) = self.growing.read().as_ref() {
1163 let mut growing = growing_arc.write();
1164 if growing.contains(id) && growing.state().is_writable() {
1165 let seg_id = growing.id();
1166 let deleted = growing.delete(id)?;
1167 if deleted {
1168 self.entity_segment.write().remove(&id);
1169 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1170 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1171 }
1172 return Ok(deleted);
1173 }
1174 }
1175
1176 let segment_id = self.entity_segment.read().get(&id).copied();
1179
1180 if let Some(seg_id) = segment_id {
1181 if let Some(growing_arc) = self.growing.read().as_ref() {
1182 let mut growing = growing_arc.write();
1183 if growing.id() == seg_id && growing.state().is_writable() {
1184 let deleted = growing.delete(id)?;
1185 if deleted {
1186 self.entity_segment.write().remove(&id);
1187 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1188 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1189 }
1190 return Ok(deleted);
1191 }
1192 }
1193 }
1194
1195 {
1198 let sealed = self.sealed.read();
1199 for segment_arc in sealed.iter() {
1200 let mut seg = segment_arc.write();
1201 let seg_id = seg.id();
1202 if seg.contains(id) {
1203 let deleted = seg.force_delete(id);
1204 drop(seg);
1205 if deleted {
1206 self.entity_segment.write().remove(&id);
1207 self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1208 self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1209 }
1210 return Ok(deleted);
1211 }
1212 }
1213 }
1214
1215 Ok(false)
1216 }
1217
1218 pub fn delete_batch(&self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
1219 if ids.is_empty() {
1220 return Ok(Vec::new());
1221 }
1222
1223 let mut deleted_ids = Vec::with_capacity(ids.len());
1224
1225 if let Some(growing_arc) = self.growing.read().as_ref() {
1226 let mut growing = growing_arc.write();
1227 if growing.state().is_writable() {
1228 let seg_id = growing.id();
1229 let deleted = growing.delete_batch(ids)?;
1230 if !deleted.is_empty() {
1231 {
1232 let mut entity_segment = self.entity_segment.write();
1233 for id in &deleted {
1234 entity_segment.remove(id);
1235 }
1236 }
1237 self.total_entities_atomic
1238 .fetch_sub(deleted.len() as u64, Ordering::Relaxed);
1239 for id in &deleted {
1240 self.emit(LifecycleEvent::EntityDeleted(*id, seg_id));
1241 }
1242 deleted_ids.extend(deleted);
1243 }
1244 }
1245 }
1246
1247 if deleted_ids.len() == ids.len() {
1248 return Ok(deleted_ids);
1249 }
1250
1251 let deleted_set: std::collections::HashSet<EntityId> =
1252 deleted_ids.iter().copied().collect();
1253 for &id in ids {
1254 if deleted_set.contains(&id) {
1255 continue;
1256 }
1257 if self.delete(id)? {
1258 deleted_ids.push(id);
1259 }
1260 }
1261
1262 Ok(deleted_ids)
1263 }
1264
1265 pub fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1267 if let Some(growing_arc) = self.growing.read().as_ref() {
1269 let growing = growing_arc.read();
1270 if growing.contains(id) {
1271 return growing.get_metadata(id);
1272 }
1273 }
1274
1275 let segment_id = self.entity_segment.read().get(&id).copied();
1277
1278 if let Some(seg_id) = segment_id {
1279 if let Some(growing_arc) = self.growing.read().as_ref() {
1280 let growing = growing_arc.read();
1281 if growing.id() == seg_id {
1282 return growing.get_metadata(id);
1283 }
1284 }
1285
1286 let sealed = self.sealed.read();
1287 for segment in sealed.iter() {
1288 if segment.id() == seg_id {
1289 return segment.get_metadata(id);
1290 }
1291 }
1292 }
1293
1294 if let Some(segment_arc) = self.find_sealed_segment_arc(id) {
1295 return segment_arc.read().get_metadata(id);
1296 }
1297
1298 None
1299 }
1300
1301 pub fn set_metadata(&self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1303 if let Some(growing_arc) = self.growing.read().as_ref() {
1306 let mut growing = growing_arc.write();
1307 if growing.contains(id) && growing.state().is_writable() {
1308 return growing.set_metadata(id, metadata);
1309 }
1310 }
1311
1312 let segment_id = self.entity_segment.read().get(&id).copied();
1314
1315 if let Some(seg_id) = segment_id {
1316 if let Some(growing_arc) = self.growing.read().as_ref() {
1317 let mut growing = growing_arc.write();
1318 if growing.id() == seg_id && growing.state().is_writable() {
1319 return growing.set_metadata(id, metadata);
1320 }
1321 }
1322 }
1323
1324 if let Some(entity) = self.get(id) {
1325 return self.rewrite_sealed_entity_into_growing(entity, Some(&metadata));
1326 }
1327
1328 Err(SegmentError::NotFound(id))
1329 }
1330
1331 fn maybe_seal_growing(&self) -> Result<(), SegmentError> {
1333 let should_seal = {
1334 let growing_opt = self.growing.read();
1335 if let Some(growing_arc) = growing_opt.as_ref() {
1336 let growing = growing_arc.read();
1337 growing.should_seal(&self.config.segment_config)
1338 || growing.idle_secs() >= self.config.idle_seal_secs
1339 } else {
1340 false
1341 }
1342 };
1343
1344 if should_seal {
1345 self.seal_current()?;
1346 }
1347
1348 Ok(())
1349 }
1350
1351 pub fn seal_current(&self) -> Result<SegmentId, SegmentError> {
1353 let mut growing_slot = self.growing.write();
1356 if let Some(growing_arc) = growing_slot.take() {
1357 let mut growing = growing_arc.write();
1358 let seg_id = growing.id();
1359 let entity_count = growing.stats().entity_count as u64;
1360
1361 growing.seal()?;
1363
1364 drop(growing); self.sealed.write().push(growing_arc);
1370 drop(growing_slot);
1371
1372 self.mark_sealed_pages_visible(entity_count);
1374
1375 {
1377 let mut stats = self.stats.write();
1378 stats.growing_count = stats.growing_count.saturating_sub(1);
1379 stats.sealed_count += 1;
1380 stats.seal_ops += 1;
1381 }
1382
1383 self.emit(LifecycleEvent::SegmentSealed(seg_id));
1384
1385 return Ok(seg_id);
1386 }
1387
1388 Err(SegmentError::InvalidState(SegmentState::Sealed))
1389 }
1390
1391 pub fn force_seal(&self) -> Result<Option<SegmentId>, SegmentError> {
1393 let has_growing = self.growing.read().is_some();
1394 if has_growing {
1395 Ok(Some(self.seal_current()?))
1396 } else {
1397 Ok(None)
1398 }
1399 }
1400
1401 pub fn all_visible_fraction(&self) -> f64 {
1411 const ROWS_PER_PAGE: u32 = 256;
1412 let sealed = self.sealed.read();
1413 if sealed.is_empty() {
1414 return 0.0;
1415 }
1416 let mut total_pages: u64 = 0;
1417 for seg_arc in sealed.iter() {
1418 let seg = seg_arc.read();
1419 let entity_count = seg.stats().entity_count as u64;
1420 let pages = entity_count.div_ceil(ROWS_PER_PAGE as u64);
1421 total_pages += pages;
1422 }
1423 if total_pages == 0 {
1424 return 0.0;
1425 }
1426 let visible = self.visibility_map.all_visible_count();
1427 (visible as f64 / total_pages as f64).min(1.0)
1428 }
1429
1430 fn mark_sealed_pages_visible(&self, seg_entity_count: u64) {
1433 const ROWS_PER_PAGE: u32 = 256;
1434 let existing_visible = self.visibility_map.all_visible_count();
1435 let start_page = existing_visible as u32;
1437 let new_pages = seg_entity_count.div_ceil(ROWS_PER_PAGE as u64);
1438 let end_page = start_page + new_pages as u32;
1439 self.visibility_map.mark_range_visible(start_page, end_page);
1440 }
1441
1442 pub(crate) fn find_unique_key_conflict(
1446 &self,
1447 columns: &[Vec<String>],
1448 key_index: usize,
1449 signatures: &[String],
1450 exclude_id: Option<EntityId>,
1451 reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1452 ) -> Option<EntityId> {
1453 if let Some(growing) = self.growing.read().as_ref() {
1454 if let Some(id) = growing.read().find_unique_key_conflict(
1455 columns,
1456 key_index,
1457 signatures,
1458 exclude_id,
1459 reserves_key,
1460 ) {
1461 return Some(id);
1462 }
1463 }
1464 for segment in self.sealed.read().iter() {
1465 if let Some(id) = segment.read().find_unique_key_conflict(
1466 columns,
1467 key_index,
1468 signatures,
1469 exclude_id,
1470 reserves_key,
1471 ) {
1472 return Some(id);
1473 }
1474 }
1475 None
1476 }
1477
1478 pub fn for_each_entity<F>(&self, callback: F)
1484 where
1485 F: FnMut(&UnifiedEntity) -> bool,
1486 {
1487 self.for_each_entity_in_segments(|_| true, || true, callback);
1488 }
1489
1490 pub(crate) fn checkpoint_revisions(&self) -> Vec<(u64, u64)> {
1492 let growing = self.growing.read();
1493 let sealed = self.sealed.read();
1494 growing
1495 .iter()
1496 .chain(sealed.iter())
1497 .map(|segment| {
1498 let segment = segment.read();
1499 (segment.id(), segment.checkpoint_revision())
1500 })
1501 .collect()
1502 }
1503
1504 pub(crate) fn checkpoint_mutations_since(
1505 &self,
1506 before: &[(u64, u64)],
1507 ) -> Option<Vec<EntityId>> {
1508 let growing = self.growing.read();
1509 let sealed = self.sealed.read();
1510 if growing.iter().count() + sealed.len() != before.len() {
1511 return None;
1512 }
1513 let mut changed = std::collections::BTreeSet::new();
1514 for (segment, &(id, revision)) in growing.iter().chain(sealed.iter()).zip(before) {
1515 let segment = segment.read();
1516 if segment.id() != id {
1517 return None;
1518 }
1519 changed.extend(segment.checkpoint_mutations_since(revision)?);
1520 if changed.len() > 1024 {
1521 return None;
1522 }
1523 }
1524 Some(changed.into_iter().collect())
1525 }
1526
1527 pub(crate) fn checkpoint_tracking_bytes(&self) -> usize {
1528 let growing = self.growing.read();
1529 let sealed = self.sealed.read();
1530 growing
1531 .iter()
1532 .chain(sealed.iter())
1533 .map(|segment| segment.read().checkpoint_tracking_bytes())
1534 .sum()
1535 }
1536
1537 pub(crate) fn cached_physical_root(&self, seed: u64, compute: impl FnOnce() -> u64) -> u64 {
1539 let segments = self.checkpoint_revisions();
1540 let mut cache = self.physical_root_cache.lock();
1541 if let Some(previous) = cache.as_ref() {
1542 if previous.seed == seed && previous.segments == segments {
1543 return previous.root;
1544 }
1545 }
1546 let root = compute();
1547 *cache = Some(PhysicalRootCacheEntry {
1548 segments,
1549 seed,
1550 root,
1551 });
1552 root
1553 }
1554
1555 pub(crate) fn for_each_entity_in_id_order(
1560 &self,
1561 mut visit: impl FnMut(&UnifiedEntity) -> bool,
1562 ) {
1563 self.for_each_entity_source_in_id_order(|_, entity| visit(entity));
1564 }
1565
1566 pub(crate) fn page_in_id_order(
1568 &self,
1569 offset: usize,
1570 limit: usize,
1571 ) -> (usize, Vec<UnifiedEntity>) {
1572 let end = offset.saturating_add(limit);
1573 let mut total = 0;
1574 let mut items = Vec::with_capacity(limit.min(self.count()));
1575 self.for_each_entity_in_id_order(|entity| {
1576 if total >= offset && total < end {
1577 items.push(entity.clone());
1578 }
1579 total += 1;
1580 true
1581 });
1582 self.restore_owned_row_schemas(items.iter_mut());
1583 (total, items)
1584 }
1585
1586 pub(crate) fn for_each_entity_with_metadata_in_id_order(
1587 &self,
1588 mut visit: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1589 ) {
1590 self.for_each_entity_source_in_id_order(|segment, entity| {
1591 visit(entity, segment.get_metadata(entity.id))
1592 });
1593 }
1594
1595 fn for_each_entity_source_in_id_order(
1596 &self,
1597 mut visit: impl FnMut(&GrowingSegment, &UnifiedEntity) -> bool,
1598 ) {
1599 let segments: Vec<_> = {
1600 let growing = self.growing.read();
1601 let sealed = self.sealed.read();
1602 growing.iter().chain(sealed.iter()).cloned().collect()
1603 };
1604 let mut order = Vec::new();
1605 for (source, segment) in segments.iter().enumerate() {
1606 segment.read().for_each_fast(|entity| {
1607 order.push((entity.id, source));
1608 true
1609 });
1610 }
1611 order.sort_by_key(|(id, _)| id.raw());
1612 for (id, source) in order {
1613 let segment = segments[source].read();
1614 if let Some(entity) = segment.get(id) {
1615 if !visit(&segment, entity) {
1616 break;
1617 }
1618 }
1619 }
1620 }
1621
1622 pub(crate) fn for_each_entity_with_metadata(
1625 &self,
1626 mut callback: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1627 ) {
1628 if let Some(growing_arc) = self.growing.read().as_ref() {
1629 let growing = growing_arc.read();
1630 if !growing.for_each_fast(|entity| callback(entity, growing.get_metadata(entity.id))) {
1631 return;
1632 }
1633 }
1634 let sealed = self.sealed.read();
1635 for segment_arc in sealed.iter() {
1636 let segment = segment_arc.read();
1637 if !segment.for_each_fast(|entity| callback(entity, segment.get_metadata(entity.id))) {
1638 return;
1639 }
1640 }
1641 }
1642
1643 fn for_each_entity_in_segments<F>(
1644 &self,
1645 segment_filter: impl Fn(&GrowingSegment) -> bool,
1646 mut before_segment: impl FnMut() -> bool,
1647 mut callback: F,
1648 ) where
1649 F: FnMut(&UnifiedEntity) -> bool,
1650 {
1651 if let Some(growing_arc) = self.growing.read().as_ref() {
1655 if !before_segment() {
1656 return;
1657 }
1658 let growing = if let Some(g) = growing_arc.try_read() {
1659 g
1660 } else {
1661 growing_arc.read()
1662 };
1663 if segment_filter(&growing) && !growing.for_each_fast(&mut callback) {
1664 return;
1665 }
1666 }
1667
1668 let sealed = self.sealed.read();
1670 for segment_arc in sealed.iter() {
1671 if !before_segment() {
1672 return;
1673 }
1674 let segment = segment_arc.read();
1675 if segment_filter(&segment) && !segment.for_each_fast(&mut callback) {
1676 return;
1677 }
1678 }
1679 }
1680
1681 pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1697 &self,
1698 init: FInit,
1699 fold: FFold,
1700 reduce: FReduce,
1701 ) -> T
1702 where
1703 T: Send,
1704 FInit: Fn() -> T + Send + Sync,
1705 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1706 FReduce: Fn(T, T) -> T + Send + Sync,
1707 {
1708 use rayon::prelude::*;
1709
1710 let mut acc = init();
1713 if let Some(growing_arc) = self.growing.read().as_ref() {
1714 let growing = if let Some(g) = growing_arc.try_read() {
1715 g
1716 } else {
1717 growing_arc.read()
1718 };
1719 growing.for_each_fast(|entity| {
1720 acc = fold(std::mem::replace(&mut acc, init()), entity);
1721 true
1722 });
1723 }
1724
1725 let segments: Vec<_> = {
1729 let sealed = self.sealed.read();
1730 sealed.iter().cloned().collect()
1731 };
1732
1733 if segments.len() <= 1 {
1734 for seg_arc in &segments {
1735 let seg = seg_arc.read();
1736 seg.for_each_fast(|entity| {
1737 acc = fold(std::mem::replace(&mut acc, init()), entity);
1738 true
1739 });
1740 }
1741 return acc;
1742 }
1743
1744 let sealed_acc = segments
1745 .into_par_iter()
1746 .map(|seg_arc| {
1747 let mut local = init();
1748 let seg = seg_arc.read();
1749 seg.for_each_fast(|entity| {
1750 local = fold(std::mem::replace(&mut local, init()), entity);
1751 true
1752 });
1753 local
1754 })
1755 .reduce(&init, &reduce);
1756
1757 reduce(acc, sealed_acc)
1758 }
1759
1760 pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1769 where
1770 F: FnMut(&UnifiedEntity) -> bool,
1771 {
1772 let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1773 }
1774
1775 pub fn for_each_entity_zoned_with_stats<F>(
1776 &self,
1777 zone_preds: &[(&str, ZoneColPred<'_>)],
1778 mut callback: F,
1779 ) -> SegmentScanStats
1780 where
1781 F: FnMut(&UnifiedEntity) -> bool,
1782 {
1783 let mut scan_stats = SegmentScanStats::default();
1784 if let Some(growing_arc) = self.growing.read().as_ref() {
1790 scan_stats.segments_total += 1;
1791 scan_stats.segments_scanned += 1;
1792 let growing = if let Some(g) = growing_arc.try_read() {
1793 g
1794 } else {
1795 growing_arc.read()
1796 };
1797 if !growing.for_each_fast(&mut callback) {
1798 return scan_stats;
1799 }
1800 }
1801
1802 let sealed = self.sealed.read();
1804 for segment_arc in sealed.iter() {
1805 scan_stats.segments_total += 1;
1806 let segment = segment_arc.read();
1807 if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1808 scan_stats.segments_pruned += 1;
1809 continue; }
1811 scan_stats.segments_scanned += 1;
1812 if !segment.for_each_fast(&mut callback) {
1813 return scan_stats;
1814 }
1815 }
1816 scan_stats
1817 }
1818
1819 pub fn query_all_zoned<F>(
1829 &self,
1830 zone_preds: &[(&str, ZoneColPred<'_>)],
1831 filter: F,
1832 ) -> Vec<UnifiedEntity>
1833 where
1834 F: Fn(&UnifiedEntity) -> bool + Sync,
1835 {
1836 self.query_all_zoned_with_stats(zone_preds, filter).0
1837 }
1838
1839 pub fn query_all_zoned_with_stats<F>(
1840 &self,
1841 zone_preds: &[(&str, ZoneColPred<'_>)],
1842 filter: F,
1843 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1844 where
1845 F: Fn(&UnifiedEntity) -> bool + Sync,
1846 {
1847 let mut results = Vec::new();
1848 let mut scan_stats = SegmentScanStats::default();
1849
1850 if let Some(growing_arc) = self.growing.read().as_ref() {
1853 scan_stats.segments_total += 1;
1854 scan_stats.segments_scanned += 1;
1855 let growing = if let Some(g) = growing_arc.try_read() {
1856 g
1857 } else {
1858 growing_arc.read()
1859 };
1860 results.extend(growing.iter().filter(|e| filter(e)).cloned());
1861 }
1862
1863 let sealed = self.sealed.read();
1865 let surviving: Vec<_> = sealed
1867 .iter()
1868 .filter(|seg_arc| {
1869 scan_stats.segments_total += 1;
1870 if zone_preds.is_empty() {
1871 scan_stats.segments_scanned += 1;
1872 return true;
1873 }
1874 let seg = seg_arc.read();
1875 if seg.can_skip_zone_preds(zone_preds) {
1876 scan_stats.segments_pruned += 1;
1877 false
1878 } else {
1879 scan_stats.segments_scanned += 1;
1880 true
1881 }
1882 })
1883 .collect();
1884
1885 let use_parallel = surviving.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1886
1887 if use_parallel {
1888 let filter_ref = &filter;
1889 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1890 surviving
1891 .iter()
1892 .map(|segment| {
1893 s.spawn(move || {
1894 segment
1895 .read()
1896 .iter()
1897 .filter(|e| filter_ref(e))
1898 .cloned()
1899 .collect::<Vec<_>>()
1900 })
1901 })
1902 .collect::<Vec<_>>()
1903 .into_iter()
1904 .map(|handle| handle.join().unwrap_or_default())
1905 .collect()
1906 });
1907 for batch in segment_results {
1908 results.extend(batch);
1909 }
1910 } else {
1911 for segment_arc in surviving {
1912 let seg = segment_arc.read();
1913 results.extend(seg.iter().filter(|e| filter(e)).cloned());
1914 }
1915 }
1916
1917 (results, scan_stats)
1918 }
1919
1920 fn scan_entity_visible(snapshot: Option<&SnapshotContext>, entity: &UnifiedEntity) -> bool {
1926 match snapshot {
1927 Some(ctx) => entity_visible_with_context(Some(ctx), entity),
1928 None => {
1929 !crate::runtime::ai::moderation::entity_moderation_hidden(entity)
1930 && entity.xmax == 0
1931 }
1932 }
1933 }
1934
1935 pub fn scan<F>(&self, snapshot: Option<&SnapshotContext>, filter: F) -> Vec<UnifiedEntity>
1942 where
1943 F: Fn(&UnifiedEntity) -> bool + Sync,
1944 {
1945 self.query_all(|entity| Self::scan_entity_visible(snapshot, entity) && filter(entity))
1946 }
1947
1948 pub(crate) fn project_key_entity<T>(
1953 &self,
1954 key: &str,
1955 snapshot: Option<&SnapshotContext>,
1956 versioned: bool,
1957 mut project: impl FnMut(&UnifiedEntity) -> T,
1958 ) -> Option<T> {
1959 let mut probe = |segment: &GrowingSegment, best: &mut Option<(u64, T)>| {
1960 if let Some(entity) = segment.find_key_entity(key, versioned, &mut |entity| {
1961 !versioned || Self::scan_entity_visible(snapshot, entity)
1962 }) {
1963 if best.as_ref().is_none_or(|(xmin, _)| entity.xmin >= *xmin) {
1964 *best = Some((entity.xmin, project(entity)));
1965 }
1966 }
1967 };
1968 let mut best = None;
1969 if let Some(growing) = self.growing.read().as_ref() {
1970 probe(&growing.read(), &mut best);
1971 }
1972 for segment in self.sealed.read().iter() {
1973 if !versioned && best.is_some() {
1974 break;
1975 }
1976 probe(&segment.read(), &mut best);
1977 }
1978 best.map(|(_, projected)| projected)
1979 }
1980
1981 pub(crate) fn scan_graph_kind(
1983 &self,
1984 snapshot: Option<&SnapshotContext>,
1985 kind: GraphEntityKind,
1986 ) -> Vec<UnifiedEntity> {
1987 self.query_all_in_segments(
1988 |segment| segment.may_contain_graph_kind(kind),
1989 |entity| Self::scan_entity_visible(snapshot, entity) && kind.matches(&entity.kind),
1990 )
1991 }
1992
1993 pub(crate) fn scan_graph_candidates_for_each<F>(
1998 &self,
1999 snapshot: Option<&SnapshotContext>,
2000 kind: GraphEntityKind,
2001 before_segment: impl FnMut() -> bool,
2002 mut callback: F,
2003 ) where
2004 F: FnMut(&UnifiedEntity) -> bool,
2005 {
2006 self.for_each_entity_in_segments(
2007 |segment| segment.may_contain_graph_kind(kind),
2008 before_segment,
2009 |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
2010 );
2011 }
2012
2013 pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
2018 where
2019 F: FnMut(&UnifiedEntity) -> bool,
2020 {
2021 self.for_each_entity(|entity| {
2022 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2023 });
2024 }
2025
2026 pub(crate) fn physical_scan_cursor(&self) -> PhysicalScanCursor {
2027 let growing = self.growing.read();
2028 let sealed = self.sealed.read();
2029 PhysicalScanCursor {
2030 sources: growing
2031 .iter()
2032 .chain(sealed.iter())
2033 .map(|segment| (Arc::downgrade(segment), segment.read().scan_cursor()))
2034 .collect(),
2035 source: 0,
2036 }
2037 }
2038
2039 pub(crate) fn physical_scan_batch(
2042 &self,
2043 cursor: &mut PhysicalScanCursor,
2044 work_left: &mut usize,
2045 ) -> Vec<EntityId> {
2046 let mut ids = Vec::with_capacity(super::segment::SCAN_BATCH_SIZE.min(*work_left));
2047 while !cursor.finished() && *work_left > 0 {
2048 let (source, position) = &mut cursor.sources[cursor.source];
2049 if position.finished() {
2050 *work_left -= 1;
2051 cursor.source += 1;
2052 continue;
2053 }
2054 let Some(segment) = source.upgrade() else {
2055 *work_left -= 1;
2056 cursor.source += 1;
2057 continue;
2058 };
2059 segment.read().scan_batch(
2060 position,
2061 &mut || {
2062 if *work_left == 0 {
2063 return false;
2064 }
2065 *work_left -= 1;
2066 true
2067 },
2068 &mut |id| ids.push(id),
2069 );
2070 if position.finished() {
2071 cursor.source += 1;
2072 }
2073 if !ids.is_empty() {
2074 break;
2075 }
2076 }
2077 ids
2078 }
2079
2080 pub(crate) fn scan_batches(
2086 &self,
2087 snapshot: Option<&SnapshotContext>,
2088 mut before_work: impl FnMut() -> bool,
2089 mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
2090 ) -> bool {
2091 let segments = {
2092 let growing = self.growing.read();
2093 let sealed = self.sealed.read();
2094 let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
2095 for segment in growing.iter().chain(sealed.iter()) {
2096 if !before_work() {
2097 return false;
2098 }
2099 let cursor = segment.read().scan_cursor();
2100 segments.push((Arc::clone(segment), cursor));
2101 }
2102 segments
2103 };
2104 let mut ids = Vec::new();
2105 let mut batch = Vec::new();
2106 for (segment, mut cursor) in segments {
2107 while !cursor.finished() {
2108 ids.clear();
2109 batch.clear();
2110 let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
2111 ids.reserve_exact(batch_size);
2112 batch.reserve_exact(batch_size);
2113 {
2114 let segment = segment.read();
2115 if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
2116 return false;
2117 }
2118 }
2119 if !before_work() {
2122 return false;
2123 }
2124 self.for_each_id(&ids, |_, entity| {
2125 if Self::scan_entity_visible(snapshot, entity) {
2126 batch.push(entity.clone());
2127 }
2128 });
2129 if !before_work() {
2130 return false;
2131 }
2132 if !consume(&batch) {
2133 return false;
2134 }
2135 }
2136 }
2137 true
2138 }
2139
2140 pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
2142 &self,
2143 snapshot: Option<&SnapshotContext>,
2144 init: FInit,
2145 fold: FFold,
2146 reduce: FReduce,
2147 ) -> T
2148 where
2149 T: Send,
2150 FInit: Fn() -> T + Send + Sync,
2151 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
2152 FReduce: Fn(T, T) -> T + Send + Sync,
2153 {
2154 self.fold_entities_parallel(
2155 init,
2156 |acc, entity| {
2157 if Self::scan_entity_visible(snapshot, entity) {
2158 fold(acc, entity)
2159 } else {
2160 acc
2161 }
2162 },
2163 reduce,
2164 )
2165 }
2166
2167 pub fn scan_for_each_zoned_with_stats<F>(
2169 &self,
2170 snapshot: Option<&SnapshotContext>,
2171 zone_preds: &[(&str, ZoneColPred<'_>)],
2172 mut callback: F,
2173 ) -> SegmentScanStats
2174 where
2175 F: FnMut(&UnifiedEntity) -> bool,
2176 {
2177 self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
2178 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2179 })
2180 }
2181
2182 pub fn scan_zoned_with_stats<F>(
2184 &self,
2185 snapshot: Option<&SnapshotContext>,
2186 zone_preds: &[(&str, ZoneColPred<'_>)],
2187 filter: F,
2188 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
2189 where
2190 F: Fn(&UnifiedEntity) -> bool + Sync,
2191 {
2192 self.query_all_zoned_with_stats(zone_preds, |entity| {
2193 Self::scan_entity_visible(snapshot, entity) && filter(entity)
2194 })
2195 }
2196
2197 pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
2200 where
2201 F: Fn(&UnifiedEntity) -> bool + Sync,
2202 {
2203 let mut entities = self.query_all_in_segments(|_| true, filter);
2204 self.restore_owned_row_schemas(entities.iter_mut());
2205 entities
2206 }
2207
2208 fn query_all_in_segments<F>(
2209 &self,
2210 segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
2211 filter: F,
2212 ) -> Vec<UnifiedEntity>
2213 where
2214 F: Fn(&UnifiedEntity) -> bool + Sync,
2215 {
2216 let mut results = Vec::new();
2217
2218 if let Some(growing_arc) = self.growing.read().as_ref() {
2221 let growing = if let Some(g) = growing_arc.try_read() {
2222 g
2223 } else {
2224 growing_arc.read()
2225 };
2226 if segment_filter(&growing) {
2227 results.extend(growing.iter().filter(|e| filter(e)).cloned());
2228 }
2229 }
2230
2231 let sealed = self.sealed.read();
2233 let use_parallel = sealed.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
2234 if use_parallel {
2235 let filter_ref = &filter;
2236 let segment_filter_ref = &segment_filter;
2237 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
2238 sealed
2239 .iter()
2240 .map(|segment| {
2241 s.spawn(move || {
2242 let segment = segment.read();
2243 if segment_filter_ref(&segment) {
2244 segment
2245 .iter()
2246 .filter(|e| filter_ref(e))
2247 .cloned()
2248 .collect::<Vec<_>>()
2249 } else {
2250 Vec::new()
2251 }
2252 })
2253 })
2254 .collect::<Vec<_>>()
2255 .into_iter()
2256 .map(|handle| handle.join().unwrap_or_default())
2257 .collect()
2258 });
2259 for batch in segment_results {
2260 results.extend(batch);
2261 }
2262 } else {
2263 for segment in sealed.iter() {
2264 let seg = segment.read();
2265 if segment_filter(&seg) {
2266 results.extend(seg.iter().filter(|e| filter(e)).cloned());
2267 }
2268 }
2269 }
2270
2271 results
2272 }
2273
2274 pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
2279 if let Some(growing_arc) = self.growing.read().as_ref() {
2280 let growing = growing_arc.read();
2281 if growing.may_contain_exact_key(key) {
2282 return true;
2283 }
2284 } else {
2285 return true;
2286 }
2287
2288 let sealed = self.sealed.read();
2289 for segment_arc in sealed.iter() {
2290 let segment = segment_arc.read();
2291 if segment.may_contain_exact_key(key) {
2292 return true;
2293 }
2294 }
2295
2296 false
2297 }
2298
2299 pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2301 let mut results = Vec::new();
2302
2303 if let Some(growing_arc) = self.growing.read().as_ref() {
2305 let growing = growing_arc.read();
2306 results.extend(growing.filter_metadata(filters));
2307 }
2308
2309 let sealed = self.sealed.read();
2311 for segment in sealed.iter() {
2312 results.extend(segment.filter_metadata(filters));
2313 }
2314
2315 results
2316 }
2317
2318 pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2320 let mut results = Vec::new();
2321
2322 if let Some(growing_arc) = self.growing.read().as_ref() {
2324 let growing = growing_arc.read();
2325 for entity in growing.iter_kind(kind) {
2326 results.push(entity.clone());
2327 }
2328 }
2329
2330 let sealed = self.sealed.read();
2335 for segment_arc in sealed.iter() {
2336 let segment = segment_arc.read();
2337 for entity in segment.iter_kind(kind) {
2338 results.push(entity.clone());
2339 }
2340 }
2341
2342 results
2343 }
2344
2345 pub fn count(&self) -> usize {
2347 self.total_entities_atomic.load(Ordering::Relaxed) as usize
2348 }
2349
2350 pub fn segment_ids(&self) -> Vec<SegmentId> {
2352 let mut ids = Vec::new();
2353
2354 if let Some(growing_arc) = self.growing.read().as_ref() {
2355 ids.push(growing_arc.read().id());
2356 }
2357
2358 let sealed = self.sealed.read();
2359 for segment in sealed.iter() {
2360 ids.push(segment.id());
2361 }
2362
2363 ids.extend(self.archived.read().iter().copied());
2364
2365 ids
2366 }
2367
2368 #[inline]
2381 #[allow(clippy::unused_self)]
2382 fn emit(&self, _event: LifecycleEvent) {}
2383
2384 pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2387 std::mem::take(&mut *self.events.write())
2388 }
2389
2390 pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2392 self.maybe_seal_growing()?;
2394
2395 if self.config.enable_consolidation {
2396 self.consolidation_tick();
2397 }
2398
2399 Ok(())
2400 }
2401
2402 fn consolidation_tick(&self) {
2413 let running = self.consolidation.read().is_some();
2416 if !running && !self.start_consolidation() {
2417 return;
2418 }
2419
2420 let mut guard = self.consolidation.write();
2423 let Some(run) = guard.as_mut() else {
2424 return;
2425 };
2426 self.copy_bounded(run);
2427 if run.cursor >= run.sources.len() {
2428 if let Some(run) = guard.take() {
2429 self.finish_consolidation(run);
2430 }
2431 }
2432 }
2433
2434 fn start_consolidation(&self) -> bool {
2438 let sources = self.select_candidates();
2439 self.start_consolidation_from_sources(sources)
2440 }
2441
2442 fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2443 if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2444 return false;
2445 }
2446
2447 let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2448 let merged = GrowingSegment::new(merged_id, &self.collection);
2449
2450 *self.consolidation.write() = Some(Consolidation {
2451 sources: sources
2452 .into_iter()
2453 .map(|id| SourceProgress {
2454 id,
2455 copied: Vec::new(),
2456 tombstones_at_open: None,
2457 })
2458 .collect(),
2459 merged_id,
2460 merged,
2461 cursor: 0,
2462 pending_ids: Vec::new(),
2463 pending_cursor: 0,
2464 entities_copied: 0,
2465 });
2466
2467 self.stats.write().consolidation.runs_started += 1;
2468 true
2469 }
2470
2471 pub fn pressure_consolidation_tick(&self) -> bool {
2476 if !self.config.enable_consolidation {
2477 return false;
2478 }
2479
2480 let running = self.consolidation.read().is_some();
2481 if !running {
2482 let sources = self.dirty_sealed_segments();
2483 if !self.start_consolidation_from_sources(sources) {
2484 return false;
2485 }
2486 }
2487
2488 self.consolidation_tick();
2489 true
2490 }
2491
2492 fn select_candidates(&self) -> Vec<SegmentId> {
2507 let sealed = self.sealed.read();
2508 if sealed.is_empty() {
2509 return Vec::new();
2510 }
2511
2512 if sealed.len() > self.config.max_sealed_segments {
2513 return sealed.iter().map(|segment| segment.read().id()).collect();
2514 }
2515
2516 let mut live = 0usize;
2517 let mut tombstones = 0usize;
2518 let mut held = 0u64;
2519 let mut reclaimable = 0u64;
2520 let mut dirty = Vec::new();
2521
2522 for segment_arc in sealed.iter() {
2523 let segment = segment_arc.read();
2524 live += segment.live_entity_count();
2525 tombstones += segment.tombstone_count();
2526 held += segment.resident_bytes();
2527 reclaimable += segment.reclaimable_bytes();
2528 if segment.tombstone_count() > 0 {
2529 dirty.push(segment.id());
2530 }
2531 }
2532
2533 let total = live + tombstones;
2534 let tombstone_ratio = if total == 0 {
2535 0.0
2536 } else {
2537 tombstones as f64 / total as f64
2538 };
2539 let fragmentation = if held == 0 {
2540 0.0
2541 } else {
2542 reclaimable as f64 / held as f64
2543 };
2544
2545 if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2546 || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2547 {
2548 return dirty;
2549 }
2550
2551 Vec::new()
2552 }
2553
2554 fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2555 self.sealed
2556 .read()
2557 .iter()
2558 .filter_map(|segment_arc| {
2559 let segment = segment_arc.read();
2560 (segment.reclaimable_bytes() > 0).then(|| segment.id())
2561 })
2562 .collect()
2563 }
2564
2565 fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2568 if sources.is_empty() {
2569 return false;
2570 }
2571 let sealed = self.sealed.read();
2572 sealed.iter().any(|segment_arc| {
2573 let segment = segment_arc.read();
2574 sources.contains(&segment.id()) && segment.tombstone_count() > 0
2575 })
2576 }
2577
2578 fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2579 let sealed = self.sealed.read();
2580 sealed
2581 .iter()
2582 .find(|segment| segment.read().id() == id)
2583 .map(Arc::clone)
2584 }
2585
2586 fn copy_bounded(&self, run: &mut Consolidation) {
2591 let mut budget = self.config.consolidation_entities_per_tick;
2592
2593 while budget > 0 && run.cursor < run.sources.len() {
2594 let source_id = run.sources[run.cursor].id;
2595 let Some(source_arc) = self.sealed_segment(source_id) else {
2596 run.cursor += 1;
2600 run.pending_ids.clear();
2601 run.pending_cursor = 0;
2602 continue;
2603 };
2604
2605 if run.sources[run.cursor].tombstones_at_open.is_none() {
2606 let source = source_arc.read();
2607 run.pending_ids = source.live_entity_ids();
2608 run.pending_cursor = 0;
2609 run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2610 }
2611
2612 {
2613 let source = source_arc.read();
2614 while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2615 let id = run.pending_ids[run.pending_cursor];
2616 run.pending_cursor += 1;
2617 budget -= 1;
2618
2619 let Some(entity) = source.get(id) else {
2622 continue;
2623 };
2624 let metadata = source.get_metadata(id);
2625 run.merged.adopt_entity(entity.clone(), metadata);
2626 run.sources[run.cursor].copied.push(id);
2627 run.entities_copied += 1;
2628 }
2629 }
2630
2631 if run.pending_cursor >= run.pending_ids.len() {
2632 run.cursor += 1;
2633 run.pending_ids.clear();
2634 run.pending_cursor = 0;
2635 }
2636 }
2637 }
2638
2639 fn finish_consolidation(&self, mut run: Consolidation) {
2645 let mut sealed = self.sealed.write();
2646
2647 let mut positions = Vec::with_capacity(run.sources.len());
2648 for source in &run.sources {
2649 if let Some(index) = sealed
2650 .iter()
2651 .position(|segment| segment.read().id() == source.id)
2652 {
2653 positions.push(index);
2654 }
2655 }
2656 if positions.is_empty() {
2657 return;
2658 }
2659 positions.sort_unstable();
2660
2661 let mut stale = Vec::new();
2665 for source in &run.sources {
2666 let Some(source_arc) = sealed
2667 .iter()
2668 .find(|segment| segment.read().id() == source.id)
2669 else {
2670 continue;
2671 };
2672 let segment = source_arc.read();
2673 if source.tombstones_at_open == Some(segment.tombstone_count()) {
2674 continue;
2675 }
2676 stale.extend(
2677 source
2678 .copied
2679 .iter()
2680 .copied()
2681 .filter(|id| !segment.contains(*id)),
2682 );
2683 }
2684 for id in stale {
2685 run.merged.evict_entity(id);
2686 }
2687
2688 let reclaimed_bytes: u64 = positions
2689 .iter()
2690 .map(|&index| sealed[index].read().resident_bytes())
2691 .sum();
2692 let reclaimed_tombstones: u64 = positions
2693 .iter()
2694 .map(|&index| sealed[index].read().tombstone_count() as u64)
2695 .sum();
2696
2697 if run.merged.seal().is_err() {
2698 return;
2699 }
2700 let merged_bytes = run.merged.resident_bytes();
2701 let merged_arc = Arc::new(RwLock::new(run.merged));
2702
2703 {
2704 let mut retired = self.retired.write();
2705 retired.retain(|source| source.strong_count() > 0);
2706 for &index in positions.iter().rev() {
2707 let source = sealed.remove(index);
2708 if Arc::strong_count(&source) > 1 {
2709 retired.push(Arc::downgrade(&source));
2710 }
2711 }
2712 }
2713 let insert_at = positions[0].min(sealed.len());
2714 sealed.insert(insert_at, Arc::clone(&merged_arc));
2715 drop(sealed);
2716
2717 let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
2719 {
2720 let mut entity_segment = self.entity_segment.write();
2721 for segment_id in entity_segment.values_mut() {
2722 if source_ids.contains(segment_id) {
2723 *segment_id = run.merged_id;
2724 }
2725 }
2726 }
2727
2728 {
2729 let mut stats = self.stats.write();
2730 stats.sealed_count = stats
2731 .sealed_count
2732 .saturating_sub(positions.len())
2733 .saturating_add(1);
2734 let consolidation = &mut stats.consolidation;
2735 consolidation.runs_completed += 1;
2736 consolidation.segments_merged += positions.len() as u64;
2737 consolidation.tombstones_reclaimed += reclaimed_tombstones;
2738 consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
2739 }
2740
2741 self.emit(LifecycleEvent::SegmentConsolidated {
2742 source: source_ids,
2743 target: run.merged_id,
2744 });
2745 }
2746
2747 #[cfg(test)]
2750 fn consolidation_progress(&self) -> Option<u64> {
2751 self.consolidation
2752 .read()
2753 .as_ref()
2754 .map(|run| run.entities_copied)
2755 }
2756}
2757
2758impl UnifiedSegment for Arc<RwLock<GrowingSegment>> {
2761 fn id(&self) -> SegmentId {
2762 self.read().id()
2763 }
2764
2765 fn state(&self) -> SegmentState {
2766 self.read().state()
2767 }
2768
2769 fn collection(&self) -> &str {
2770 "unknown"
2772 }
2773
2774 fn stats(&self) -> SegmentStats {
2775 self.read().stats()
2776 }
2777
2778 fn entity_count(&self) -> usize {
2779 self.read().entity_count()
2780 }
2781
2782 fn contains(&self, id: EntityId) -> bool {
2783 self.read().contains(id)
2784 }
2785
2786 fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2787 None
2790 }
2791
2792 fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2793 None
2794 }
2795
2796 fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2797 self.write().insert(entity)
2798 }
2799
2800 fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2801 self.write().update(entity)
2802 }
2803
2804 fn update_hot(
2805 &mut self,
2806 entity: UnifiedEntity,
2807 modified_columns: &[String],
2808 ) -> Result<(), SegmentError> {
2809 self.write().update_hot(entity, modified_columns)
2810 }
2811
2812 fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2813 self.write().delete(id)
2814 }
2815
2816 fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2817 self.read().get_metadata(id)
2818 }
2819
2820 fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2821 self.write().set_metadata(id, metadata)
2822 }
2823
2824 fn seal(&mut self) -> Result<(), SegmentError> {
2825 self.write().seal()
2826 }
2827
2828 fn should_seal(&self, config: &SegmentConfig) -> bool {
2829 self.read().should_seal(config)
2830 }
2831
2832 fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2833 Box::new(std::iter::empty())
2835 }
2836
2837 fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2838 Box::new(std::iter::empty())
2839 }
2840
2841 fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2842 self.read().filter_metadata(filters)
2843 }
2844}
2845
2846#[cfg(test)]
2847mod tests {
2848 use std::collections::HashSet;
2849
2850 use super::*;
2851 use crate::runtime::mvcc::SnapshotContext;
2852 use crate::storage::transaction::snapshot::{Snapshot, SnapshotManager};
2853 use crate::storage::unified::entity::{EntityData, EntityKind, RowData};
2854 use reddb_types::Value;
2855
2856 #[test]
2857 fn physical_pages_keep_global_order_across_segments_and_deleted_slots() {
2858 let manager = SegmentManager::new("pages");
2859 for group in [[50, 10], [30, 20]] {
2860 for id in group {
2861 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "pages", id, vec![]);
2862 entity.data = EntityData::Row(RowData::with_names(
2863 vec![Value::Integer(id as i64)],
2864 vec!["id".into()],
2865 ));
2866 manager.insert(entity).unwrap();
2867 }
2868 manager.force_seal().unwrap();
2869 }
2870 manager
2871 .insert(UnifiedEntity::table_row(
2872 EntityId::new(40),
2873 "pages",
2874 40,
2875 vec![],
2876 ))
2877 .unwrap();
2878 let (total, page) = manager.page_in_id_order(1, 2);
2879 assert_eq!(total, 5);
2880 assert_eq!(
2881 page.iter()
2882 .map(|entity| entity.id.raw())
2883 .collect::<Vec<_>>(),
2884 [20, 30]
2885 );
2886 let EntityData::Row(row) = &page[0].data else {
2887 panic!("row")
2888 };
2889 assert_eq!(row.get_field("id"), Some(&Value::Integer(20)));
2890 manager.delete(EntityId::new(30)).unwrap();
2891 let (total, page) = manager.page_in_id_order(1, 2);
2892 assert_eq!(total, 4);
2893 assert_eq!(
2894 page.iter()
2895 .map(|entity| entity.id.raw())
2896 .collect::<Vec<_>>(),
2897 [20, 40]
2898 );
2899 for offset in [4, usize::MAX] {
2900 let (total, page) = manager.page_in_id_order(offset, 2);
2901 assert_eq!(total, 4);
2902 assert!(page.is_empty());
2903 }
2904 }
2905
2906 fn lookup_version(id: u64, key: &str, xmin: u64, xmax: u64) -> UnifiedEntity {
2907 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "keys", id, vec![]);
2908 entity.data = EntityData::Row(RowData::with_names(
2909 vec![Value::text(key)],
2910 vec!["key".into()],
2911 ));
2912 entity.set_xmin(xmin);
2913 entity.set_xmax(xmax);
2914 entity
2915 }
2916
2917 #[test]
2918 fn key_lookup_matches_snapshot_scan_across_segments() {
2919 let manager = Arc::new(SegmentManager::new("keys"));
2920 for entity in [
2921 lookup_version(1, "changed", 3, 12),
2922 lookup_version(2, "deleted", 3, 7),
2923 ] {
2924 manager.insert(entity).expect("old version");
2925 }
2926 manager.force_seal().expect("seal old versions");
2927 for entity in [
2928 lookup_version(3, "changed", 12, 0),
2929 lookup_version(4, "pending", 4, 0),
2930 lookup_version(5, "own", 15, 0),
2931 lookup_version(6, "live", 3, 0),
2932 ] {
2933 manager.insert(entity).expect("new version");
2934 }
2935 std::thread::spawn(move || {
2936 let snapshot = SnapshotContext {
2937 snapshot: Snapshot {
2938 xid: 10,
2939 in_progress: HashSet::from([4]),
2940 },
2941 manager: Arc::new(SnapshotManager::new()),
2942 own_xids: HashSet::from([15]),
2943 requires_index_fallback: false,
2944 serializable_reader: None,
2945 };
2946 for snapshot in [Some(&snapshot), None] {
2947 for key in ["changed", "deleted", "pending", "own", "live", "absent"] {
2948 let expected = manager
2949 .scan(snapshot, |entity| {
2950 super::super::segment_key_lookup::SegmentKeyLookup::key(entity)
2951 == Some(key)
2952 })
2953 .into_iter()
2954 .max_by_key(|entity| entity.xmin)
2955 .map(|entity| entity.id);
2956 assert_eq!(
2957 manager.project_key_entity(key, snapshot, true, |entity| entity.id),
2958 expected,
2959 "key={key}"
2960 );
2961 }
2962 }
2963 assert_eq!(
2964 manager.project_key_entity("changed", Some(&snapshot), true, |entity| entity.id),
2965 Some(EntityId::new(1))
2966 );
2967 assert_eq!(
2968 manager.project_key_entity("changed", None, true, |entity| entity.id),
2969 Some(EntityId::new(3))
2970 );
2971 assert!(manager
2972 .project_key_entity("deleted", None, true, |entity| entity.id)
2973 .is_none());
2974 })
2975 .join()
2976 .expect("snapshot worker");
2977 }
2978
2979 #[test]
2980 fn key_lookup_survives_paced_consolidation() {
2981 let manager = consolidating_manager(1);
2982 manager
2983 .bulk_insert(
2984 (1..=10)
2985 .map(|id| lookup_version(id, &format!("key-{id}"), 1, 0))
2986 .collect(),
2987 )
2988 .expect("rows");
2989 manager.force_seal().expect("seal");
2990 for id in 1..=4 {
2991 manager.delete(EntityId::new(id)).expect("delete");
2992 }
2993 assert!(manager.pressure_consolidation_tick());
2994 drain_maintenance(&manager);
2995 for id in 1..=10 {
2996 assert_eq!(
2997 manager.project_key_entity(&format!("key-{id}"), None, false, |entity| entity.id),
2998 (id > 4).then_some(EntityId::new(id))
2999 );
3000 }
3001 }
3002
3003 #[test]
3004 #[ignore = "diagnostic of segment fragmentation and retained history; no timing assertions"]
3005 fn key_lookup_fragmentation_history_diagnostic() {
3006 use std::{hint::black_box, time::Instant};
3007 for segments in [1, 16, 64] {
3008 for versions in [1, 100, 1000] {
3009 let manager = SegmentManager::new("keys");
3010 let mut id = 1;
3011 for segment in 0..segments {
3012 let mut rows: Vec<_> = (0..128)
3013 .map(|_| {
3014 let entity = lookup_version(id, &format!("other-{id}"), 1, 0);
3015 id += 1;
3016 entity
3017 })
3018 .collect();
3019 for version in (segment..versions).step_by(segments) {
3020 rows.push(lookup_version(
3021 id,
3022 "history",
3023 version as u64 + 1,
3024 if version + 1 == versions {
3025 0
3026 } else {
3027 version as u64 + 2
3028 },
3029 ));
3030 id += 1;
3031 }
3032 manager.bulk_insert(rows).expect("rows");
3033 manager.force_seal().expect("seal");
3034 }
3035 let started = Instant::now();
3036 for _ in 0..2000 {
3037 let xmin =
3038 manager.project_key_entity(black_box("history"), None, true, |entity| {
3039 entity.xmin
3040 });
3041 assert_eq!(xmin, Some(versions as u64));
3042 }
3043 eprintln!("segments={segments} versions={versions} reads=2000 elapsed_us={} resident_bytes={}", started.elapsed().as_micros(), manager.resident_bytes());
3044 }
3045 }
3046 }
3047
3048 #[test]
3049 fn ordered_fingerprint_scan_matches_legacy_sort_without_cloning_rows() {
3050 let manager = SegmentManager::new("fingerprint");
3051 let row = |id, label: &str| {
3052 UnifiedEntity::table_row(
3053 EntityId::new(id),
3054 "fingerprint",
3055 id,
3056 vec![Value::text(label)],
3057 )
3058 };
3059 manager
3060 .bulk_insert(vec![row(8, "eight"), row(2, "sealed-two"), row(5, "five")])
3061 .expect("sealed rows");
3062 manager.force_seal().expect("seal");
3063 manager
3064 .bulk_insert(vec![row(7, "seven"), row(2, "growing-two"), row(1, "one")])
3065 .expect("growing rows");
3066
3067 let mut expected = manager.query_all(|_| true);
3068 expected.sort_by_key(|entity| entity.id.raw());
3069 let expected: Vec<_> = expected
3070 .iter()
3071 .map(|entity| format!("{entity:?}"))
3072 .collect();
3073 let mut stored_addresses = HashSet::new();
3074 manager.for_each_entity(|entity| {
3075 stored_addresses.insert(entity as *const UnifiedEntity as usize);
3076 true
3077 });
3078
3079 let mut actual = Vec::new();
3080 manager.for_each_entity_in_id_order(|entity| {
3081 assert!(stored_addresses.contains(&(entity as *const UnifiedEntity as usize)));
3082 assert!(
3083 manager.growing.try_write().is_some(),
3084 "topology guard retained"
3085 );
3086 assert!(
3087 manager.sealed.try_write().is_some(),
3088 "topology guard retained"
3089 );
3090 actual.push(format!("{entity:?}"));
3091 true
3092 });
3093 assert_eq!(
3094 actual, expected,
3095 "equal IDs retain their original segment order"
3096 );
3097 }
3098
3099 #[test]
3100 fn ordered_fingerprint_scan_stops_early_and_releases_sources() {
3101 let manager = SegmentManager::new("fingerprint");
3102 for id in [3, 1, 2] {
3103 manager
3104 .insert(UnifiedEntity::table_row(
3105 EntityId::new(id),
3106 "fingerprint",
3107 id,
3108 vec![Value::Integer(id as i64)],
3109 ))
3110 .expect("row");
3111 }
3112 let source = manager.growing.read().as_ref().expect("source").clone();
3113 let handles = Arc::strong_count(&source);
3114 let mut seen = Vec::new();
3115 manager.for_each_entity_in_id_order(|entity| {
3116 seen.push(entity.id.raw());
3117 false
3118 });
3119 assert_eq!(seen, vec![1]);
3120 assert_eq!(Arc::strong_count(&source), handles);
3121 assert!(
3122 source.try_write().is_some(),
3123 "segment guard retained after early stop"
3124 );
3125 }
3126
3127 #[test]
3128 fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
3129 let manager = SegmentManager::with_config(
3130 "vectors",
3131 ManagerConfig {
3132 max_sealed_segments: 1,
3133 consolidation_entities_per_tick: 10_000,
3134 ..Default::default()
3135 },
3136 );
3137 for part in 0..3 {
3138 manager
3139 .bulk_insert(
3140 (1..=300)
3141 .map(|offset| {
3142 UnifiedEntity::vector(
3143 EntityId::new(part * 300 + offset),
3144 "vectors",
3145 vec![1.0, 0.0],
3146 )
3147 })
3148 .collect(),
3149 )
3150 .expect("batch");
3151 if part < 2 {
3152 manager.force_seal().expect("seal");
3153 }
3154 }
3155 let original_growing = manager.growing.read().as_ref().expect("growing").clone();
3156 let mut changed = false;
3157 let mut seen = Vec::new();
3158 assert!(manager.scan_batches(
3159 None,
3160 || true,
3161 |batch| {
3162 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3163 assert!(
3164 manager.growing.try_write().is_some(),
3165 "consumer holds no topology guard"
3166 );
3167 assert!(
3168 original_growing.try_write().is_some(),
3169 "consumer holds no segment guard"
3170 );
3171 for entity in batch {
3172 assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
3173 seen.push(entity.id.raw());
3174 }
3175 if !changed {
3176 changed = true;
3177 manager.force_seal().expect("seal during scan");
3178 manager
3179 .insert(UnifiedEntity::vector(
3180 EntityId::new(5000),
3181 "vectors",
3182 vec![1.0, 0.0],
3183 ))
3184 .expect("new write");
3185 drain_maintenance(&manager);
3186 assert!(manager.stats().consolidation.runs_completed > 0);
3187 assert!(manager
3188 .delete(EntityId::new(1))
3189 .expect("delete after source retirement"));
3190 assert!(manager.get(EntityId::new(1)).is_none());
3191 }
3192 true
3193 }
3194 ));
3195 seen.sort_unstable();
3196 assert_eq!(
3197 seen,
3198 (2..=900).collect::<Vec<_>>(),
3199 "no duplicate, missing or late candidates"
3200 );
3201 let mut after = Vec::new();
3202 assert!(manager.scan_batches(
3203 None,
3204 || true,
3205 |batch| {
3206 after.extend(batch.iter().map(|entity| entity.id.raw()));
3207 true
3208 }
3209 ));
3210 after.sort_unstable();
3211 let mut expected: Vec<_> = (2..=900).collect();
3212 expected.push(5000);
3213 assert_eq!(after, expected, "adopted entities have scan positions");
3214 }
3215
3216 #[test]
3217 fn bounded_scan_applies_snapshot_to_relocated_versions() {
3218 for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
3219 let manager = SegmentManager::with_config(
3220 "vectors",
3221 ManagerConfig {
3222 max_sealed_segments: 1,
3223 ..Default::default()
3224 },
3225 );
3226 for ids in [[1, 2], [3, 4]] {
3227 let entities = ids.map(|id| {
3228 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3229 entity.set_xmin(3);
3230 entity
3231 });
3232 if bulk {
3233 manager
3234 .bulk_insert(entities.into())
3235 .expect("initial versions");
3236 } else {
3237 for entity in entities {
3238 manager.insert(entity).expect("initial version");
3239 }
3240 }
3241 manager.force_seal().expect("seal source");
3242 }
3243 let snapshot = SnapshotContext {
3244 snapshot: Snapshot {
3245 xid: 10,
3246 in_progress: HashSet::new(),
3247 },
3248 manager: Arc::new(SnapshotManager::new()),
3249 own_xids: HashSet::new(),
3250 requires_index_fallback: false,
3251 serializable_reader: None,
3252 };
3253 let mut changed = false;
3254 let mut seen = Vec::new();
3255 assert!(manager.scan_batches(
3256 Some(&snapshot),
3257 || true,
3258 |batch| {
3259 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3260 if !changed {
3261 changed = true;
3262 if consolidate {
3263 drain_maintenance(&manager);
3264 assert!(manager.stats().consolidation.runs_completed > 0);
3265 }
3266 for (id, xmax) in [(3, 8), (4, 12)] {
3267 let mut entity =
3268 manager.get(EntityId::new(id)).expect("retained version");
3269 entity.set_xmax(xmax);
3270 manager
3271 .update(entity)
3272 .expect("update after source retirement");
3273 }
3274 }
3275 true
3276 }
3277 ));
3278 seen.sort_unstable();
3279 assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
3280 let mut again = Vec::new();
3281 assert!(manager.scan_batches(
3282 Some(&snapshot),
3283 || true,
3284 |batch| {
3285 again.extend(batch.iter().map(|entity| entity.id.raw()));
3286 true
3287 }
3288 ));
3289 again.sort_unstable();
3290 assert_eq!(
3291 again,
3292 vec![1, 2, 4],
3293 "old slots do not duplicate relocated versions"
3294 );
3295 let mut current = Vec::new();
3296 assert!(manager.scan_batches(
3297 None,
3298 || true,
3299 |batch| {
3300 current.extend(batch.iter().map(|entity| entity.id.raw()));
3301 true
3302 }
3303 ));
3304 current.sort_unstable();
3305 assert_eq!(
3306 current,
3307 vec![1, 2],
3308 "current view excludes both deleted versions"
3309 );
3310 }
3311 }
3312
3313 #[test]
3314 fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
3315 let manager = SegmentManager::new("mixed");
3316 let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
3317 manager
3318 .bulk_insert((1..=300).map(vector).collect())
3319 .expect("flat");
3320 manager.insert(vector(500)).expect("hash insert");
3321 manager
3322 .bulk_insert(vec![vector(502), vector(505)])
3323 .expect("gapped bulk");
3324 manager.delete(EntityId::new(2)).expect("flat delete");
3325 manager.delete(EntityId::new(505)).expect("hash delete");
3326 let mut changed = false;
3327 let mut seen = Vec::new();
3328 assert!(manager.scan_batches(
3329 None,
3330 || true,
3331 |batch| {
3332 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3333 if !changed {
3334 changed = true;
3335 manager
3336 .insert(vector(700))
3337 .expect("append after cursor boundary");
3338 manager
3339 .update(UnifiedEntity::graph_node(
3340 EntityId::new(500),
3341 "mixed",
3342 "Node",
3343 HashMap::new(),
3344 ))
3345 .expect("structural update");
3346 }
3347 true
3348 }
3349 ));
3350 seen.sort_unstable();
3351 let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
3352 expected.extend([500, 502]);
3353 assert_eq!(seen, expected);
3354 }
3355
3356 #[test]
3357 fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
3358 let manager = SegmentManager::new("vectors");
3359 let mut entities = Vec::new();
3360 for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
3361 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3362 entity.set_xmin(xmin);
3363 entity.set_xmax(xmax);
3364 entities.push(entity);
3365 }
3366 manager.bulk_insert(entities).expect("versions");
3367 manager.force_seal().expect("seal");
3368 let snapshot = SnapshotContext {
3369 snapshot: Snapshot {
3370 xid: 10,
3371 in_progress: HashSet::from([4]),
3372 },
3373 manager: Arc::new(SnapshotManager::new()),
3374 own_xids: HashSet::new(),
3375 requires_index_fallback: false,
3376 serializable_reader: None,
3377 };
3378 let mut work = 0;
3379 let mut callbacks = 0;
3380 assert!(!manager.scan_batches(
3381 Some(&snapshot),
3382 || {
3383 work += 1;
3384 work <= 2
3385 },
3386 |_| {
3387 callbacks += 1;
3388 true
3389 }
3390 ));
3391 assert_eq!(callbacks, 0, "a stopped batch is not consumed");
3392 for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
3393 let mut seen = Vec::new();
3394 assert!(manager.scan_batches(
3395 view,
3396 || true,
3397 |batch| {
3398 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3399 true
3400 }
3401 ));
3402 seen.sort_unstable();
3403 assert_eq!(seen, expected);
3404 }
3405 }
3406
3407 #[test]
3408 fn test_manager_basic() {
3409 let manager = SegmentManager::new("test_collection");
3410
3411 let entity = UnifiedEntity::table_row(
3412 manager.next_entity_id(),
3413 "users",
3414 1,
3415 vec![Value::text("Alice".to_string())],
3416 );
3417
3418 let id = manager.insert(entity).unwrap();
3419 assert!(manager.get(id).is_some());
3420 assert_eq!(manager.count(), 1);
3421 }
3422
3423 #[test]
3424 fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
3425 let manager = Arc::new(SegmentManager::new("mixed"));
3426 manager
3427 .bulk_insert(
3428 (1..=128)
3429 .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
3430 .collect(),
3431 )
3432 .expect("vector-only segment");
3433 manager.force_seal().expect("seal vectors");
3434 let mut old =
3435 UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
3436 old.set_xmin(3);
3437 old.set_xmax(12);
3438 let mut new =
3439 UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
3440 new.set_xmin(12);
3441 let edge = UnifiedEntity::graph_edge(
3442 EntityId::new(202),
3443 "mixed",
3444 "a",
3445 "b",
3446 1.0,
3447 Default::default(),
3448 );
3449 manager
3450 .bulk_insert(vec![old, new, edge])
3451 .expect("physical graph versions");
3452 manager.force_seal().expect("seal graph");
3453 manager
3454 .insert(UnifiedEntity::graph_node(
3455 EntityId::new(203),
3456 "mixed",
3457 "person",
3458 Default::default(),
3459 ))
3460 .expect("growing node");
3461 let snapshot = SnapshotContext {
3462 snapshot: Snapshot {
3463 xid: 10,
3464 in_progress: HashSet::new(),
3465 },
3466 manager: Arc::new(SnapshotManager::new()),
3467 own_xids: HashSet::new(),
3468 requires_index_fallback: false,
3469 serializable_reader: None,
3470 };
3471 std::thread::spawn(move || {
3472 for view in [None, Some(&snapshot)] {
3473 for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
3474 let expected: HashSet<_> = manager
3475 .scan(view, |item| kind.matches(&item.kind))
3476 .into_iter()
3477 .map(|item| item.id)
3478 .collect();
3479 let actual: HashSet<_> = manager
3480 .scan_graph_kind(view, kind)
3481 .into_iter()
3482 .map(|item| item.id)
3483 .collect();
3484 assert_eq!(actual, expected);
3485 let mut visited = 0;
3486 let mut candidates = HashSet::new();
3487 manager.scan_graph_candidates_for_each(
3488 view,
3489 kind,
3490 || true,
3491 |item| {
3492 visited += 1;
3493 if kind.matches(&item.kind) {
3494 candidates.insert(item.id);
3495 }
3496 true
3497 },
3498 );
3499 assert_eq!(candidates, expected);
3500 assert_eq!(
3501 visited,
3502 if matches!(kind, GraphEntityKind::Node) {
3503 3
3504 } else {
3505 2
3506 }
3507 );
3508 }
3509 }
3510 let ids: HashSet<_> = manager
3511 .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
3512 .into_iter()
3513 .map(|item| item.id.raw())
3514 .collect();
3515 assert_eq!(ids, HashSet::from([200, 203]));
3516 let ids: HashSet<_> = manager
3517 .scan_graph_kind(None, GraphEntityKind::Node)
3518 .into_iter()
3519 .map(|item| item.id.raw())
3520 .collect();
3521 assert_eq!(ids, HashSet::from([201, 203]));
3522 let mut visits = 0;
3523 manager.scan_graph_candidates_for_each(
3524 None,
3525 GraphEntityKind::Node,
3526 || true,
3527 |_| {
3528 visits += 1;
3529 false
3530 },
3531 );
3532 assert_eq!(
3533 visits, 1,
3534 "early termination must also stop subsequent segments"
3535 );
3536 })
3537 .join()
3538 .expect("snapshot worker");
3539 }
3540
3541 #[test]
3542 fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
3543 let manager = Arc::new(SegmentManager::new("accounts"));
3544 let cases = [
3545 ("legacy row", 0, 0, true),
3546 ("committed creator", 3, 0, true),
3547 ("future creator", 11, 0, false),
3548 ("in-progress creator", 4, 0, false),
3549 ("committed deleter", 3, 7, false),
3550 ("future deleter", 3, 11, true),
3551 ("in-progress deleter", 3, 4, true),
3552 ];
3553
3554 for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
3555 let row_id = index as u64 + 1;
3556 let mut entity = UnifiedEntity::table_row(
3557 EntityId::new(row_id),
3558 "accounts",
3559 row_id,
3560 vec![Value::Integer(row_id as i64)],
3561 );
3562 entity.set_xmin(*xmin);
3563 entity.set_xmax(*xmax);
3564 manager.insert(entity).unwrap();
3565 }
3566
3567 let snapshot = SnapshotContext {
3568 snapshot: Snapshot {
3569 xid: 10,
3570 in_progress: HashSet::from([4]),
3571 },
3572 manager: Arc::new(SnapshotManager::new()),
3573 own_xids: HashSet::new(),
3574 requires_index_fallback: false,
3575 serializable_reader: None,
3576 };
3577
3578 let raw_visible_row_ids: HashSet<u64> = manager
3579 .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
3580 .into_iter()
3581 .filter_map(|entity| match entity.kind {
3582 EntityKind::TableRow { row_id, .. } => Some(row_id),
3583 _ => None,
3584 })
3585 .collect();
3586
3587 let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3588 .join()
3589 .unwrap();
3590 let visible_row_ids: HashSet<u64> = scanned
3591 .into_iter()
3592 .filter_map(|entity| match entity.kind {
3593 EntityKind::TableRow { row_id, .. } => Some(row_id),
3594 _ => None,
3595 })
3596 .collect();
3597 assert_eq!(visible_row_ids, raw_visible_row_ids);
3598
3599 for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3600 let row_id = index as u64 + 1;
3601 assert_eq!(
3602 visible_row_ids.contains(&row_id),
3603 *expected_visible,
3604 "{name} visibility mismatch"
3605 );
3606 }
3607 }
3608
3609 #[test]
3610 fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3611 let manager = SegmentManager::new("accounts");
3615 let mut live =
3616 UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3617 live.set_xmin(2);
3618 live.set_xmax(0);
3619 manager.insert(live).unwrap();
3620
3621 let mut superseded =
3622 UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3623 superseded.set_xmin(2);
3624 superseded.set_xmax(9);
3625 manager.insert(superseded).unwrap();
3626
3627 let rows: Vec<u64> = manager
3628 .scan(None, |_| true)
3629 .into_iter()
3630 .filter_map(|entity| match entity.kind {
3631 EntityKind::TableRow { row_id, .. } => Some(row_id),
3632 _ => None,
3633 })
3634 .collect();
3635 assert_eq!(
3636 rows,
3637 vec![1],
3638 "frameless scan must hide superseded versions"
3639 );
3640 }
3641
3642 #[test]
3643 fn query_scan_callers_use_snapshot_api_for_visibility() {
3644 let table = include_str!("../../runtime/query_exec/table.rs");
3645 let aggregate = include_str!("../../runtime/query_exec/aggregate.rs");
3646
3647 assert_eq!(
3648 table.matches(".for_each_entity(|entity|").count(),
3649 1,
3650 "only the visibility-independent schema inspection may use the raw table iterator"
3651 );
3652 assert!(!table.contains(".query_all_zoned_with_stats("));
3653 assert!(!table.contains(".for_each_entity_zoned_with_stats("));
3654 assert!(!table.contains("TableRowMvccReadResolver::captured"));
3655
3656 assert!(!aggregate.contains("TableRowMvccReadResolver"));
3657 assert!(!aggregate.contains(".for_each_entity("));
3658 assert!(!aggregate.contains(".fold_entities_parallel("));
3659 }
3660
3661 #[test]
3662 fn remaining_visibility_scan_callers_use_snapshot_api() {
3663 let aggregate_pushdown =
3664 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs");
3665 let ask = include_str!("../../runtime/ask_pipeline.rs");
3666 let dml = include_str!("../../runtime/impl_dml.rs");
3667 let dml_targets = include_str!("../../runtime/dml_target_scan.rs");
3668 let events = include_str!("../../runtime/impl_events.rs");
3669 let graph_commands = include_str!("../../runtime/impl_graph_commands.rs");
3670 let graph_dsl = include_str!("../../runtime/graph_dsl.rs");
3671 let graph_tvf = include_str!("../../runtime/graph_tvf.rs");
3672 let kv = include_str!("../../runtime/impl_kv.rs");
3673 let queue = include_str!("../../runtime/impl_queue.rs");
3674 let primary_queue = include_str!("../../runtime/primary_queue_store.rs");
3675 let record_search = include_str!("../../runtime/record_search.rs");
3676 let search = include_str!("../../runtime/impl_search.rs");
3677 let vector = include_str!("../../runtime/query_exec/vector.rs");
3678
3679 assert!(!aggregate_pushdown.contains("TableRowMvccReadResolver"));
3680 assert!(!aggregate_pushdown.contains(".for_each_entity("));
3681 assert!(!ask.contains("for entity in manager.query_all(|_| true)"));
3682 assert!(!dml.contains("let entities = manager.query_all(|_| true);"));
3683 assert_eq!(
3688 dml_targets
3689 .matches("self.candidate_visible(snapshot.as_ref(), entity)")
3690 .count(),
3691 2,
3692 "both raw DML iterations must route through candidate_visible"
3693 );
3694 assert_eq!(
3697 events.matches("manager.query_all(").count(),
3698 1,
3699 "only the visibility-free backfill dedup may use query_all"
3700 );
3701 assert!(!graph_commands.contains("TableRowMvccReadResolver"));
3702 assert!(!graph_dsl.contains("entity_visible_with_context(snap_ctx.as_ref(), e)"));
3703 assert!(!graph_tvf.contains("entity_visible_with_context(snap_ctx.as_ref(), &entity)"));
3704 assert!(!kv.contains("TableRowMvccReadResolver"));
3705 assert!(!queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3706 assert!(!primary_queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3707 assert!(!record_search.contains("TableRowMvccReadResolver"));
3708 assert!(!search.contains("for entity in manager.query_all(|_| true)"));
3709 assert!(!vector.contains("manager.query_all("));
3710 }
3711
3712 fn production_source(src: &str) -> String {
3717 let mut kept: Vec<&str> = Vec::new();
3718 let lines: Vec<&str> = src.lines().collect();
3719 let mut idx = 0;
3720 while idx < lines.len() {
3721 let starts_test_mod = lines[idx].starts_with("#[cfg(test)]")
3722 && lines
3723 .get(idx + 1)
3724 .is_some_and(|next| next.starts_with("mod "));
3725 if !starts_test_mod {
3726 kept.push(lines[idx]);
3727 idx += 1;
3728 continue;
3729 }
3730 idx += 1;
3731 let mut depth: isize = 0;
3732 while idx < lines.len() {
3733 depth += lines[idx].matches('{').count() as isize;
3734 depth -= lines[idx].matches('}').count() as isize;
3735 idx += 1;
3736 if depth <= 0 {
3737 break;
3738 }
3739 }
3740 }
3741 kept.join("\n")
3742 }
3743
3744 fn raw_iteration_sites(src: &str) -> usize {
3746 src.matches("query_all(").count()
3747 + src.matches(".for_each_entity").count()
3748 + src.matches("fold_entities_parallel(").count()
3749 }
3750
3751 #[test]
3752 fn raw_iteration_confined_to_maintenance_allowlist() {
3753 let allowlist: &[(&str, &str, usize, &str)] = &[
3761 (
3762 "runtime/query_exec/table.rs",
3763 include_str!("../../runtime/query_exec/table.rs"),
3764 1,
3765 "documented exception: schema inspection (does the collection ever \
3766 observe these fields?) is visibility-independent",
3767 ),
3768 (
3769 "runtime/query_exec/aggregate.rs",
3770 include_str!("../../runtime/query_exec/aggregate.rs"),
3771 0,
3772 "fully migrated by batch 1",
3773 ),
3774 (
3775 "runtime/query_exec/aggregate_pushdown_dispatch.rs",
3776 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs"),
3777 0,
3778 "fully migrated by batch 1",
3779 ),
3780 (
3781 "runtime/query_exec/vector.rs",
3782 include_str!("../../runtime/query_exec/vector.rs"),
3783 0,
3784 "fully migrated by batch 2",
3785 ),
3786 (
3787 "runtime/ask_pipeline.rs",
3788 include_str!("../../runtime/ask_pipeline.rs"),
3789 0,
3790 "fully migrated by batch 2",
3791 ),
3792 (
3793 "runtime/impl_dml.rs",
3794 include_str!("../../runtime/impl_dml.rs"),
3795 1,
3796 "documented exception: ON CONFLICT resolves a unique key against \
3797 physical versions on purpose. A snapshot scan cannot see a row an \
3798 uncommitted concurrent writer already holds the key for, which is \
3799 exactly the case that must raise a serialization conflict rather \
3800 than insert a duplicate; the same pass reaps index entries left by \
3801 aborted writers. \
3802 TODO(#2263 follow-up): narrow the candidates through the unique \
3803 index instead of walking the collection",
3804 ),
3805 (
3806 "runtime/dml_target_scan.rs",
3807 include_str!("../../runtime/dml_target_scan.rs"),
3808 2,
3809 "documented exception: kind-conditional visibility the uniform scan \
3810 API cannot express; both iterations gate on candidate_visible",
3811 ),
3812 (
3813 "runtime/impl_events.rs",
3814 include_str!("../../runtime/impl_events.rs"),
3815 1,
3816 "documented exception: backfill dedup must include consumed messages, \
3817 so it is deliberately visibility-free",
3818 ),
3819 (
3820 "runtime/impl_graph_commands.rs",
3821 include_str!("../../runtime/impl_graph_commands.rs"),
3822 0,
3823 "native node properties resolve one snapshot-visible, RLS-admitted version",
3824 ),
3825 (
3826 "runtime/graph_dsl.rs",
3827 include_str!("../../runtime/graph_dsl.rs"),
3828 1,
3829 "remaining legacy debt: unused edge-property helper; search expansion \
3830 now uses snapshot visitors and RLS-gated hydration",
3831 ),
3832 (
3833 "runtime/graph_tvf.rs",
3834 include_str!("../../runtime/graph_tvf.rs"),
3835 0,
3836 "fully migrated by batch 2",
3837 ),
3838 (
3839 "runtime/impl_kv.rs",
3840 include_str!("../../runtime/impl_kv.rs"),
3841 3,
3842 "one non-versioned listing fallback (the versioned arm scans under \
3843 the captured snapshot). Point reads use physical key candidates with \
3844 versioned visibility rechecks. Vault reads use snapshot scans. Offline vault \
3845 export preserves all \
3846 physical versions across tenants; restore's emptiness check must reject \
3847 every physical row, including versions invisible to a snapshot",
3848 ),
3849 (
3850 "runtime/impl_queue.rs",
3851 include_str!("../../runtime/impl_queue.rs"),
3852 10,
3853 "maintenance: red_queue_meta bookkeeping (config, groups, acks, \
3854 pending, dedup TTL reaper, gauge counts) — queue message bodies are \
3855 read through scan. \
3856 TODO(#2138 follow-up): QUEUE PENDING projects meta rows to clients",
3857 ),
3858 (
3859 "runtime/primary_queue_store.rs",
3860 include_str!("../../runtime/primary_queue_store.rs"),
3861 3,
3862 "maintenance: generic red_queue_meta read/delete helpers and the \
3863 next-position allocator, which must not collide with rows invisible \
3864 to the writer's snapshot",
3865 ),
3866 (
3867 "runtime/record_search.rs",
3868 include_str!("../../runtime/record_search.rs"),
3869 0,
3870 "fully migrated by batch 2",
3871 ),
3872 (
3873 "runtime/search_graph.rs",
3874 include_str!("../../runtime/search_graph.rs"),
3875 0,
3876 "search graph preparation uses snapshot visitors and gated hydration",
3877 ),
3878 (
3879 "runtime/impl_search.rs",
3880 include_str!("../../runtime/impl_search.rs"),
3881 1,
3882 "maintenance: ask-audit retention sweep must see every physical row",
3883 ),
3884 ];
3885
3886 for (path, src, allowed, reason) in allowlist {
3887 let found = raw_iteration_sites(&production_source(src));
3888 assert_eq!(
3889 found, *allowed,
3890 "{path}: {found} raw-iteration sites, allowlist permits {allowed} \
3891 ({reason}). Raw iteration bypasses MVCC visibility — use \
3892 SegmentManager::scan / scan_for_each / scan_fold_parallel / \
3893 scan_zoned_with_stats, or extend the allowlist with a maintenance \
3894 justification."
3895 );
3896 }
3897 }
3898
3899 #[test]
3900 fn explicit_snapshot_scan_ignores_ambient_thread_local() {
3901 let manager = Arc::new(SegmentManager::new("accounts"));
3902 let versions = [(0, 0), (3, 0), (11, 0), (4, 0), (3, 7), (3, 11), (3, 4)];
3904 for (index, (xmin, xmax)) in versions.iter().enumerate() {
3905 let row_id = index as u64 + 1;
3906 let mut entity = UnifiedEntity::table_row(
3907 EntityId::new(row_id),
3908 "accounts",
3909 row_id,
3910 vec![Value::Integer(row_id as i64)],
3911 );
3912 entity.set_xmin(*xmin);
3913 entity.set_xmax(*xmax);
3914 manager.insert(entity).unwrap();
3915 }
3916
3917 let explicit = SnapshotContext {
3918 snapshot: Snapshot {
3919 xid: 10,
3920 in_progress: HashSet::from([4]),
3921 },
3922 manager: Arc::new(SnapshotManager::new()),
3923 own_xids: HashSet::new(),
3924 requires_index_fallback: false,
3925 serializable_reader: None,
3926 };
3927 let decoy = SnapshotContext {
3930 snapshot: Snapshot {
3931 xid: 1,
3932 in_progress: HashSet::new(),
3933 },
3934 manager: Arc::new(SnapshotManager::new()),
3935 own_xids: HashSet::new(),
3936 requires_index_fallback: false,
3937 serializable_reader: None,
3938 };
3939
3940 let row_ids = |entities: Vec<UnifiedEntity>| -> HashSet<u64> {
3941 entities
3942 .into_iter()
3943 .filter_map(|entity| match entity.kind {
3944 EntityKind::TableRow { row_id, .. } => Some(row_id),
3945 _ => None,
3946 })
3947 .collect()
3948 };
3949
3950 let guard = crate::runtime::execution_context::CurrentSnapshotGuard::install(decoy);
3951 let on_installing_thread = row_ids(manager.scan(Some(&explicit), |_| true));
3952 drop(guard);
3953
3954 let spawned_manager = Arc::clone(&manager);
3955 let spawned_snapshot = explicit.clone();
3956 let on_fresh_thread = std::thread::spawn(move || {
3957 row_ids(spawned_manager.scan(Some(&spawned_snapshot), |_| true))
3958 })
3959 .join()
3960 .unwrap();
3961
3962 assert_eq!(
3963 on_installing_thread, on_fresh_thread,
3964 "an explicit snapshot must produce the same visible set on any thread"
3965 );
3966 assert_eq!(on_installing_thread, HashSet::from([1, 2, 6, 7]));
3967
3968 let raw_manager = Arc::clone(&manager);
3971 let raw_count = std::thread::spawn(move || raw_manager.query_all(|_| true).len())
3972 .join()
3973 .unwrap();
3974 assert_eq!(
3975 raw_count,
3976 versions.len(),
3977 "raw iteration must expose every physical version"
3978 );
3979 }
3980
3981 #[test]
3982 fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3983 let manager = SegmentManager::new("test_collection");
3984
3985 let entity = UnifiedEntity::table_row(
3986 manager.next_entity_id(),
3987 "users",
3988 1,
3989 vec![Value::text("Alice".to_string())],
3990 );
3991
3992 let id = manager.insert(entity).unwrap();
3993
3994 assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3995 assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
3996 assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
3997
3998 assert!(manager.delete(id).unwrap());
3999 assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
4000 }
4001
4002 #[test]
4003 fn zoned_scan_reports_pruned_segments_and_preserves_results() {
4004 let config = ManagerConfig {
4005 segment_config: SegmentConfig {
4006 max_entities: 2,
4007 ..Default::default()
4008 },
4009 ..Default::default()
4010 };
4011 let manager = SegmentManager::with_config("events", config);
4012
4013 for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
4014 let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
4015 let entity = UnifiedEntity::new(
4016 manager.next_entity_id(),
4017 EntityKind::TableRow {
4018 table: "events".into(),
4019 row_id: (idx + 1) as u64,
4020 },
4021 EntityData::Row(row),
4022 );
4023 manager.insert(entity).unwrap();
4024 }
4025
4026 let full = manager.query_all(|entity| match &entity.data {
4027 EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
4028 _ => false,
4029 });
4030 let probe = Value::Integer(100);
4031 let (pruned, stats) =
4032 manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
4033 match &entity.data {
4034 EntityData::Row(row) => {
4035 matches!(row.get_field("ts"), Some(Value::Integer(100)))
4036 }
4037 _ => false,
4038 }
4039 });
4040
4041 assert_eq!(pruned.len(), full.len());
4042 assert_eq!(pruned[0].id, full[0].id);
4043 assert_eq!(stats.segments_total, 3);
4044 assert_eq!(stats.segments_scanned, 2);
4045 assert_eq!(stats.segments_pruned, 1);
4046 }
4047
4048 #[test]
4049 fn test_manager_auto_seal() {
4050 let config = ManagerConfig {
4051 segment_config: SegmentConfig {
4052 max_entities: 2,
4053 ..Default::default()
4054 },
4055 ..Default::default()
4056 };
4057
4058 let manager = SegmentManager::with_config("test", config);
4059
4060 manager
4062 .insert(UnifiedEntity::vector(
4063 manager.next_entity_id(),
4064 "v",
4065 vec![0.1],
4066 ))
4067 .unwrap();
4068
4069 manager
4071 .insert(UnifiedEntity::vector(
4072 manager.next_entity_id(),
4073 "v",
4074 vec![0.2],
4075 ))
4076 .unwrap();
4077
4078 manager
4080 .insert(UnifiedEntity::vector(
4081 manager.next_entity_id(),
4082 "v",
4083 vec![0.3],
4084 ))
4085 .unwrap();
4086
4087 let stats = manager.stats();
4088 assert_eq!(stats.total_entities, 3);
4089 }
4090
4091 #[test]
4092 fn test_manager_delete() {
4093 let manager = SegmentManager::new("test");
4094
4095 let id = manager
4096 .insert(UnifiedEntity::vector(
4097 manager.next_entity_id(),
4098 "v",
4099 vec![0.1],
4100 ))
4101 .unwrap();
4102
4103 assert!(manager.get(id).is_some());
4104 assert!(manager.delete(id).unwrap());
4105 assert!(manager.get(id).is_none());
4106 }
4107
4108 #[test]
4109 fn test_manager_metadata() {
4110 let manager = SegmentManager::new("test");
4111
4112 let id = manager
4113 .insert(UnifiedEntity::table_row(
4114 manager.next_entity_id(),
4115 "hosts",
4116 1,
4117 vec![Value::text("192.168.1.1".to_string())],
4118 ))
4119 .unwrap();
4120
4121 let mut meta = Metadata::new();
4122 meta.set(
4123 "os",
4124 super::super::metadata::MetadataValue::String("linux".to_string()),
4125 );
4126
4127 manager.set_metadata(id, meta).unwrap();
4128
4129 let retrieved = manager.get_metadata(id).unwrap();
4130 assert!(retrieved.has("os"));
4131 }
4132
4133 #[test]
4134 fn test_manager_query_by_kind() {
4135 let manager = SegmentManager::new("test");
4136
4137 manager
4138 .insert(UnifiedEntity::table_row(
4139 manager.next_entity_id(),
4140 "hosts",
4141 1,
4142 vec![],
4143 ))
4144 .unwrap();
4145
4146 manager
4147 .insert(UnifiedEntity::vector(
4148 manager.next_entity_id(),
4149 "embeddings",
4150 vec![0.1],
4151 ))
4152 .unwrap();
4153
4154 manager
4155 .insert(UnifiedEntity::table_row(
4156 manager.next_entity_id(),
4157 "hosts",
4158 2,
4159 vec![],
4160 ))
4161 .unwrap();
4162
4163 let rows = manager.get_by_kind("table");
4164 assert_eq!(rows.len(), 2);
4165
4166 let vectors = manager.get_by_kind("vector");
4167 assert_eq!(vectors.len(), 1);
4168 }
4169
4170 fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
4177 SegmentManager::with_config(
4178 "test",
4179 ManagerConfig {
4180 max_sealed_segments: 1_000,
4181 consolidation_entities_per_tick: entities_per_tick,
4182 ..Default::default()
4183 },
4184 )
4185 }
4186
4187 fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
4191 let entities: Vec<UnifiedEntity> = (0..count)
4192 .map(|i| {
4193 UnifiedEntity::table_row(
4194 manager.next_entity_id(),
4195 "users",
4196 i as u64,
4197 vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
4198 )
4199 })
4200 .collect();
4201 let ids = manager.bulk_insert(entities).expect("bulk insert");
4202 manager.force_seal().expect("seal");
4203 ids
4204 }
4205
4206 fn live_ids(manager: &SegmentManager) -> Vec<u64> {
4207 let mut ids: Vec<u64> = manager
4208 .query_all(|_| true)
4209 .into_iter()
4210 .map(|entity| entity.id.raw())
4211 .collect();
4212 ids.sort_unstable();
4213 ids
4214 }
4215
4216 fn drain_maintenance(manager: &SegmentManager) -> usize {
4217 let mut ticks = 0;
4218 while manager.consolidation.read().is_some() || ticks == 0 {
4219 manager.run_maintenance().expect("maintenance");
4220 ticks += 1;
4221 assert!(ticks < 1_000, "consolidation failed to converge");
4222 }
4223 ticks
4224 }
4225
4226 #[test]
4227 fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
4228 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4229 let ids = manager
4230 .bulk_insert(
4231 (1..=1024)
4232 .map(|id| {
4233 UnifiedEntity::graph_edge(
4234 EntityId::new(id),
4235 "step",
4236 "2000",
4237 "3000",
4238 1.0,
4239 HashMap::new(),
4240 )
4241 })
4242 .collect(),
4243 )
4244 .expect("edges");
4245 manager.force_seal().expect("seal");
4246 for id in ids.iter().take(300) {
4247 manager.delete(*id).expect("fragment source");
4248 }
4249 let source = Arc::downgrade(&manager.sealed.read()[0]);
4250 let mut seen = Vec::new();
4251 let mut changed = false;
4252 let mut metadata_bytes = 0;
4253 assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
4254 || true,
4255 |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
4256 |batch| {
4257 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
4258 assert!(manager.growing.try_write().is_some(), "no growing topology guard");
4259 assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
4260 assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
4261 if !changed {
4262 changed = true;
4263 drain_maintenance(&manager);
4264 assert!(manager.stats().consolidation.runs_completed > 0);
4265 assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
4266 let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
4267 if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
4268 manager.update(updated).expect("update authoritative merged copy");
4269 }
4270 for id in batch {
4271 let entity = manager.get(*id).expect("hydrate current manager");
4272 if id.raw() == 1024 {
4273 assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
4274 }
4275 seen.push(id.raw());
4276 }
4277 Ok(true)
4278 }).expect("cursor"));
4279 assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
4280 assert!(
4281 source.upgrade().is_none(),
4282 "finished cursor releases retired source"
4283 );
4284 assert!(
4285 metadata_bytes < 4096,
4286 "fixed ID buffer plus source handles: {metadata_bytes}"
4287 );
4288 }
4289
4290 #[test]
4291 fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
4292 let manager = SegmentManager::new("graph");
4293 manager
4294 .bulk_insert(
4295 (1..=1024)
4296 .map(|id| {
4297 UnifiedEntity::graph_edge(
4298 EntityId::new(id),
4299 "step",
4300 "2000",
4301 "3000",
4302 1.0,
4303 HashMap::new(),
4304 )
4305 })
4306 .collect(),
4307 )
4308 .expect("edges");
4309 let mut delivered = 0;
4310 let denied = manager.visit_graph_index_batches(
4311 GraphEntityKind::Edge,
4312 &[EntityId::new(2000)],
4313 || true,
4314 |_| Err("out of memory"),
4315 |ids| {
4316 delivered += ids.len();
4317 Ok(true)
4318 },
4319 );
4320 assert_eq!(denied, Err("out of memory"));
4321 assert_eq!(delivered, 0);
4322 let mut work = 0;
4323 assert!(!manager
4324 .visit_graph_index_batches(
4325 GraphEntityKind::Edge,
4326 &[EntityId::new(2000)],
4327 || {
4328 work += 1;
4329 work < 100
4330 },
4331 |_| Ok::<_, ()>(()),
4332 |ids| {
4333 delivered += ids.len();
4334 Ok(true)
4335 }
4336 )
4337 .expect("cancelled cursor"));
4338 assert_eq!(
4339 delivered, 0,
4340 "cancelled partial batch cannot reach consumer"
4341 );
4342 assert!(!manager
4343 .visit_graph_index_batches(
4344 GraphEntityKind::Edge,
4345 &[EntityId::new(2000)],
4346 || true,
4347 |_| Ok::<_, ()>(()),
4348 |ids| {
4349 delivered += ids.len();
4350 Ok(false)
4351 }
4352 )
4353 .expect("consumer stop"));
4354 assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
4355 }
4356
4357 #[test]
4358 fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
4359 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4360 let ids = seal_bulk_segment(&manager, 100);
4361 for id in ids.iter().take(40) {
4362 manager.delete(*id).expect("delete before scan");
4363 }
4364 let source_bytes = manager.resident_bytes();
4365 let source = Arc::downgrade(&manager.sealed.read()[0]);
4366 std::thread::scope(|scope| {
4367 let (ready, waiting) = std::sync::mpsc::channel();
4368 let (release_first, first_wait) = std::sync::mpsc::channel();
4369 let (release_last, last_wait) = std::sync::mpsc::channel();
4370 let reader = |resume: std::sync::mpsc::Receiver<()>,
4371 ready: std::sync::mpsc::Sender<()>| {
4372 assert!(manager.scan_batches(
4373 None,
4374 || true,
4375 |_| {
4376 ready.send(()).expect("signal captured cursor");
4377 let _ = resume.recv();
4378 true
4379 }
4380 ));
4381 };
4382 let ready_first = ready.clone();
4383 let first = scope.spawn(move || reader(first_wait, ready_first));
4384 let last = scope.spawn(move || reader(last_wait, ready));
4385 for _ in 0..2 {
4386 waiting
4387 .recv_timeout(std::time::Duration::from_secs(10))
4388 .expect("reader ready");
4389 }
4390 drain_maintenance(&manager);
4391 assert!(
4392 source.upgrade().is_some(),
4393 "both cursors pin the retired source"
4394 );
4395 let active_bytes = manager.sealed.read()[0].read().resident_bytes();
4396 let held = manager.resident_bytes();
4397 assert!(
4398 held >= active_bytes + source_bytes,
4399 "retired source remains resident"
4400 );
4401 release_first.send(()).expect("release first reader");
4402 first.join().expect("first reader");
4403 assert_eq!(
4404 manager.resident_bytes(),
4405 held,
4406 "one reader still pins the source"
4407 );
4408 release_last.send(()).expect("release last reader");
4409 last.join().expect("last reader");
4410 assert!(
4411 source.upgrade().is_none(),
4412 "accounting must not retain payloads"
4413 );
4414 assert_eq!(held - manager.resident_bytes(), source_bytes);
4415 assert_eq!(
4416 manager.stats().total_memory_bytes as u64,
4417 manager.resident_bytes()
4418 );
4419 });
4420 }
4421
4422 #[test]
4423 fn retained_memory_gates_runtime_growth_until_the_cursor_is_cancelled() {
4424 use crate::storage::memory_pools::MemoryPool;
4425 use crate::{RedDBOptions, RedDBRuntime};
4426
4427 let budget = 128 * 1024;
4428 let runtime =
4429 RedDBRuntime::with_options(RedDBOptions::in_memory().with_memory_budget(budget))
4430 .expect("runtime");
4431 runtime
4432 .db()
4433 .store()
4434 .create_collection("test")
4435 .expect("collection");
4436 let manager = runtime
4437 .db()
4438 .store()
4439 .get_collection("test")
4440 .expect("manager");
4441 let ids = seal_bulk_segment(&manager, 100);
4442 for id in ids.iter().take(40) {
4443 manager.delete(*id).expect("delete before scan");
4444 }
4445 let source_bytes = manager.resident_bytes();
4446 let mut growth = 0;
4447 let mut held_used = 0;
4448 assert!(!manager.scan_batches(
4449 None,
4450 || true,
4451 |_| {
4452 drain_maintenance(&manager);
4453 runtime.refresh_memory_accounting();
4454 held_used = runtime.memory_accounting().total_used_bytes();
4457 growth = budget
4458 .checked_sub(held_used)
4459 .expect("fixture fits while the cursor is open")
4460 + source_bytes / 2;
4461 assert!(growth > 0);
4462 assert!(
4463 runtime
4464 .admit_non_evictable_growth(
4465 MemoryPool::SegmentArena,
4466 "growth with pinned source",
4467 growth,
4468 )
4469 .is_err(),
4470 "retained source must participate in runtime admission"
4471 );
4472 false
4473 }
4474 ));
4475 runtime.refresh_memory_accounting();
4476 assert_eq!(
4477 held_used - runtime.memory_accounting().total_used_bytes(),
4478 source_bytes
4479 );
4480 let _reservation = runtime
4481 .admit_non_evictable_growth(
4482 MemoryPool::SegmentArena,
4483 "growth after cursor cancellation",
4484 growth,
4485 )
4486 .expect("the same growth fits once the cursor releases its source");
4487 }
4488
4489 #[test]
4490 fn retained_memory_includes_an_inflight_consolidation_copy() {
4491 let manager = consolidating_manager(1);
4492 let ids = seal_bulk_segment(&manager, 100);
4493 for id in ids.iter().take(40) {
4494 manager.delete(*id).expect("delete");
4495 }
4496 let before = manager.resident_bytes();
4497 assert!(manager.pressure_consolidation_tick());
4498 let copied = manager
4499 .consolidation
4500 .read()
4501 .as_ref()
4502 .expect("paced merge")
4503 .merged
4504 .resident_bytes();
4505 assert!(copied > 0);
4506 assert!(
4507 manager.resident_bytes() >= before + copied,
4508 "unpublished copy consumes memory"
4509 );
4510 drain_maintenance(&manager);
4511 assert!(
4512 manager.resident_bytes() < before,
4513 "completed merge releases the old storage"
4514 );
4515 }
4516
4517 #[test]
4518 fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
4519 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4520 let ids = seal_bulk_segment(&manager, 100);
4521
4522 let bytes_before_delete = manager.resident_bytes();
4523
4524 for id in ids.iter().take(40) {
4526 assert!(manager.delete(*id).expect("delete"));
4527 }
4528
4529 let bytes_with_tombstones = manager.resident_bytes();
4532 assert!(bytes_with_tombstones > bytes_before_delete);
4533
4534 let survivors = live_ids(&manager);
4535 assert_eq!(survivors.len(), 60);
4536
4537 drain_maintenance(&manager);
4538
4539 let stats = manager.stats();
4540 assert_eq!(stats.consolidation.runs_started, 1);
4541 assert_eq!(stats.consolidation.runs_completed, 1);
4542 assert_eq!(stats.consolidation.segments_merged, 1);
4543 assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
4544 assert!(stats.consolidation.bytes_reclaimed > 0);
4545
4546 let bytes_after = manager.resident_bytes();
4548 assert!(
4549 bytes_after <= bytes_before_delete,
4550 "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
4551 );
4552 assert_eq!(
4553 bytes_with_tombstones - bytes_after,
4554 stats.consolidation.bytes_reclaimed
4555 );
4556 assert_eq!(stats.total_memory_bytes, bytes_after as usize);
4557
4558 assert_eq!(live_ids(&manager), survivors);
4559 assert_eq!(manager.sealed.read().len(), 1);
4560 assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
4561 }
4562
4563 #[test]
4564 fn query_results_are_identical_before_during_and_after_consolidation() {
4565 let manager = consolidating_manager(3);
4566 let ids = seal_bulk_segment(&manager, 20);
4567 seal_bulk_segment(&manager, 20);
4568 for id in ids.iter().take(10) {
4569 assert!(manager.delete(*id).expect("delete"));
4570 }
4571
4572 let before = live_ids(&manager);
4573 let survivor = *ids.last().expect("at least one row");
4574 let tombstoned = ids[0];
4575
4576 let mut mid_tick_observations = 0;
4579 while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
4580 manager.run_maintenance().expect("maintenance");
4581 assert_eq!(live_ids(&manager), before);
4582 assert!(manager.get(survivor).is_some());
4583 assert!(manager.get(tombstoned).is_none());
4584 assert_eq!(manager.get_by_kind("table").len(), before.len());
4585 mid_tick_observations += 1;
4586 assert!(mid_tick_observations < 1_000);
4587 }
4588
4589 assert!(
4590 mid_tick_observations > 2,
4591 "expected the run to span several ticks, saw {mid_tick_observations}"
4592 );
4593 assert_eq!(live_ids(&manager), before);
4594 assert!(manager.get(survivor).is_some());
4595 assert!(manager.get(tombstoned).is_none());
4596 assert_eq!(manager.get_by_kind("table").len(), before.len());
4597 }
4598
4599 #[test]
4600 fn get_by_kind_reaches_sealed_segments() {
4601 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4602 let ids = seal_bulk_segment(&manager, 5);
4603 assert!(manager.delete(ids[0]).expect("delete"));
4604
4605 assert_eq!(manager.get_by_kind("table").len(), 4);
4609 assert_eq!(manager.get_by_kind("vector").len(), 0);
4610 }
4611
4612 #[test]
4613 fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4614 const PER_TICK: usize = 4;
4615 let manager = consolidating_manager(PER_TICK);
4616 let ids = seal_bulk_segment(&manager, 60);
4617 for id in ids.iter().take(30) {
4618 assert!(manager.delete(*id).expect("delete"));
4619 }
4620
4621 manager.run_maintenance().expect("maintenance");
4623 assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4624
4625 let mut previous = PER_TICK as u64;
4628 let mut ticks = 1;
4629 while let Some(copied) = manager.consolidation_progress() {
4630 assert!(
4631 copied - previous <= PER_TICK as u64,
4632 "tick copied {} entities, bound is {PER_TICK}",
4633 copied - previous
4634 );
4635 previous = copied;
4636 manager.run_maintenance().expect("maintenance");
4637 ticks += 1;
4638 assert!(ticks < 100);
4639 }
4640
4641 assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4644 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4645 assert_eq!(live_ids(&manager).len(), 30);
4646 }
4647
4648 #[test]
4649 fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4650 let manager = SegmentManager::with_config(
4651 "test",
4652 ManagerConfig {
4653 max_sealed_segments: 3,
4654 ..Default::default()
4655 },
4656 );
4657 for _ in 0..4 {
4658 seal_bulk_segment(&manager, 5);
4659 }
4660 assert_eq!(manager.sealed.read().len(), 4);
4661
4662 let before = live_ids(&manager);
4663 drain_maintenance(&manager);
4664
4665 assert_eq!(manager.sealed.read().len(), 1);
4668 assert_eq!(manager.stats().consolidation.segments_merged, 4);
4669 assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4670 assert_eq!(live_ids(&manager), before);
4671 }
4672
4673 #[test]
4674 fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4675 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4676 seal_bulk_segment(&manager, 10);
4677 seal_bulk_segment(&manager, 10);
4678
4679 for _ in 0..5 {
4680 manager.run_maintenance().expect("maintenance");
4681 }
4682
4683 assert_eq!(manager.stats().consolidation.runs_started, 0);
4684 assert_eq!(manager.sealed.read().len(), 2);
4685 }
4686
4687 #[test]
4688 fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4689 use std::sync::atomic::AtomicBool;
4690
4691 let manager = Arc::new(consolidating_manager(2));
4692 let ids = seal_bulk_segment(&manager, 40);
4693 seal_bulk_segment(&manager, 40);
4694 for id in ids.iter().take(20) {
4695 assert!(manager.delete(*id).expect("delete"));
4696 }
4697
4698 let expected = live_ids(&manager);
4699 let stop = Arc::new(AtomicBool::new(false));
4700
4701 let readers: Vec<_> = (0..4)
4702 .map(|_| {
4703 let manager = Arc::clone(&manager);
4704 let stop = Arc::clone(&stop);
4705 let expected = expected.clone();
4706 std::thread::spawn(move || {
4707 let mut scans = 0u64;
4708 loop {
4711 let mut seen: Vec<u64> = manager
4712 .query_all(|_| true)
4713 .into_iter()
4714 .map(|entity| entity.id.raw())
4715 .collect();
4716 seen.sort_unstable();
4717 assert_eq!(seen, expected, "reader saw a torn sealed set");
4720 scans += 1;
4721 if stop.load(Ordering::Relaxed) {
4722 break;
4723 }
4724 }
4725 scans
4726 })
4727 })
4728 .collect();
4729
4730 drain_maintenance(&manager);
4731 stop.store(true, Ordering::Relaxed);
4732
4733 for reader in readers {
4734 let scans = reader.join().expect("reader thread");
4735 assert!(scans > 0, "reader never got to scan");
4736 }
4737
4738 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4739 assert_eq!(live_ids(&manager), expected);
4740 }
4741
4742 #[test]
4743 fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4744 let manager = consolidating_manager(4);
4745 let ids = seal_bulk_segment(&manager, 20);
4746 for id in ids.iter().take(6) {
4747 assert!(manager.delete(*id).expect("delete"));
4748 }
4749
4750 manager.run_maintenance().expect("maintenance");
4752 assert_eq!(manager.consolidation_progress(), Some(4));
4753 let doomed = ids[6];
4754 assert!(manager.delete(doomed).expect("delete"));
4755
4756 let expected = live_ids(&manager);
4757 drain_maintenance(&manager);
4758
4759 assert!(manager.get(doomed).is_none(), "deleted row came back");
4760 assert_eq!(live_ids(&manager), expected);
4761 }
4762
4763 #[test]
4764 fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
4765 let manager = consolidating_manager(3);
4766 let ids = seal_bulk_segment(&manager, 30);
4767 for id in ids.iter().take(12) {
4768 assert!(manager.delete(*id).expect("delete"));
4769 }
4770
4771 let before = live_ids(&manager);
4772 let sealed_before: Vec<SegmentId> = manager
4773 .sealed
4774 .read()
4775 .iter()
4776 .map(|segment| segment.read().id())
4777 .collect();
4778
4779 manager.run_maintenance().expect("maintenance");
4780 manager.run_maintenance().expect("maintenance");
4781 assert!(manager.consolidation.read().is_some());
4782
4783 *manager.consolidation.write() = None;
4787
4788 assert_eq!(live_ids(&manager), before);
4789 let sealed_after: Vec<SegmentId> = manager
4790 .sealed
4791 .read()
4792 .iter()
4793 .map(|segment| segment.read().id())
4794 .collect();
4795 assert_eq!(sealed_after, sealed_before);
4796 assert_eq!(manager.stats().consolidation.runs_completed, 0);
4797
4798 drain_maintenance(&manager);
4799 assert_eq!(live_ids(&manager), before);
4800 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4801 }
4802
4803 #[test]
4804 fn consolidation_preserves_entity_payload_metadata_and_sequence() {
4805 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4806
4807 let entities: Vec<UnifiedEntity> = (0..10)
4811 .map(|i| {
4812 UnifiedEntity::table_row(
4813 manager.next_entity_id(),
4814 "users",
4815 i as u64,
4816 vec![Value::Integer(i as i64)],
4817 )
4818 })
4819 .collect();
4820 let ids = manager.bulk_insert(entities).expect("bulk insert");
4821
4822 let mut meta = Metadata::new();
4823 meta.set(
4824 "os",
4825 super::super::metadata::MetadataValue::String("linux".to_string()),
4826 );
4827 manager.set_metadata(ids[9], meta).expect("set metadata");
4828 manager.force_seal().expect("seal");
4829
4830 assert!(manager.delete(ids[0]).expect("delete"));
4831 assert!(manager.delete(ids[1]).expect("delete"));
4832 assert!(manager.delete(ids[2]).expect("delete"));
4833
4834 let before: Vec<UnifiedEntity> = {
4835 let mut entities = manager.query_all(|_| true);
4836 entities.sort_by_key(|entity| entity.id.raw());
4837 entities
4838 };
4839
4840 drain_maintenance(&manager);
4841
4842 let after: Vec<UnifiedEntity> = {
4843 let mut entities = manager.query_all(|_| true);
4844 entities.sort_by_key(|entity| entity.id.raw());
4845 entities
4846 };
4847
4848 let columns = |entity: &UnifiedEntity| match &entity.data {
4849 super::super::entity::EntityData::Row(row) => row.columns.clone(),
4850 _ => Vec::new(),
4851 };
4852
4853 assert_eq!(before.len(), after.len());
4854 for (old, new) in before.iter().zip(after.iter()) {
4855 assert_eq!(old.id, new.id);
4856 assert_eq!(columns(old), columns(new));
4857 assert_eq!(
4858 old.sequence_id, new.sequence_id,
4859 "merge must not re-issue sequence ids"
4860 );
4861 }
4862 assert!(manager
4863 .get_metadata(ids[9])
4864 .expect("metadata survives the merge")
4865 .has("os"));
4866 }
4867
4868 #[test]
4869 #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
4870 fn test_lifecycle_events() {
4871 let manager = SegmentManager::new("test");
4872
4873 manager
4874 .insert(UnifiedEntity::vector(
4875 manager.next_entity_id(),
4876 "v",
4877 vec![0.1],
4878 ))
4879 .unwrap();
4880
4881 let events = manager.drain_events();
4882
4883 assert!(events
4885 .iter()
4886 .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
4887 assert!(events
4888 .iter()
4889 .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
4890 }
4891}