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_preserving_sequence(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 let previous: HashMap<_, _> = before.iter().copied().collect();
1515 if previous.len() != before.len() {
1516 return None;
1517 }
1518 let mut retained_sources = 0;
1519 let mut changed = std::collections::BTreeSet::new();
1520 for segment in growing.iter().chain(sealed.iter()) {
1521 let segment = segment.read();
1522 let revision = previous.get(&segment.id()).copied();
1523 retained_sources += usize::from(revision.is_some());
1524 let revision = revision.unwrap_or(0);
1527 changed.extend(segment.checkpoint_mutations_since(revision)?);
1528 if changed.len() > super::segment::CHECKPOINT_MUTATIONS_MAX {
1529 return None;
1530 }
1531 }
1532 (retained_sources == previous.len()).then(|| changed.into_iter().collect())
1535 }
1536
1537 pub(crate) fn checkpoint_tracking_bytes(&self) -> usize {
1538 let growing = self.growing.read();
1539 let sealed = self.sealed.read();
1540 growing
1541 .iter()
1542 .chain(sealed.iter())
1543 .map(|segment| segment.read().checkpoint_tracking_bytes())
1544 .sum()
1545 }
1546
1547 pub(crate) fn cached_physical_root(&self, seed: u64, compute: impl FnOnce() -> u64) -> u64 {
1549 let segments = self.checkpoint_revisions();
1550 let mut cache = self.physical_root_cache.lock();
1551 if let Some(previous) = cache.as_ref() {
1552 if previous.seed == seed && previous.segments == segments {
1553 return previous.root;
1554 }
1555 }
1556 let root = compute();
1557 *cache = Some(PhysicalRootCacheEntry {
1558 segments,
1559 seed,
1560 root,
1561 });
1562 root
1563 }
1564
1565 pub(crate) fn for_each_entity_in_id_order(
1570 &self,
1571 mut visit: impl FnMut(&UnifiedEntity) -> bool,
1572 ) {
1573 self.for_each_entity_source_in_id_order(|_, entity| visit(entity));
1574 }
1575
1576 pub(crate) fn page_in_id_order(
1578 &self,
1579 offset: usize,
1580 limit: usize,
1581 ) -> (usize, Vec<UnifiedEntity>) {
1582 let end = offset.saturating_add(limit);
1583 let mut total = 0;
1584 let mut items = Vec::with_capacity(limit.min(self.count()));
1585 self.for_each_entity_in_id_order(|entity| {
1586 if total >= offset && total < end {
1587 items.push(entity.clone());
1588 }
1589 total += 1;
1590 true
1591 });
1592 self.restore_owned_row_schemas(items.iter_mut());
1593 (total, items)
1594 }
1595
1596 pub(crate) fn for_each_entity_with_metadata_in_id_order(
1597 &self,
1598 mut visit: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1599 ) {
1600 self.for_each_entity_source_in_id_order(|segment, entity| {
1601 visit(entity, segment.get_metadata(entity.id))
1602 });
1603 }
1604
1605 fn for_each_entity_source_in_id_order(
1606 &self,
1607 mut visit: impl FnMut(&GrowingSegment, &UnifiedEntity) -> bool,
1608 ) {
1609 let segments: Vec<_> = {
1610 let growing = self.growing.read();
1611 let sealed = self.sealed.read();
1612 growing.iter().chain(sealed.iter()).cloned().collect()
1613 };
1614 let mut order = Vec::new();
1615 for (source, segment) in segments.iter().enumerate() {
1616 segment.read().for_each_fast(|entity| {
1617 order.push((entity.id, source));
1618 true
1619 });
1620 }
1621 order.sort_by_key(|(id, _)| id.raw());
1622 for (id, source) in order {
1623 let segment = segments[source].read();
1624 if let Some(entity) = segment.get(id) {
1625 if !visit(&segment, entity) {
1626 break;
1627 }
1628 }
1629 }
1630 }
1631
1632 pub(crate) fn for_each_entity_with_metadata(
1635 &self,
1636 mut callback: impl FnMut(&UnifiedEntity, Option<Metadata>) -> bool,
1637 ) {
1638 if let Some(growing_arc) = self.growing.read().as_ref() {
1639 let growing = growing_arc.read();
1640 if !growing.for_each_fast(|entity| callback(entity, growing.get_metadata(entity.id))) {
1641 return;
1642 }
1643 }
1644 let sealed = self.sealed.read();
1645 for segment_arc in sealed.iter() {
1646 let segment = segment_arc.read();
1647 if !segment.for_each_fast(|entity| callback(entity, segment.get_metadata(entity.id))) {
1648 return;
1649 }
1650 }
1651 }
1652
1653 fn for_each_entity_in_segments<F>(
1654 &self,
1655 segment_filter: impl Fn(&GrowingSegment) -> bool,
1656 mut before_segment: impl FnMut() -> bool,
1657 mut callback: F,
1658 ) where
1659 F: FnMut(&UnifiedEntity) -> bool,
1660 {
1661 if let Some(growing_arc) = self.growing.read().as_ref() {
1665 if !before_segment() {
1666 return;
1667 }
1668 let growing = if let Some(g) = growing_arc.try_read() {
1669 g
1670 } else {
1671 growing_arc.read()
1672 };
1673 if segment_filter(&growing) && !growing.for_each_fast(&mut callback) {
1674 return;
1675 }
1676 }
1677
1678 let sealed = self.sealed.read();
1680 for segment_arc in sealed.iter() {
1681 if !before_segment() {
1682 return;
1683 }
1684 let segment = segment_arc.read();
1685 if segment_filter(&segment) && !segment.for_each_fast(&mut callback) {
1686 return;
1687 }
1688 }
1689 }
1690
1691 pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1707 &self,
1708 init: FInit,
1709 fold: FFold,
1710 reduce: FReduce,
1711 ) -> T
1712 where
1713 T: Send,
1714 FInit: Fn() -> T + Send + Sync,
1715 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1716 FReduce: Fn(T, T) -> T + Send + Sync,
1717 {
1718 use rayon::prelude::*;
1719
1720 let mut acc = init();
1723 if let Some(growing_arc) = self.growing.read().as_ref() {
1724 let growing = if let Some(g) = growing_arc.try_read() {
1725 g
1726 } else {
1727 growing_arc.read()
1728 };
1729 growing.for_each_fast(|entity| {
1730 acc = fold(std::mem::replace(&mut acc, init()), entity);
1731 true
1732 });
1733 }
1734
1735 let segments: Vec<_> = {
1739 let sealed = self.sealed.read();
1740 sealed.iter().cloned().collect()
1741 };
1742
1743 if segments.len() <= 1 {
1744 for seg_arc in &segments {
1745 let seg = seg_arc.read();
1746 seg.for_each_fast(|entity| {
1747 acc = fold(std::mem::replace(&mut acc, init()), entity);
1748 true
1749 });
1750 }
1751 return acc;
1752 }
1753
1754 let sealed_acc = segments
1755 .into_par_iter()
1756 .map(|seg_arc| {
1757 let mut local = init();
1758 let seg = seg_arc.read();
1759 seg.for_each_fast(|entity| {
1760 local = fold(std::mem::replace(&mut local, init()), entity);
1761 true
1762 });
1763 local
1764 })
1765 .reduce(&init, &reduce);
1766
1767 reduce(acc, sealed_acc)
1768 }
1769
1770 pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1779 where
1780 F: FnMut(&UnifiedEntity) -> bool,
1781 {
1782 let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1783 }
1784
1785 pub fn for_each_entity_zoned_with_stats<F>(
1786 &self,
1787 zone_preds: &[(&str, ZoneColPred<'_>)],
1788 mut callback: F,
1789 ) -> SegmentScanStats
1790 where
1791 F: FnMut(&UnifiedEntity) -> bool,
1792 {
1793 let mut scan_stats = SegmentScanStats::default();
1794 if let Some(growing_arc) = self.growing.read().as_ref() {
1800 scan_stats.segments_total += 1;
1801 scan_stats.segments_scanned += 1;
1802 let growing = if let Some(g) = growing_arc.try_read() {
1803 g
1804 } else {
1805 growing_arc.read()
1806 };
1807 if !growing.for_each_fast(&mut callback) {
1808 return scan_stats;
1809 }
1810 }
1811
1812 let sealed = self.sealed.read();
1814 for segment_arc in sealed.iter() {
1815 scan_stats.segments_total += 1;
1816 let segment = segment_arc.read();
1817 if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1818 scan_stats.segments_pruned += 1;
1819 continue; }
1821 scan_stats.segments_scanned += 1;
1822 if !segment.for_each_fast(&mut callback) {
1823 return scan_stats;
1824 }
1825 }
1826 scan_stats
1827 }
1828
1829 pub fn query_all_zoned<F>(
1839 &self,
1840 zone_preds: &[(&str, ZoneColPred<'_>)],
1841 filter: F,
1842 ) -> Vec<UnifiedEntity>
1843 where
1844 F: Fn(&UnifiedEntity) -> bool + Sync,
1845 {
1846 self.query_all_zoned_with_stats(zone_preds, filter).0
1847 }
1848
1849 pub fn query_all_zoned_with_stats<F>(
1850 &self,
1851 zone_preds: &[(&str, ZoneColPred<'_>)],
1852 filter: F,
1853 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1854 where
1855 F: Fn(&UnifiedEntity) -> bool + Sync,
1856 {
1857 let mut results = Vec::new();
1858 let mut scan_stats = SegmentScanStats::default();
1859
1860 if let Some(growing_arc) = self.growing.read().as_ref() {
1863 scan_stats.segments_total += 1;
1864 scan_stats.segments_scanned += 1;
1865 let growing = if let Some(g) = growing_arc.try_read() {
1866 g
1867 } else {
1868 growing_arc.read()
1869 };
1870 results.extend(growing.iter().filter(|e| filter(e)).cloned());
1871 }
1872
1873 let sealed = self.sealed.read();
1875 let surviving: Vec<_> = sealed
1877 .iter()
1878 .filter(|seg_arc| {
1879 scan_stats.segments_total += 1;
1880 if zone_preds.is_empty() {
1881 scan_stats.segments_scanned += 1;
1882 return true;
1883 }
1884 let seg = seg_arc.read();
1885 if seg.can_skip_zone_preds(zone_preds) {
1886 scan_stats.segments_pruned += 1;
1887 false
1888 } else {
1889 scan_stats.segments_scanned += 1;
1890 true
1891 }
1892 })
1893 .collect();
1894
1895 let use_parallel = surviving.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1896
1897 if use_parallel {
1898 let filter_ref = &filter;
1899 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1900 surviving
1901 .iter()
1902 .map(|segment| {
1903 s.spawn(move || {
1904 segment
1905 .read()
1906 .iter()
1907 .filter(|e| filter_ref(e))
1908 .cloned()
1909 .collect::<Vec<_>>()
1910 })
1911 })
1912 .collect::<Vec<_>>()
1913 .into_iter()
1914 .map(|handle| handle.join().unwrap_or_default())
1915 .collect()
1916 });
1917 for batch in segment_results {
1918 results.extend(batch);
1919 }
1920 } else {
1921 for segment_arc in surviving {
1922 let seg = segment_arc.read();
1923 results.extend(seg.iter().filter(|e| filter(e)).cloned());
1924 }
1925 }
1926
1927 (results, scan_stats)
1928 }
1929
1930 fn scan_entity_visible(snapshot: Option<&SnapshotContext>, entity: &UnifiedEntity) -> bool {
1936 match snapshot {
1937 Some(ctx) => entity_visible_with_context(Some(ctx), entity),
1938 None => {
1939 !crate::runtime::ai::moderation::entity_moderation_hidden(entity)
1940 && entity.xmax == 0
1941 }
1942 }
1943 }
1944
1945 pub fn scan<F>(&self, snapshot: Option<&SnapshotContext>, filter: F) -> Vec<UnifiedEntity>
1952 where
1953 F: Fn(&UnifiedEntity) -> bool + Sync,
1954 {
1955 self.query_all(|entity| Self::scan_entity_visible(snapshot, entity) && filter(entity))
1956 }
1957
1958 pub(crate) fn project_key_entity<T>(
1963 &self,
1964 key: &str,
1965 snapshot: Option<&SnapshotContext>,
1966 versioned: bool,
1967 mut project: impl FnMut(&UnifiedEntity) -> T,
1968 ) -> Option<T> {
1969 let mut probe = |segment: &GrowingSegment, best: &mut Option<(u64, T)>| {
1970 if let Some(entity) = segment.find_key_entity(key, versioned, &mut |entity| {
1971 !versioned || Self::scan_entity_visible(snapshot, entity)
1972 }) {
1973 if best.as_ref().is_none_or(|(xmin, _)| entity.xmin >= *xmin) {
1974 *best = Some((entity.xmin, project(entity)));
1975 }
1976 }
1977 };
1978 let mut best = None;
1979 if let Some(growing) = self.growing.read().as_ref() {
1980 probe(&growing.read(), &mut best);
1981 }
1982 for segment in self.sealed.read().iter() {
1983 if !versioned && best.is_some() {
1984 break;
1985 }
1986 probe(&segment.read(), &mut best);
1987 }
1988 best.map(|(_, projected)| projected)
1989 }
1990
1991 pub(crate) fn scan_graph_kind(
1993 &self,
1994 snapshot: Option<&SnapshotContext>,
1995 kind: GraphEntityKind,
1996 ) -> Vec<UnifiedEntity> {
1997 self.query_all_in_segments(
1998 |segment| segment.may_contain_graph_kind(kind),
1999 |entity| Self::scan_entity_visible(snapshot, entity) && kind.matches(&entity.kind),
2000 )
2001 }
2002
2003 pub(crate) fn scan_graph_candidates_for_each<F>(
2008 &self,
2009 snapshot: Option<&SnapshotContext>,
2010 kind: GraphEntityKind,
2011 before_segment: impl FnMut() -> bool,
2012 mut callback: F,
2013 ) where
2014 F: FnMut(&UnifiedEntity) -> bool,
2015 {
2016 self.for_each_entity_in_segments(
2017 |segment| segment.may_contain_graph_kind(kind),
2018 before_segment,
2019 |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
2020 );
2021 }
2022
2023 pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
2028 where
2029 F: FnMut(&UnifiedEntity) -> bool,
2030 {
2031 self.for_each_entity(|entity| {
2032 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2033 });
2034 }
2035
2036 pub(crate) fn physical_scan_cursor(&self) -> PhysicalScanCursor {
2037 let growing = self.growing.read();
2038 let sealed = self.sealed.read();
2039 PhysicalScanCursor {
2040 sources: growing
2041 .iter()
2042 .chain(sealed.iter())
2043 .map(|segment| (Arc::downgrade(segment), segment.read().scan_cursor()))
2044 .collect(),
2045 source: 0,
2046 }
2047 }
2048
2049 pub(crate) fn physical_scan_batch(
2052 &self,
2053 cursor: &mut PhysicalScanCursor,
2054 work_left: &mut usize,
2055 ) -> Vec<EntityId> {
2056 let mut ids = Vec::with_capacity(super::segment::SCAN_BATCH_SIZE.min(*work_left));
2057 while !cursor.finished() && *work_left > 0 {
2058 let (source, position) = &mut cursor.sources[cursor.source];
2059 if position.finished() {
2060 *work_left -= 1;
2061 cursor.source += 1;
2062 continue;
2063 }
2064 let Some(segment) = source.upgrade() else {
2065 *work_left -= 1;
2066 cursor.source += 1;
2067 continue;
2068 };
2069 segment.read().scan_batch(
2070 position,
2071 &mut || {
2072 if *work_left == 0 {
2073 return false;
2074 }
2075 *work_left -= 1;
2076 true
2077 },
2078 &mut |id| ids.push(id),
2079 );
2080 if position.finished() {
2081 cursor.source += 1;
2082 }
2083 if !ids.is_empty() {
2084 break;
2085 }
2086 }
2087 ids
2088 }
2089
2090 pub(crate) fn scan_batches(
2096 &self,
2097 snapshot: Option<&SnapshotContext>,
2098 mut before_work: impl FnMut() -> bool,
2099 mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
2100 ) -> bool {
2101 let segments = {
2102 let growing = self.growing.read();
2103 let sealed = self.sealed.read();
2104 let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
2105 for segment in growing.iter().chain(sealed.iter()) {
2106 if !before_work() {
2107 return false;
2108 }
2109 let cursor = segment.read().scan_cursor();
2110 segments.push((Arc::clone(segment), cursor));
2111 }
2112 segments
2113 };
2114 let mut ids = Vec::new();
2115 let mut batch = Vec::new();
2116 for (segment, mut cursor) in segments {
2117 while !cursor.finished() {
2118 ids.clear();
2119 batch.clear();
2120 let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
2121 ids.reserve_exact(batch_size);
2122 batch.reserve_exact(batch_size);
2123 {
2124 let segment = segment.read();
2125 if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
2126 return false;
2127 }
2128 }
2129 if !before_work() {
2132 return false;
2133 }
2134 self.for_each_id(&ids, |_, entity| {
2135 if Self::scan_entity_visible(snapshot, entity) {
2136 batch.push(entity.clone());
2137 }
2138 });
2139 if !before_work() {
2140 return false;
2141 }
2142 if !consume(&batch) {
2143 return false;
2144 }
2145 }
2146 }
2147 true
2148 }
2149
2150 pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
2152 &self,
2153 snapshot: Option<&SnapshotContext>,
2154 init: FInit,
2155 fold: FFold,
2156 reduce: FReduce,
2157 ) -> T
2158 where
2159 T: Send,
2160 FInit: Fn() -> T + Send + Sync,
2161 FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
2162 FReduce: Fn(T, T) -> T + Send + Sync,
2163 {
2164 self.fold_entities_parallel(
2165 init,
2166 |acc, entity| {
2167 if Self::scan_entity_visible(snapshot, entity) {
2168 fold(acc, entity)
2169 } else {
2170 acc
2171 }
2172 },
2173 reduce,
2174 )
2175 }
2176
2177 pub fn scan_for_each_zoned_with_stats<F>(
2179 &self,
2180 snapshot: Option<&SnapshotContext>,
2181 zone_preds: &[(&str, ZoneColPred<'_>)],
2182 mut callback: F,
2183 ) -> SegmentScanStats
2184 where
2185 F: FnMut(&UnifiedEntity) -> bool,
2186 {
2187 self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
2188 !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2189 })
2190 }
2191
2192 pub fn scan_zoned_with_stats<F>(
2194 &self,
2195 snapshot: Option<&SnapshotContext>,
2196 zone_preds: &[(&str, ZoneColPred<'_>)],
2197 filter: F,
2198 ) -> (Vec<UnifiedEntity>, SegmentScanStats)
2199 where
2200 F: Fn(&UnifiedEntity) -> bool + Sync,
2201 {
2202 self.query_all_zoned_with_stats(zone_preds, |entity| {
2203 Self::scan_entity_visible(snapshot, entity) && filter(entity)
2204 })
2205 }
2206
2207 pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
2210 where
2211 F: Fn(&UnifiedEntity) -> bool + Sync,
2212 {
2213 let mut entities = self.query_all_in_segments(|_| true, filter);
2214 self.restore_owned_row_schemas(entities.iter_mut());
2215 entities
2216 }
2217
2218 fn query_all_in_segments<F>(
2219 &self,
2220 segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
2221 filter: F,
2222 ) -> Vec<UnifiedEntity>
2223 where
2224 F: Fn(&UnifiedEntity) -> bool + Sync,
2225 {
2226 let mut results = Vec::new();
2227
2228 if let Some(growing_arc) = self.growing.read().as_ref() {
2231 let growing = if let Some(g) = growing_arc.try_read() {
2232 g
2233 } else {
2234 growing_arc.read()
2235 };
2236 if segment_filter(&growing) {
2237 results.extend(growing.iter().filter(|e| filter(e)).cloned());
2238 }
2239 }
2240
2241 let sealed = self.sealed.read();
2243 let use_parallel = sealed.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
2244 if use_parallel {
2245 let filter_ref = &filter;
2246 let segment_filter_ref = &segment_filter;
2247 let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
2248 sealed
2249 .iter()
2250 .map(|segment| {
2251 s.spawn(move || {
2252 let segment = segment.read();
2253 if segment_filter_ref(&segment) {
2254 segment
2255 .iter()
2256 .filter(|e| filter_ref(e))
2257 .cloned()
2258 .collect::<Vec<_>>()
2259 } else {
2260 Vec::new()
2261 }
2262 })
2263 })
2264 .collect::<Vec<_>>()
2265 .into_iter()
2266 .map(|handle| handle.join().unwrap_or_default())
2267 .collect()
2268 });
2269 for batch in segment_results {
2270 results.extend(batch);
2271 }
2272 } else {
2273 for segment in sealed.iter() {
2274 let seg = segment.read();
2275 if segment_filter(&seg) {
2276 results.extend(seg.iter().filter(|e| filter(e)).cloned());
2277 }
2278 }
2279 }
2280
2281 results
2282 }
2283
2284 pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
2289 if let Some(growing_arc) = self.growing.read().as_ref() {
2290 let growing = growing_arc.read();
2291 if growing.may_contain_exact_key(key) {
2292 return true;
2293 }
2294 } else {
2295 return true;
2296 }
2297
2298 let sealed = self.sealed.read();
2299 for segment_arc in sealed.iter() {
2300 let segment = segment_arc.read();
2301 if segment.may_contain_exact_key(key) {
2302 return true;
2303 }
2304 }
2305
2306 false
2307 }
2308
2309 pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2311 let mut results = Vec::new();
2312
2313 if let Some(growing_arc) = self.growing.read().as_ref() {
2315 let growing = growing_arc.read();
2316 results.extend(growing.filter_metadata(filters));
2317 }
2318
2319 let sealed = self.sealed.read();
2321 for segment in sealed.iter() {
2322 results.extend(segment.filter_metadata(filters));
2323 }
2324
2325 results
2326 }
2327
2328 pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2330 let mut results = Vec::new();
2331
2332 if let Some(growing_arc) = self.growing.read().as_ref() {
2334 let growing = growing_arc.read();
2335 for entity in growing.iter_kind(kind) {
2336 results.push(entity.clone());
2337 }
2338 }
2339
2340 let sealed = self.sealed.read();
2345 for segment_arc in sealed.iter() {
2346 let segment = segment_arc.read();
2347 for entity in segment.iter_kind(kind) {
2348 results.push(entity.clone());
2349 }
2350 }
2351
2352 results
2353 }
2354
2355 pub fn count(&self) -> usize {
2357 self.total_entities_atomic.load(Ordering::Relaxed) as usize
2358 }
2359
2360 pub fn segment_ids(&self) -> Vec<SegmentId> {
2362 let mut ids = Vec::new();
2363
2364 if let Some(growing_arc) = self.growing.read().as_ref() {
2365 ids.push(growing_arc.read().id());
2366 }
2367
2368 let sealed = self.sealed.read();
2369 for segment in sealed.iter() {
2370 ids.push(segment.id());
2371 }
2372
2373 ids.extend(self.archived.read().iter().copied());
2374
2375 ids
2376 }
2377
2378 #[inline]
2391 #[allow(clippy::unused_self)]
2392 fn emit(&self, _event: LifecycleEvent) {}
2393
2394 pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2397 std::mem::take(&mut *self.events.write())
2398 }
2399
2400 pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2402 self.maybe_seal_growing()?;
2404
2405 if self.config.enable_consolidation {
2406 self.consolidation_tick();
2407 }
2408
2409 Ok(())
2410 }
2411
2412 fn consolidation_tick(&self) {
2423 let running = self.consolidation.read().is_some();
2426 if !running && !self.start_consolidation() {
2427 return;
2428 }
2429
2430 let mut guard = self.consolidation.write();
2433 let Some(run) = guard.as_mut() else {
2434 return;
2435 };
2436 self.copy_bounded(run);
2437 if run.cursor >= run.sources.len() {
2438 if let Some(run) = guard.take() {
2439 self.finish_consolidation(run);
2440 }
2441 }
2442 }
2443
2444 fn start_consolidation(&self) -> bool {
2448 let sources = self.select_candidates();
2449 self.start_consolidation_from_sources(sources)
2450 }
2451
2452 fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2453 if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2454 return false;
2455 }
2456
2457 let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2458 let merged = GrowingSegment::new(merged_id, &self.collection);
2459
2460 *self.consolidation.write() = Some(Consolidation {
2461 sources: sources
2462 .into_iter()
2463 .map(|id| SourceProgress {
2464 id,
2465 copied: Vec::new(),
2466 tombstones_at_open: None,
2467 })
2468 .collect(),
2469 merged_id,
2470 merged,
2471 cursor: 0,
2472 pending_ids: Vec::new(),
2473 pending_cursor: 0,
2474 entities_copied: 0,
2475 });
2476
2477 self.stats.write().consolidation.runs_started += 1;
2478 true
2479 }
2480
2481 pub fn pressure_consolidation_tick(&self) -> bool {
2486 if !self.config.enable_consolidation {
2487 return false;
2488 }
2489
2490 let running = self.consolidation.read().is_some();
2491 if !running {
2492 let sources = self.dirty_sealed_segments();
2493 if !self.start_consolidation_from_sources(sources) {
2494 return false;
2495 }
2496 }
2497
2498 self.consolidation_tick();
2499 true
2500 }
2501
2502 fn select_candidates(&self) -> Vec<SegmentId> {
2517 let sealed = self.sealed.read();
2518 if sealed.is_empty() {
2519 return Vec::new();
2520 }
2521
2522 if sealed.len() > self.config.max_sealed_segments {
2523 return sealed.iter().map(|segment| segment.read().id()).collect();
2524 }
2525
2526 let mut live = 0usize;
2527 let mut tombstones = 0usize;
2528 let mut held = 0u64;
2529 let mut reclaimable = 0u64;
2530 let mut dirty = Vec::new();
2531
2532 for segment_arc in sealed.iter() {
2533 let segment = segment_arc.read();
2534 live += segment.live_entity_count();
2535 tombstones += segment.tombstone_count();
2536 held += segment.resident_bytes();
2537 reclaimable += segment.reclaimable_bytes();
2538 if segment.tombstone_count() > 0 {
2539 dirty.push(segment.id());
2540 }
2541 }
2542
2543 let total = live + tombstones;
2544 let tombstone_ratio = if total == 0 {
2545 0.0
2546 } else {
2547 tombstones as f64 / total as f64
2548 };
2549 let fragmentation = if held == 0 {
2550 0.0
2551 } else {
2552 reclaimable as f64 / held as f64
2553 };
2554
2555 if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2556 || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2557 {
2558 return dirty;
2559 }
2560
2561 Vec::new()
2562 }
2563
2564 fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2565 self.sealed
2566 .read()
2567 .iter()
2568 .filter_map(|segment_arc| {
2569 let segment = segment_arc.read();
2570 (segment.reclaimable_bytes() > 0).then(|| segment.id())
2571 })
2572 .collect()
2573 }
2574
2575 fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2578 if sources.is_empty() {
2579 return false;
2580 }
2581 let sealed = self.sealed.read();
2582 sealed.iter().any(|segment_arc| {
2583 let segment = segment_arc.read();
2584 sources.contains(&segment.id()) && segment.tombstone_count() > 0
2585 })
2586 }
2587
2588 fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2589 let sealed = self.sealed.read();
2590 sealed
2591 .iter()
2592 .find(|segment| segment.read().id() == id)
2593 .map(Arc::clone)
2594 }
2595
2596 fn copy_bounded(&self, run: &mut Consolidation) {
2601 let mut budget = self.config.consolidation_entities_per_tick;
2602
2603 while budget > 0 && run.cursor < run.sources.len() {
2604 let source_id = run.sources[run.cursor].id;
2605 let Some(source_arc) = self.sealed_segment(source_id) else {
2606 run.cursor += 1;
2610 run.pending_ids.clear();
2611 run.pending_cursor = 0;
2612 continue;
2613 };
2614
2615 if run.sources[run.cursor].tombstones_at_open.is_none() {
2616 let source = source_arc.read();
2617 run.pending_ids = source.live_entity_ids();
2618 run.pending_cursor = 0;
2619 run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2620 }
2621
2622 {
2623 let source = source_arc.read();
2624 while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2625 let id = run.pending_ids[run.pending_cursor];
2626 run.pending_cursor += 1;
2627 budget -= 1;
2628
2629 let Some(entity) = source.get(id) else {
2632 continue;
2633 };
2634 let metadata = source.get_metadata(id);
2635 run.merged.adopt_entity(entity.clone(), metadata);
2636 run.sources[run.cursor].copied.push(id);
2637 run.entities_copied += 1;
2638 }
2639 }
2640
2641 if run.pending_cursor >= run.pending_ids.len() {
2642 run.cursor += 1;
2643 run.pending_ids.clear();
2644 run.pending_cursor = 0;
2645 }
2646 }
2647 }
2648
2649 fn finish_consolidation(&self, mut run: Consolidation) {
2655 let mut sealed = self.sealed.write();
2656
2657 let mut positions = Vec::with_capacity(run.sources.len());
2658 for source in &run.sources {
2659 if let Some(index) = sealed
2660 .iter()
2661 .position(|segment| segment.read().id() == source.id)
2662 {
2663 positions.push(index);
2664 }
2665 }
2666 if positions.is_empty() {
2667 return;
2668 }
2669 positions.sort_unstable();
2670
2671 let mut stale = Vec::new();
2675 for source in &run.sources {
2676 let Some(source_arc) = sealed
2677 .iter()
2678 .find(|segment| segment.read().id() == source.id)
2679 else {
2680 continue;
2681 };
2682 let segment = source_arc.read();
2683 if source.tombstones_at_open == Some(segment.tombstone_count()) {
2684 continue;
2685 }
2686 stale.extend(
2687 source
2688 .copied
2689 .iter()
2690 .copied()
2691 .filter(|id| !segment.contains(*id)),
2692 );
2693 }
2694 for id in stale {
2695 run.merged.evict_entity(id);
2696 }
2697
2698 let reclaimed_bytes: u64 = positions
2699 .iter()
2700 .map(|&index| sealed[index].read().resident_bytes())
2701 .sum();
2702 let reclaimed_tombstones: u64 = positions
2703 .iter()
2704 .map(|&index| sealed[index].read().tombstone_count() as u64)
2705 .sum();
2706
2707 if run.merged.seal().is_err() {
2708 return;
2709 }
2710 let merged_bytes = run.merged.resident_bytes();
2711 let merged_arc = Arc::new(RwLock::new(run.merged));
2712
2713 {
2714 let mut retired = self.retired.write();
2715 retired.retain(|source| source.strong_count() > 0);
2716 for &index in positions.iter().rev() {
2717 let source = sealed.remove(index);
2718 if Arc::strong_count(&source) > 1 {
2719 retired.push(Arc::downgrade(&source));
2720 }
2721 }
2722 }
2723 let insert_at = positions[0].min(sealed.len());
2724 sealed.insert(insert_at, Arc::clone(&merged_arc));
2725 drop(sealed);
2726
2727 let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
2729 {
2730 let mut entity_segment = self.entity_segment.write();
2731 for segment_id in entity_segment.values_mut() {
2732 if source_ids.contains(segment_id) {
2733 *segment_id = run.merged_id;
2734 }
2735 }
2736 }
2737
2738 {
2739 let mut stats = self.stats.write();
2740 stats.sealed_count = stats
2741 .sealed_count
2742 .saturating_sub(positions.len())
2743 .saturating_add(1);
2744 let consolidation = &mut stats.consolidation;
2745 consolidation.runs_completed += 1;
2746 consolidation.segments_merged += positions.len() as u64;
2747 consolidation.tombstones_reclaimed += reclaimed_tombstones;
2748 consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
2749 }
2750
2751 self.emit(LifecycleEvent::SegmentConsolidated {
2752 source: source_ids,
2753 target: run.merged_id,
2754 });
2755 }
2756
2757 #[cfg(test)]
2760 fn consolidation_progress(&self) -> Option<u64> {
2761 self.consolidation
2762 .read()
2763 .as_ref()
2764 .map(|run| run.entities_copied)
2765 }
2766}
2767
2768impl UnifiedSegment for Arc<RwLock<GrowingSegment>> {
2771 fn id(&self) -> SegmentId {
2772 self.read().id()
2773 }
2774
2775 fn state(&self) -> SegmentState {
2776 self.read().state()
2777 }
2778
2779 fn collection(&self) -> &str {
2780 "unknown"
2782 }
2783
2784 fn stats(&self) -> SegmentStats {
2785 self.read().stats()
2786 }
2787
2788 fn entity_count(&self) -> usize {
2789 self.read().entity_count()
2790 }
2791
2792 fn contains(&self, id: EntityId) -> bool {
2793 self.read().contains(id)
2794 }
2795
2796 fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2797 None
2800 }
2801
2802 fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2803 None
2804 }
2805
2806 fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2807 self.write().insert(entity)
2808 }
2809
2810 fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2811 self.write().update(entity)
2812 }
2813
2814 fn update_hot(
2815 &mut self,
2816 entity: UnifiedEntity,
2817 modified_columns: &[String],
2818 ) -> Result<(), SegmentError> {
2819 self.write().update_hot(entity, modified_columns)
2820 }
2821
2822 fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2823 self.write().delete(id)
2824 }
2825
2826 fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2827 self.read().get_metadata(id)
2828 }
2829
2830 fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2831 self.write().set_metadata(id, metadata)
2832 }
2833
2834 fn seal(&mut self) -> Result<(), SegmentError> {
2835 self.write().seal()
2836 }
2837
2838 fn should_seal(&self, config: &SegmentConfig) -> bool {
2839 self.read().should_seal(config)
2840 }
2841
2842 fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2843 Box::new(std::iter::empty())
2845 }
2846
2847 fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2848 Box::new(std::iter::empty())
2849 }
2850
2851 fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2852 self.read().filter_metadata(filters)
2853 }
2854}
2855
2856#[cfg(test)]
2857mod tests {
2858 use std::collections::HashSet;
2859
2860 use super::*;
2861 use crate::runtime::mvcc::SnapshotContext;
2862 use crate::storage::transaction::snapshot::{Snapshot, SnapshotManager};
2863 use crate::storage::unified::entity::{EntityData, EntityKind, RowData};
2864 use reddb_types::Value;
2865
2866 #[test]
2867 fn physical_pages_keep_global_order_across_segments_and_deleted_slots() {
2868 let manager = SegmentManager::new("pages");
2869 for group in [[50, 10], [30, 20]] {
2870 for id in group {
2871 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "pages", id, vec![]);
2872 entity.data = EntityData::Row(RowData::with_names(
2873 vec![Value::Integer(id as i64)],
2874 vec!["id".into()],
2875 ));
2876 manager.insert(entity).unwrap();
2877 }
2878 manager.force_seal().unwrap();
2879 }
2880 manager
2881 .insert(UnifiedEntity::table_row(
2882 EntityId::new(40),
2883 "pages",
2884 40,
2885 vec![],
2886 ))
2887 .unwrap();
2888 let (total, page) = manager.page_in_id_order(1, 2);
2889 assert_eq!(total, 5);
2890 assert_eq!(
2891 page.iter()
2892 .map(|entity| entity.id.raw())
2893 .collect::<Vec<_>>(),
2894 [20, 30]
2895 );
2896 let EntityData::Row(row) = &page[0].data else {
2897 panic!("row")
2898 };
2899 assert_eq!(row.get_field("id"), Some(&Value::Integer(20)));
2900 manager.delete(EntityId::new(30)).unwrap();
2901 let (total, page) = manager.page_in_id_order(1, 2);
2902 assert_eq!(total, 4);
2903 assert_eq!(
2904 page.iter()
2905 .map(|entity| entity.id.raw())
2906 .collect::<Vec<_>>(),
2907 [20, 40]
2908 );
2909 for offset in [4, usize::MAX] {
2910 let (total, page) = manager.page_in_id_order(offset, 2);
2911 assert_eq!(total, 4);
2912 assert!(page.is_empty());
2913 }
2914 }
2915
2916 fn lookup_version(id: u64, key: &str, xmin: u64, xmax: u64) -> UnifiedEntity {
2917 let mut entity = UnifiedEntity::table_row(EntityId::new(id), "keys", id, vec![]);
2918 entity.data = EntityData::Row(RowData::with_names(
2919 vec![Value::text(key)],
2920 vec!["key".into()],
2921 ));
2922 entity.set_xmin(xmin);
2923 entity.set_xmax(xmax);
2924 entity
2925 }
2926
2927 #[test]
2928 fn key_lookup_matches_snapshot_scan_across_segments() {
2929 let manager = Arc::new(SegmentManager::new("keys"));
2930 for entity in [
2931 lookup_version(1, "changed", 3, 12),
2932 lookup_version(2, "deleted", 3, 7),
2933 ] {
2934 manager.insert(entity).expect("old version");
2935 }
2936 manager.force_seal().expect("seal old versions");
2937 for entity in [
2938 lookup_version(3, "changed", 12, 0),
2939 lookup_version(4, "pending", 4, 0),
2940 lookup_version(5, "own", 15, 0),
2941 lookup_version(6, "live", 3, 0),
2942 ] {
2943 manager.insert(entity).expect("new version");
2944 }
2945 std::thread::spawn(move || {
2946 let snapshot = SnapshotContext {
2947 snapshot: Snapshot {
2948 xid: 10,
2949 in_progress: HashSet::from([4]),
2950 },
2951 manager: Arc::new(SnapshotManager::new()),
2952 own_xids: HashSet::from([15]),
2953 requires_index_fallback: false,
2954 serializable_reader: None,
2955 };
2956 for snapshot in [Some(&snapshot), None] {
2957 for key in ["changed", "deleted", "pending", "own", "live", "absent"] {
2958 let expected = manager
2959 .scan(snapshot, |entity| {
2960 super::super::segment_key_lookup::SegmentKeyLookup::key(entity)
2961 == Some(key)
2962 })
2963 .into_iter()
2964 .max_by_key(|entity| entity.xmin)
2965 .map(|entity| entity.id);
2966 assert_eq!(
2967 manager.project_key_entity(key, snapshot, true, |entity| entity.id),
2968 expected,
2969 "key={key}"
2970 );
2971 }
2972 }
2973 assert_eq!(
2974 manager.project_key_entity("changed", Some(&snapshot), true, |entity| entity.id),
2975 Some(EntityId::new(1))
2976 );
2977 assert_eq!(
2978 manager.project_key_entity("changed", None, true, |entity| entity.id),
2979 Some(EntityId::new(3))
2980 );
2981 assert!(manager
2982 .project_key_entity("deleted", None, true, |entity| entity.id)
2983 .is_none());
2984 })
2985 .join()
2986 .expect("snapshot worker");
2987 }
2988
2989 #[test]
2990 fn key_lookup_survives_paced_consolidation() {
2991 let manager = consolidating_manager(1);
2992 manager
2993 .bulk_insert(
2994 (1..=10)
2995 .map(|id| lookup_version(id, &format!("key-{id}"), 1, 0))
2996 .collect(),
2997 )
2998 .expect("rows");
2999 manager.force_seal().expect("seal");
3000 for id in 1..=4 {
3001 manager.delete(EntityId::new(id)).expect("delete");
3002 }
3003 assert!(manager.pressure_consolidation_tick());
3004 drain_maintenance(&manager);
3005 for id in 1..=10 {
3006 assert_eq!(
3007 manager.project_key_entity(&format!("key-{id}"), None, false, |entity| entity.id),
3008 (id > 4).then_some(EntityId::new(id))
3009 );
3010 }
3011 }
3012
3013 #[test]
3014 #[ignore = "diagnostic of segment fragmentation and retained history; no timing assertions"]
3015 fn key_lookup_fragmentation_history_diagnostic() {
3016 use std::{hint::black_box, time::Instant};
3017 for segments in [1, 16, 64] {
3018 for versions in [1, 100, 1000] {
3019 let manager = SegmentManager::new("keys");
3020 let mut id = 1;
3021 for segment in 0..segments {
3022 let mut rows: Vec<_> = (0..128)
3023 .map(|_| {
3024 let entity = lookup_version(id, &format!("other-{id}"), 1, 0);
3025 id += 1;
3026 entity
3027 })
3028 .collect();
3029 for version in (segment..versions).step_by(segments) {
3030 rows.push(lookup_version(
3031 id,
3032 "history",
3033 version as u64 + 1,
3034 if version + 1 == versions {
3035 0
3036 } else {
3037 version as u64 + 2
3038 },
3039 ));
3040 id += 1;
3041 }
3042 manager.bulk_insert(rows).expect("rows");
3043 manager.force_seal().expect("seal");
3044 }
3045 let started = Instant::now();
3046 for _ in 0..2000 {
3047 let xmin =
3048 manager.project_key_entity(black_box("history"), None, true, |entity| {
3049 entity.xmin
3050 });
3051 assert_eq!(xmin, Some(versions as u64));
3052 }
3053 eprintln!("segments={segments} versions={versions} reads=2000 elapsed_us={} resident_bytes={}", started.elapsed().as_micros(), manager.resident_bytes());
3054 }
3055 }
3056 }
3057
3058 #[test]
3059 fn ordered_fingerprint_scan_matches_legacy_sort_without_cloning_rows() {
3060 let manager = SegmentManager::new("fingerprint");
3061 let row = |id, label: &str| {
3062 UnifiedEntity::table_row(
3063 EntityId::new(id),
3064 "fingerprint",
3065 id,
3066 vec![Value::text(label)],
3067 )
3068 };
3069 manager
3070 .bulk_insert(vec![row(8, "eight"), row(2, "sealed-two"), row(5, "five")])
3071 .expect("sealed rows");
3072 manager.force_seal().expect("seal");
3073 manager
3074 .bulk_insert(vec![row(7, "seven"), row(2, "growing-two"), row(1, "one")])
3075 .expect("growing rows");
3076
3077 let mut expected = manager.query_all(|_| true);
3078 expected.sort_by_key(|entity| entity.id.raw());
3079 let expected: Vec<_> = expected
3080 .iter()
3081 .map(|entity| format!("{entity:?}"))
3082 .collect();
3083 let mut stored_addresses = HashSet::new();
3084 manager.for_each_entity(|entity| {
3085 stored_addresses.insert(entity as *const UnifiedEntity as usize);
3086 true
3087 });
3088
3089 let mut actual = Vec::new();
3090 manager.for_each_entity_in_id_order(|entity| {
3091 assert!(stored_addresses.contains(&(entity as *const UnifiedEntity as usize)));
3092 assert!(
3093 manager.growing.try_write().is_some(),
3094 "topology guard retained"
3095 );
3096 assert!(
3097 manager.sealed.try_write().is_some(),
3098 "topology guard retained"
3099 );
3100 actual.push(format!("{entity:?}"));
3101 true
3102 });
3103 assert_eq!(
3104 actual, expected,
3105 "equal IDs retain their original segment order"
3106 );
3107 }
3108
3109 #[test]
3110 fn ordered_fingerprint_scan_stops_early_and_releases_sources() {
3111 let manager = SegmentManager::new("fingerprint");
3112 for id in [3, 1, 2] {
3113 manager
3114 .insert(UnifiedEntity::table_row(
3115 EntityId::new(id),
3116 "fingerprint",
3117 id,
3118 vec![Value::Integer(id as i64)],
3119 ))
3120 .expect("row");
3121 }
3122 let source = manager.growing.read().as_ref().expect("source").clone();
3123 let handles = Arc::strong_count(&source);
3124 let mut seen = Vec::new();
3125 manager.for_each_entity_in_id_order(|entity| {
3126 seen.push(entity.id.raw());
3127 false
3128 });
3129 assert_eq!(seen, vec![1]);
3130 assert_eq!(Arc::strong_count(&source), handles);
3131 assert!(
3132 source.try_write().is_some(),
3133 "segment guard retained after early stop"
3134 );
3135 }
3136
3137 #[test]
3138 fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
3139 let manager = SegmentManager::with_config(
3140 "vectors",
3141 ManagerConfig {
3142 max_sealed_segments: 1,
3143 consolidation_entities_per_tick: 10_000,
3144 ..Default::default()
3145 },
3146 );
3147 for part in 0..3 {
3148 manager
3149 .bulk_insert(
3150 (1..=300)
3151 .map(|offset| {
3152 UnifiedEntity::vector(
3153 EntityId::new(part * 300 + offset),
3154 "vectors",
3155 vec![1.0, 0.0],
3156 )
3157 })
3158 .collect(),
3159 )
3160 .expect("batch");
3161 if part < 2 {
3162 manager.force_seal().expect("seal");
3163 }
3164 }
3165 let original_growing = manager.growing.read().as_ref().expect("growing").clone();
3166 let mut changed = false;
3167 let mut seen = Vec::new();
3168 assert!(manager.scan_batches(
3169 None,
3170 || true,
3171 |batch| {
3172 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3173 assert!(
3174 manager.growing.try_write().is_some(),
3175 "consumer holds no topology guard"
3176 );
3177 assert!(
3178 original_growing.try_write().is_some(),
3179 "consumer holds no segment guard"
3180 );
3181 for entity in batch {
3182 assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
3183 seen.push(entity.id.raw());
3184 }
3185 if !changed {
3186 changed = true;
3187 manager.force_seal().expect("seal during scan");
3188 manager
3189 .insert(UnifiedEntity::vector(
3190 EntityId::new(5000),
3191 "vectors",
3192 vec![1.0, 0.0],
3193 ))
3194 .expect("new write");
3195 drain_maintenance(&manager);
3196 assert!(manager.stats().consolidation.runs_completed > 0);
3197 assert!(manager
3198 .delete(EntityId::new(1))
3199 .expect("delete after source retirement"));
3200 assert!(manager.get(EntityId::new(1)).is_none());
3201 }
3202 true
3203 }
3204 ));
3205 seen.sort_unstable();
3206 assert_eq!(
3207 seen,
3208 (2..=900).collect::<Vec<_>>(),
3209 "no duplicate, missing or late candidates"
3210 );
3211 let mut after = Vec::new();
3212 assert!(manager.scan_batches(
3213 None,
3214 || true,
3215 |batch| {
3216 after.extend(batch.iter().map(|entity| entity.id.raw()));
3217 true
3218 }
3219 ));
3220 after.sort_unstable();
3221 let mut expected: Vec<_> = (2..=900).collect();
3222 expected.push(5000);
3223 assert_eq!(after, expected, "adopted entities have scan positions");
3224 }
3225
3226 #[test]
3227 fn bounded_scan_applies_snapshot_to_relocated_versions() {
3228 for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
3229 let manager = SegmentManager::with_config(
3230 "vectors",
3231 ManagerConfig {
3232 max_sealed_segments: 1,
3233 ..Default::default()
3234 },
3235 );
3236 for ids in [[1, 2], [3, 4]] {
3237 let entities = ids.map(|id| {
3238 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3239 entity.set_xmin(3);
3240 entity
3241 });
3242 if bulk {
3243 manager
3244 .bulk_insert(entities.into())
3245 .expect("initial versions");
3246 } else {
3247 for entity in entities {
3248 manager.insert(entity).expect("initial version");
3249 }
3250 }
3251 manager.force_seal().expect("seal source");
3252 }
3253 let snapshot = SnapshotContext {
3254 snapshot: Snapshot {
3255 xid: 10,
3256 in_progress: HashSet::new(),
3257 },
3258 manager: Arc::new(SnapshotManager::new()),
3259 own_xids: HashSet::new(),
3260 requires_index_fallback: false,
3261 serializable_reader: None,
3262 };
3263 let mut changed = false;
3264 let mut seen = Vec::new();
3265 assert!(manager.scan_batches(
3266 Some(&snapshot),
3267 || true,
3268 |batch| {
3269 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3270 if !changed {
3271 changed = true;
3272 if consolidate {
3273 drain_maintenance(&manager);
3274 assert!(manager.stats().consolidation.runs_completed > 0);
3275 }
3276 for (id, xmax) in [(3, 8), (4, 12)] {
3277 let mut entity =
3278 manager.get(EntityId::new(id)).expect("retained version");
3279 entity.set_xmax(xmax);
3280 manager
3281 .update(entity)
3282 .expect("update after source retirement");
3283 }
3284 }
3285 true
3286 }
3287 ));
3288 seen.sort_unstable();
3289 assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
3290 let mut again = Vec::new();
3291 assert!(manager.scan_batches(
3292 Some(&snapshot),
3293 || true,
3294 |batch| {
3295 again.extend(batch.iter().map(|entity| entity.id.raw()));
3296 true
3297 }
3298 ));
3299 again.sort_unstable();
3300 assert_eq!(
3301 again,
3302 vec![1, 2, 4],
3303 "old slots do not duplicate relocated versions"
3304 );
3305 let mut current = Vec::new();
3306 assert!(manager.scan_batches(
3307 None,
3308 || true,
3309 |batch| {
3310 current.extend(batch.iter().map(|entity| entity.id.raw()));
3311 true
3312 }
3313 ));
3314 current.sort_unstable();
3315 assert_eq!(
3316 current,
3317 vec![1, 2],
3318 "current view excludes both deleted versions"
3319 );
3320 }
3321 }
3322
3323 #[test]
3324 fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
3325 let manager = SegmentManager::new("mixed");
3326 let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
3327 manager
3328 .bulk_insert((1..=300).map(vector).collect())
3329 .expect("flat");
3330 manager.insert(vector(500)).expect("hash insert");
3331 manager
3332 .bulk_insert(vec![vector(502), vector(505)])
3333 .expect("gapped bulk");
3334 manager.delete(EntityId::new(2)).expect("flat delete");
3335 manager.delete(EntityId::new(505)).expect("hash delete");
3336 let mut changed = false;
3337 let mut seen = Vec::new();
3338 assert!(manager.scan_batches(
3339 None,
3340 || true,
3341 |batch| {
3342 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3343 if !changed {
3344 changed = true;
3345 manager
3346 .insert(vector(700))
3347 .expect("append after cursor boundary");
3348 manager
3349 .update(UnifiedEntity::graph_node(
3350 EntityId::new(500),
3351 "mixed",
3352 "Node",
3353 HashMap::new(),
3354 ))
3355 .expect("structural update");
3356 }
3357 true
3358 }
3359 ));
3360 seen.sort_unstable();
3361 let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
3362 expected.extend([500, 502]);
3363 assert_eq!(seen, expected);
3364 }
3365
3366 #[test]
3367 fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
3368 let manager = SegmentManager::new("vectors");
3369 let mut entities = Vec::new();
3370 for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
3371 let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3372 entity.set_xmin(xmin);
3373 entity.set_xmax(xmax);
3374 entities.push(entity);
3375 }
3376 manager.bulk_insert(entities).expect("versions");
3377 manager.force_seal().expect("seal");
3378 let snapshot = SnapshotContext {
3379 snapshot: Snapshot {
3380 xid: 10,
3381 in_progress: HashSet::from([4]),
3382 },
3383 manager: Arc::new(SnapshotManager::new()),
3384 own_xids: HashSet::new(),
3385 requires_index_fallback: false,
3386 serializable_reader: None,
3387 };
3388 let mut work = 0;
3389 let mut callbacks = 0;
3390 assert!(!manager.scan_batches(
3391 Some(&snapshot),
3392 || {
3393 work += 1;
3394 work <= 2
3395 },
3396 |_| {
3397 callbacks += 1;
3398 true
3399 }
3400 ));
3401 assert_eq!(callbacks, 0, "a stopped batch is not consumed");
3402 for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
3403 let mut seen = Vec::new();
3404 assert!(manager.scan_batches(
3405 view,
3406 || true,
3407 |batch| {
3408 seen.extend(batch.iter().map(|entity| entity.id.raw()));
3409 true
3410 }
3411 ));
3412 seen.sort_unstable();
3413 assert_eq!(seen, expected);
3414 }
3415 }
3416
3417 #[test]
3418 fn test_manager_basic() {
3419 let manager = SegmentManager::new("test_collection");
3420
3421 let entity = UnifiedEntity::table_row(
3422 manager.next_entity_id(),
3423 "users",
3424 1,
3425 vec![Value::text("Alice".to_string())],
3426 );
3427
3428 let id = manager.insert(entity).unwrap();
3429 assert!(manager.get(id).is_some());
3430 assert_eq!(manager.count(), 1);
3431 }
3432
3433 #[test]
3434 fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
3435 let manager = Arc::new(SegmentManager::new("mixed"));
3436 manager
3437 .bulk_insert(
3438 (1..=128)
3439 .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
3440 .collect(),
3441 )
3442 .expect("vector-only segment");
3443 manager.force_seal().expect("seal vectors");
3444 let mut old =
3445 UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
3446 old.set_xmin(3);
3447 old.set_xmax(12);
3448 let mut new =
3449 UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
3450 new.set_xmin(12);
3451 let edge = UnifiedEntity::graph_edge(
3452 EntityId::new(202),
3453 "mixed",
3454 "a",
3455 "b",
3456 1.0,
3457 Default::default(),
3458 );
3459 manager
3460 .bulk_insert(vec![old, new, edge])
3461 .expect("physical graph versions");
3462 manager.force_seal().expect("seal graph");
3463 manager
3464 .insert(UnifiedEntity::graph_node(
3465 EntityId::new(203),
3466 "mixed",
3467 "person",
3468 Default::default(),
3469 ))
3470 .expect("growing node");
3471 let snapshot = SnapshotContext {
3472 snapshot: Snapshot {
3473 xid: 10,
3474 in_progress: HashSet::new(),
3475 },
3476 manager: Arc::new(SnapshotManager::new()),
3477 own_xids: HashSet::new(),
3478 requires_index_fallback: false,
3479 serializable_reader: None,
3480 };
3481 std::thread::spawn(move || {
3482 for view in [None, Some(&snapshot)] {
3483 for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
3484 let expected: HashSet<_> = manager
3485 .scan(view, |item| kind.matches(&item.kind))
3486 .into_iter()
3487 .map(|item| item.id)
3488 .collect();
3489 let actual: HashSet<_> = manager
3490 .scan_graph_kind(view, kind)
3491 .into_iter()
3492 .map(|item| item.id)
3493 .collect();
3494 assert_eq!(actual, expected);
3495 let mut visited = 0;
3496 let mut candidates = HashSet::new();
3497 manager.scan_graph_candidates_for_each(
3498 view,
3499 kind,
3500 || true,
3501 |item| {
3502 visited += 1;
3503 if kind.matches(&item.kind) {
3504 candidates.insert(item.id);
3505 }
3506 true
3507 },
3508 );
3509 assert_eq!(candidates, expected);
3510 assert_eq!(
3511 visited,
3512 if matches!(kind, GraphEntityKind::Node) {
3513 3
3514 } else {
3515 2
3516 }
3517 );
3518 }
3519 }
3520 let ids: HashSet<_> = manager
3521 .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
3522 .into_iter()
3523 .map(|item| item.id.raw())
3524 .collect();
3525 assert_eq!(ids, HashSet::from([200, 203]));
3526 let ids: HashSet<_> = manager
3527 .scan_graph_kind(None, GraphEntityKind::Node)
3528 .into_iter()
3529 .map(|item| item.id.raw())
3530 .collect();
3531 assert_eq!(ids, HashSet::from([201, 203]));
3532 let mut visits = 0;
3533 manager.scan_graph_candidates_for_each(
3534 None,
3535 GraphEntityKind::Node,
3536 || true,
3537 |_| {
3538 visits += 1;
3539 false
3540 },
3541 );
3542 assert_eq!(
3543 visits, 1,
3544 "early termination must also stop subsequent segments"
3545 );
3546 })
3547 .join()
3548 .expect("snapshot worker");
3549 }
3550
3551 #[test]
3552 fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
3553 let manager = Arc::new(SegmentManager::new("accounts"));
3554 let cases = [
3555 ("legacy row", 0, 0, true),
3556 ("committed creator", 3, 0, true),
3557 ("future creator", 11, 0, false),
3558 ("in-progress creator", 4, 0, false),
3559 ("committed deleter", 3, 7, false),
3560 ("future deleter", 3, 11, true),
3561 ("in-progress deleter", 3, 4, true),
3562 ];
3563
3564 for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
3565 let row_id = index as u64 + 1;
3566 let mut entity = UnifiedEntity::table_row(
3567 EntityId::new(row_id),
3568 "accounts",
3569 row_id,
3570 vec![Value::Integer(row_id as i64)],
3571 );
3572 entity.set_xmin(*xmin);
3573 entity.set_xmax(*xmax);
3574 manager.insert(entity).unwrap();
3575 }
3576
3577 let snapshot = SnapshotContext {
3578 snapshot: Snapshot {
3579 xid: 10,
3580 in_progress: HashSet::from([4]),
3581 },
3582 manager: Arc::new(SnapshotManager::new()),
3583 own_xids: HashSet::new(),
3584 requires_index_fallback: false,
3585 serializable_reader: None,
3586 };
3587
3588 let raw_visible_row_ids: HashSet<u64> = manager
3589 .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
3590 .into_iter()
3591 .filter_map(|entity| match entity.kind {
3592 EntityKind::TableRow { row_id, .. } => Some(row_id),
3593 _ => None,
3594 })
3595 .collect();
3596
3597 let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3598 .join()
3599 .unwrap();
3600 let visible_row_ids: HashSet<u64> = scanned
3601 .into_iter()
3602 .filter_map(|entity| match entity.kind {
3603 EntityKind::TableRow { row_id, .. } => Some(row_id),
3604 _ => None,
3605 })
3606 .collect();
3607 assert_eq!(visible_row_ids, raw_visible_row_ids);
3608
3609 for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3610 let row_id = index as u64 + 1;
3611 assert_eq!(
3612 visible_row_ids.contains(&row_id),
3613 *expected_visible,
3614 "{name} visibility mismatch"
3615 );
3616 }
3617 }
3618
3619 #[test]
3620 fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3621 let manager = SegmentManager::new("accounts");
3625 let mut live =
3626 UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3627 live.set_xmin(2);
3628 live.set_xmax(0);
3629 manager.insert(live).unwrap();
3630
3631 let mut superseded =
3632 UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3633 superseded.set_xmin(2);
3634 superseded.set_xmax(9);
3635 manager.insert(superseded).unwrap();
3636
3637 let rows: Vec<u64> = manager
3638 .scan(None, |_| true)
3639 .into_iter()
3640 .filter_map(|entity| match entity.kind {
3641 EntityKind::TableRow { row_id, .. } => Some(row_id),
3642 _ => None,
3643 })
3644 .collect();
3645 assert_eq!(
3646 rows,
3647 vec![1],
3648 "frameless scan must hide superseded versions"
3649 );
3650 }
3651
3652 #[test]
3653 fn query_scan_callers_use_snapshot_api_for_visibility() {
3654 let table = include_str!("../../runtime/query_exec/table.rs");
3655 let aggregate = include_str!("../../runtime/query_exec/aggregate.rs");
3656
3657 assert_eq!(
3658 table.matches(".for_each_entity(|entity|").count(),
3659 1,
3660 "only the visibility-independent schema inspection may use the raw table iterator"
3661 );
3662 assert!(!table.contains(".query_all_zoned_with_stats("));
3663 assert!(!table.contains(".for_each_entity_zoned_with_stats("));
3664 assert!(!table.contains("TableRowMvccReadResolver::captured"));
3665
3666 assert!(!aggregate.contains("TableRowMvccReadResolver"));
3667 assert!(!aggregate.contains(".for_each_entity("));
3668 assert!(!aggregate.contains(".fold_entities_parallel("));
3669 }
3670
3671 #[test]
3672 fn remaining_visibility_scan_callers_use_snapshot_api() {
3673 let aggregate_pushdown =
3674 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs");
3675 let ask = include_str!("../../runtime/ask_pipeline.rs");
3676 let dml = include_str!("../../runtime/impl_dml.rs");
3677 let dml_targets = include_str!("../../runtime/dml_target_scan.rs");
3678 let events = include_str!("../../runtime/impl_events.rs");
3679 let graph_commands = include_str!("../../runtime/impl_graph_commands.rs");
3680 let graph_dsl = include_str!("../../runtime/graph_dsl.rs");
3681 let graph_tvf = include_str!("../../runtime/graph_tvf.rs");
3682 let kv = include_str!("../../runtime/impl_kv.rs");
3683 let queue = include_str!("../../runtime/impl_queue.rs");
3684 let primary_queue = include_str!("../../runtime/primary_queue_store.rs");
3685 let record_search = include_str!("../../runtime/record_search.rs");
3686 let search = include_str!("../../runtime/impl_search.rs");
3687 let vector = include_str!("../../runtime/query_exec/vector.rs");
3688
3689 assert!(!aggregate_pushdown.contains("TableRowMvccReadResolver"));
3690 assert!(!aggregate_pushdown.contains(".for_each_entity("));
3691 assert!(!ask.contains("for entity in manager.query_all(|_| true)"));
3692 assert!(!dml.contains("let entities = manager.query_all(|_| true);"));
3693 assert_eq!(
3698 dml_targets
3699 .matches("self.candidate_visible(snapshot.as_ref(), entity)")
3700 .count(),
3701 2,
3702 "both raw DML iterations must route through candidate_visible"
3703 );
3704 assert_eq!(
3707 events.matches("manager.query_all(").count(),
3708 1,
3709 "only the visibility-free backfill dedup may use query_all"
3710 );
3711 assert!(!graph_commands.contains("TableRowMvccReadResolver"));
3712 assert!(!graph_dsl.contains("entity_visible_with_context(snap_ctx.as_ref(), e)"));
3713 assert!(!graph_tvf.contains("entity_visible_with_context(snap_ctx.as_ref(), &entity)"));
3714 assert!(!kv.contains("TableRowMvccReadResolver"));
3715 assert!(!queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3716 assert!(!primary_queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3717 assert!(!record_search.contains("TableRowMvccReadResolver"));
3718 assert!(!search.contains("for entity in manager.query_all(|_| true)"));
3719 assert!(!vector.contains("manager.query_all("));
3720 }
3721
3722 fn production_source(src: &str) -> String {
3727 let mut kept: Vec<&str> = Vec::new();
3728 let lines: Vec<&str> = src.lines().collect();
3729 let mut idx = 0;
3730 while idx < lines.len() {
3731 let starts_test_mod = lines[idx].starts_with("#[cfg(test)]")
3732 && lines
3733 .get(idx + 1)
3734 .is_some_and(|next| next.starts_with("mod "));
3735 if !starts_test_mod {
3736 kept.push(lines[idx]);
3737 idx += 1;
3738 continue;
3739 }
3740 idx += 1;
3741 let mut depth: isize = 0;
3742 while idx < lines.len() {
3743 depth += lines[idx].matches('{').count() as isize;
3744 depth -= lines[idx].matches('}').count() as isize;
3745 idx += 1;
3746 if depth <= 0 {
3747 break;
3748 }
3749 }
3750 }
3751 kept.join("\n")
3752 }
3753
3754 fn raw_iteration_sites(src: &str) -> usize {
3756 src.matches("query_all(").count()
3757 + src.matches(".for_each_entity").count()
3758 + src.matches("fold_entities_parallel(").count()
3759 }
3760
3761 #[test]
3762 fn raw_iteration_confined_to_maintenance_allowlist() {
3763 let allowlist: &[(&str, &str, usize, &str)] = &[
3771 (
3772 "runtime/query_exec/table.rs",
3773 include_str!("../../runtime/query_exec/table.rs"),
3774 1,
3775 "documented exception: schema inspection (does the collection ever \
3776 observe these fields?) is visibility-independent",
3777 ),
3778 (
3779 "runtime/query_exec/aggregate.rs",
3780 include_str!("../../runtime/query_exec/aggregate.rs"),
3781 0,
3782 "fully migrated by batch 1",
3783 ),
3784 (
3785 "runtime/query_exec/aggregate_pushdown_dispatch.rs",
3786 include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs"),
3787 0,
3788 "fully migrated by batch 1",
3789 ),
3790 (
3791 "runtime/query_exec/vector.rs",
3792 include_str!("../../runtime/query_exec/vector.rs"),
3793 0,
3794 "fully migrated by batch 2",
3795 ),
3796 (
3797 "runtime/ask_pipeline.rs",
3798 include_str!("../../runtime/ask_pipeline.rs"),
3799 0,
3800 "fully migrated by batch 2",
3801 ),
3802 (
3803 "runtime/impl_dml.rs",
3804 include_str!("../../runtime/impl_dml.rs"),
3805 1,
3806 "documented exception: ON CONFLICT resolves a unique key against \
3807 physical versions on purpose. A snapshot scan cannot see a row an \
3808 uncommitted concurrent writer already holds the key for, which is \
3809 exactly the case that must raise a serialization conflict rather \
3810 than insert a duplicate; the same pass reaps index entries left by \
3811 aborted writers. \
3812 TODO(#2263 follow-up): narrow the candidates through the unique \
3813 index instead of walking the collection",
3814 ),
3815 (
3816 "runtime/dml_target_scan.rs",
3817 include_str!("../../runtime/dml_target_scan.rs"),
3818 2,
3819 "documented exception: kind-conditional visibility the uniform scan \
3820 API cannot express; both iterations gate on candidate_visible",
3821 ),
3822 (
3823 "runtime/impl_events.rs",
3824 include_str!("../../runtime/impl_events.rs"),
3825 1,
3826 "documented exception: backfill dedup must include consumed messages, \
3827 so it is deliberately visibility-free",
3828 ),
3829 (
3830 "runtime/impl_graph_commands.rs",
3831 include_str!("../../runtime/impl_graph_commands.rs"),
3832 0,
3833 "native node properties resolve one snapshot-visible, RLS-admitted version",
3834 ),
3835 (
3836 "runtime/graph_dsl.rs",
3837 include_str!("../../runtime/graph_dsl.rs"),
3838 1,
3839 "remaining legacy debt: unused edge-property helper; search expansion \
3840 now uses snapshot visitors and RLS-gated hydration",
3841 ),
3842 (
3843 "runtime/graph_tvf.rs",
3844 include_str!("../../runtime/graph_tvf.rs"),
3845 0,
3846 "fully migrated by batch 2",
3847 ),
3848 (
3849 "runtime/impl_kv.rs",
3850 include_str!("../../runtime/impl_kv.rs"),
3851 3,
3852 "one non-versioned listing fallback (the versioned arm scans under \
3853 the captured snapshot). Point reads use physical key candidates with \
3854 versioned visibility rechecks. Vault reads use snapshot scans. Offline vault \
3855 export preserves all \
3856 physical versions across tenants; restore's emptiness check must reject \
3857 every physical row, including versions invisible to a snapshot",
3858 ),
3859 (
3860 "runtime/impl_queue.rs",
3861 include_str!("../../runtime/impl_queue.rs"),
3862 10,
3863 "maintenance: red_queue_meta bookkeeping (config, groups, acks, \
3864 pending, dedup TTL reaper, gauge counts) — queue message bodies are \
3865 read through scan. \
3866 TODO(#2138 follow-up): QUEUE PENDING projects meta rows to clients",
3867 ),
3868 (
3869 "runtime/primary_queue_store.rs",
3870 include_str!("../../runtime/primary_queue_store.rs"),
3871 3,
3872 "maintenance: generic red_queue_meta read/delete helpers and the \
3873 next-position allocator, which must not collide with rows invisible \
3874 to the writer's snapshot",
3875 ),
3876 (
3877 "runtime/record_search.rs",
3878 include_str!("../../runtime/record_search.rs"),
3879 0,
3880 "fully migrated by batch 2",
3881 ),
3882 (
3883 "runtime/search_graph.rs",
3884 include_str!("../../runtime/search_graph.rs"),
3885 0,
3886 "search graph preparation uses snapshot visitors and gated hydration",
3887 ),
3888 (
3889 "runtime/impl_search.rs",
3890 include_str!("../../runtime/impl_search.rs"),
3891 1,
3892 "maintenance: ask-audit retention sweep must see every physical row",
3893 ),
3894 ];
3895
3896 for (path, src, allowed, reason) in allowlist {
3897 let found = raw_iteration_sites(&production_source(src));
3898 assert_eq!(
3899 found, *allowed,
3900 "{path}: {found} raw-iteration sites, allowlist permits {allowed} \
3901 ({reason}). Raw iteration bypasses MVCC visibility — use \
3902 SegmentManager::scan / scan_for_each / scan_fold_parallel / \
3903 scan_zoned_with_stats, or extend the allowlist with a maintenance \
3904 justification."
3905 );
3906 }
3907 }
3908
3909 #[test]
3910 fn explicit_snapshot_scan_ignores_ambient_thread_local() {
3911 let manager = Arc::new(SegmentManager::new("accounts"));
3912 let versions = [(0, 0), (3, 0), (11, 0), (4, 0), (3, 7), (3, 11), (3, 4)];
3914 for (index, (xmin, xmax)) in versions.iter().enumerate() {
3915 let row_id = index as u64 + 1;
3916 let mut entity = UnifiedEntity::table_row(
3917 EntityId::new(row_id),
3918 "accounts",
3919 row_id,
3920 vec![Value::Integer(row_id as i64)],
3921 );
3922 entity.set_xmin(*xmin);
3923 entity.set_xmax(*xmax);
3924 manager.insert(entity).unwrap();
3925 }
3926
3927 let explicit = SnapshotContext {
3928 snapshot: Snapshot {
3929 xid: 10,
3930 in_progress: HashSet::from([4]),
3931 },
3932 manager: Arc::new(SnapshotManager::new()),
3933 own_xids: HashSet::new(),
3934 requires_index_fallback: false,
3935 serializable_reader: None,
3936 };
3937 let decoy = SnapshotContext {
3940 snapshot: Snapshot {
3941 xid: 1,
3942 in_progress: HashSet::new(),
3943 },
3944 manager: Arc::new(SnapshotManager::new()),
3945 own_xids: HashSet::new(),
3946 requires_index_fallback: false,
3947 serializable_reader: None,
3948 };
3949
3950 let row_ids = |entities: Vec<UnifiedEntity>| -> HashSet<u64> {
3951 entities
3952 .into_iter()
3953 .filter_map(|entity| match entity.kind {
3954 EntityKind::TableRow { row_id, .. } => Some(row_id),
3955 _ => None,
3956 })
3957 .collect()
3958 };
3959
3960 let guard = crate::runtime::execution_context::CurrentSnapshotGuard::install(decoy);
3961 let on_installing_thread = row_ids(manager.scan(Some(&explicit), |_| true));
3962 drop(guard);
3963
3964 let spawned_manager = Arc::clone(&manager);
3965 let spawned_snapshot = explicit.clone();
3966 let on_fresh_thread = std::thread::spawn(move || {
3967 row_ids(spawned_manager.scan(Some(&spawned_snapshot), |_| true))
3968 })
3969 .join()
3970 .unwrap();
3971
3972 assert_eq!(
3973 on_installing_thread, on_fresh_thread,
3974 "an explicit snapshot must produce the same visible set on any thread"
3975 );
3976 assert_eq!(on_installing_thread, HashSet::from([1, 2, 6, 7]));
3977
3978 let raw_manager = Arc::clone(&manager);
3981 let raw_count = std::thread::spawn(move || raw_manager.query_all(|_| true).len())
3982 .join()
3983 .unwrap();
3984 assert_eq!(
3985 raw_count,
3986 versions.len(),
3987 "raw iteration must expose every physical version"
3988 );
3989 }
3990
3991 #[test]
3992 fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3993 let manager = SegmentManager::new("test_collection");
3994
3995 let entity = UnifiedEntity::table_row(
3996 manager.next_entity_id(),
3997 "users",
3998 1,
3999 vec![Value::text("Alice".to_string())],
4000 );
4001
4002 let id = manager.insert(entity).unwrap();
4003
4004 assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
4005 assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
4006 assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
4007
4008 assert!(manager.delete(id).unwrap());
4009 assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
4010 }
4011
4012 #[test]
4013 fn zoned_scan_reports_pruned_segments_and_preserves_results() {
4014 let config = ManagerConfig {
4015 segment_config: SegmentConfig {
4016 max_entities: 2,
4017 ..Default::default()
4018 },
4019 ..Default::default()
4020 };
4021 let manager = SegmentManager::with_config("events", config);
4022
4023 for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
4024 let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
4025 let entity = UnifiedEntity::new(
4026 manager.next_entity_id(),
4027 EntityKind::TableRow {
4028 table: "events".into(),
4029 row_id: (idx + 1) as u64,
4030 },
4031 EntityData::Row(row),
4032 );
4033 manager.insert(entity).unwrap();
4034 }
4035
4036 let full = manager.query_all(|entity| match &entity.data {
4037 EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
4038 _ => false,
4039 });
4040 let probe = Value::Integer(100);
4041 let (pruned, stats) =
4042 manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
4043 match &entity.data {
4044 EntityData::Row(row) => {
4045 matches!(row.get_field("ts"), Some(Value::Integer(100)))
4046 }
4047 _ => false,
4048 }
4049 });
4050
4051 assert_eq!(pruned.len(), full.len());
4052 assert_eq!(pruned[0].id, full[0].id);
4053 assert_eq!(stats.segments_total, 3);
4054 assert_eq!(stats.segments_scanned, 2);
4055 assert_eq!(stats.segments_pruned, 1);
4056 }
4057
4058 #[test]
4059 fn test_manager_auto_seal() {
4060 let config = ManagerConfig {
4061 segment_config: SegmentConfig {
4062 max_entities: 2,
4063 ..Default::default()
4064 },
4065 ..Default::default()
4066 };
4067
4068 let manager = SegmentManager::with_config("test", config);
4069
4070 manager
4072 .insert(UnifiedEntity::vector(
4073 manager.next_entity_id(),
4074 "v",
4075 vec![0.1],
4076 ))
4077 .unwrap();
4078
4079 manager
4081 .insert(UnifiedEntity::vector(
4082 manager.next_entity_id(),
4083 "v",
4084 vec![0.2],
4085 ))
4086 .unwrap();
4087
4088 manager
4090 .insert(UnifiedEntity::vector(
4091 manager.next_entity_id(),
4092 "v",
4093 vec![0.3],
4094 ))
4095 .unwrap();
4096
4097 let stats = manager.stats();
4098 assert_eq!(stats.total_entities, 3);
4099 }
4100
4101 #[test]
4102 fn test_manager_delete() {
4103 let manager = SegmentManager::new("test");
4104
4105 let id = manager
4106 .insert(UnifiedEntity::vector(
4107 manager.next_entity_id(),
4108 "v",
4109 vec![0.1],
4110 ))
4111 .unwrap();
4112
4113 assert!(manager.get(id).is_some());
4114 assert!(manager.delete(id).unwrap());
4115 assert!(manager.get(id).is_none());
4116 }
4117
4118 #[test]
4119 fn test_manager_metadata() {
4120 let manager = SegmentManager::new("test");
4121
4122 let id = manager
4123 .insert(UnifiedEntity::table_row(
4124 manager.next_entity_id(),
4125 "hosts",
4126 1,
4127 vec![Value::text("192.168.1.1".to_string())],
4128 ))
4129 .unwrap();
4130
4131 let mut meta = Metadata::new();
4132 meta.set(
4133 "os",
4134 super::super::metadata::MetadataValue::String("linux".to_string()),
4135 );
4136
4137 manager.set_metadata(id, meta).unwrap();
4138
4139 let retrieved = manager.get_metadata(id).unwrap();
4140 assert!(retrieved.has("os"));
4141 }
4142
4143 #[test]
4144 fn test_manager_query_by_kind() {
4145 let manager = SegmentManager::new("test");
4146
4147 manager
4148 .insert(UnifiedEntity::table_row(
4149 manager.next_entity_id(),
4150 "hosts",
4151 1,
4152 vec![],
4153 ))
4154 .unwrap();
4155
4156 manager
4157 .insert(UnifiedEntity::vector(
4158 manager.next_entity_id(),
4159 "embeddings",
4160 vec![0.1],
4161 ))
4162 .unwrap();
4163
4164 manager
4165 .insert(UnifiedEntity::table_row(
4166 manager.next_entity_id(),
4167 "hosts",
4168 2,
4169 vec![],
4170 ))
4171 .unwrap();
4172
4173 let rows = manager.get_by_kind("table");
4174 assert_eq!(rows.len(), 2);
4175
4176 let vectors = manager.get_by_kind("vector");
4177 assert_eq!(vectors.len(), 1);
4178 }
4179
4180 fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
4187 SegmentManager::with_config(
4188 "test",
4189 ManagerConfig {
4190 max_sealed_segments: 1_000,
4191 consolidation_entities_per_tick: entities_per_tick,
4192 ..Default::default()
4193 },
4194 )
4195 }
4196
4197 fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
4201 let entities: Vec<UnifiedEntity> = (0..count)
4202 .map(|i| {
4203 UnifiedEntity::table_row(
4204 manager.next_entity_id(),
4205 "users",
4206 i as u64,
4207 vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
4208 )
4209 })
4210 .collect();
4211 let ids = manager.bulk_insert(entities).expect("bulk insert");
4212 manager.force_seal().expect("seal");
4213 ids
4214 }
4215
4216 fn live_ids(manager: &SegmentManager) -> Vec<u64> {
4217 let mut ids: Vec<u64> = manager
4218 .query_all(|_| true)
4219 .into_iter()
4220 .map(|entity| entity.id.raw())
4221 .collect();
4222 ids.sort_unstable();
4223 ids
4224 }
4225
4226 fn drain_maintenance(manager: &SegmentManager) -> usize {
4227 let mut ticks = 0;
4228 while manager.consolidation.read().is_some() || ticks == 0 {
4229 manager.run_maintenance().expect("maintenance");
4230 ticks += 1;
4231 assert!(ticks < 1_000, "consolidation failed to converge");
4232 }
4233 ticks
4234 }
4235
4236 #[test]
4237 fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
4238 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4239 let ids = manager
4240 .bulk_insert(
4241 (1..=1024)
4242 .map(|id| {
4243 UnifiedEntity::graph_edge(
4244 EntityId::new(id),
4245 "step",
4246 "2000",
4247 "3000",
4248 1.0,
4249 HashMap::new(),
4250 )
4251 })
4252 .collect(),
4253 )
4254 .expect("edges");
4255 manager.force_seal().expect("seal");
4256 for id in ids.iter().take(300) {
4257 manager.delete(*id).expect("fragment source");
4258 }
4259 let source = Arc::downgrade(&manager.sealed.read()[0]);
4260 let mut seen = Vec::new();
4261 let mut changed = false;
4262 let mut metadata_bytes = 0;
4263 assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
4264 || true,
4265 |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
4266 |batch| {
4267 assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
4268 assert!(manager.growing.try_write().is_some(), "no growing topology guard");
4269 assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
4270 assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
4271 if !changed {
4272 changed = true;
4273 drain_maintenance(&manager);
4274 assert!(manager.stats().consolidation.runs_completed > 0);
4275 assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
4276 let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
4277 if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
4278 manager.update(updated).expect("update authoritative merged copy");
4279 }
4280 for id in batch {
4281 let entity = manager.get(*id).expect("hydrate current manager");
4282 if id.raw() == 1024 {
4283 assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
4284 }
4285 seen.push(id.raw());
4286 }
4287 Ok(true)
4288 }).expect("cursor"));
4289 assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
4290 assert!(
4291 source.upgrade().is_none(),
4292 "finished cursor releases retired source"
4293 );
4294 assert!(
4295 metadata_bytes < 4096,
4296 "fixed ID buffer plus source handles: {metadata_bytes}"
4297 );
4298 }
4299
4300 #[test]
4301 fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
4302 let manager = SegmentManager::new("graph");
4303 manager
4304 .bulk_insert(
4305 (1..=1024)
4306 .map(|id| {
4307 UnifiedEntity::graph_edge(
4308 EntityId::new(id),
4309 "step",
4310 "2000",
4311 "3000",
4312 1.0,
4313 HashMap::new(),
4314 )
4315 })
4316 .collect(),
4317 )
4318 .expect("edges");
4319 let mut delivered = 0;
4320 let denied = manager.visit_graph_index_batches(
4321 GraphEntityKind::Edge,
4322 &[EntityId::new(2000)],
4323 || true,
4324 |_| Err("out of memory"),
4325 |ids| {
4326 delivered += ids.len();
4327 Ok(true)
4328 },
4329 );
4330 assert_eq!(denied, Err("out of memory"));
4331 assert_eq!(delivered, 0);
4332 let mut work = 0;
4333 assert!(!manager
4334 .visit_graph_index_batches(
4335 GraphEntityKind::Edge,
4336 &[EntityId::new(2000)],
4337 || {
4338 work += 1;
4339 work < 100
4340 },
4341 |_| Ok::<_, ()>(()),
4342 |ids| {
4343 delivered += ids.len();
4344 Ok(true)
4345 }
4346 )
4347 .expect("cancelled cursor"));
4348 assert_eq!(
4349 delivered, 0,
4350 "cancelled partial batch cannot reach consumer"
4351 );
4352 assert!(!manager
4353 .visit_graph_index_batches(
4354 GraphEntityKind::Edge,
4355 &[EntityId::new(2000)],
4356 || true,
4357 |_| Ok::<_, ()>(()),
4358 |ids| {
4359 delivered += ids.len();
4360 Ok(false)
4361 }
4362 )
4363 .expect("consumer stop"));
4364 assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
4365 }
4366
4367 #[test]
4368 fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
4369 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4370 let ids = seal_bulk_segment(&manager, 100);
4371 for id in ids.iter().take(40) {
4372 manager.delete(*id).expect("delete before scan");
4373 }
4374 let source_bytes = manager.resident_bytes();
4375 let source = Arc::downgrade(&manager.sealed.read()[0]);
4376 std::thread::scope(|scope| {
4377 let (ready, waiting) = std::sync::mpsc::channel();
4378 let (release_first, first_wait) = std::sync::mpsc::channel();
4379 let (release_last, last_wait) = std::sync::mpsc::channel();
4380 let reader = |resume: std::sync::mpsc::Receiver<()>,
4381 ready: std::sync::mpsc::Sender<()>| {
4382 assert!(manager.scan_batches(
4383 None,
4384 || true,
4385 |_| {
4386 ready.send(()).expect("signal captured cursor");
4387 let _ = resume.recv();
4388 true
4389 }
4390 ));
4391 };
4392 let ready_first = ready.clone();
4393 let first = scope.spawn(move || reader(first_wait, ready_first));
4394 let last = scope.spawn(move || reader(last_wait, ready));
4395 for _ in 0..2 {
4396 waiting
4397 .recv_timeout(std::time::Duration::from_secs(10))
4398 .expect("reader ready");
4399 }
4400 drain_maintenance(&manager);
4401 assert!(
4402 source.upgrade().is_some(),
4403 "both cursors pin the retired source"
4404 );
4405 let active_bytes = manager.sealed.read()[0].read().resident_bytes();
4406 let held = manager.resident_bytes();
4407 assert!(
4408 held >= active_bytes + source_bytes,
4409 "retired source remains resident"
4410 );
4411 release_first.send(()).expect("release first reader");
4412 first.join().expect("first reader");
4413 assert_eq!(
4414 manager.resident_bytes(),
4415 held,
4416 "one reader still pins the source"
4417 );
4418 release_last.send(()).expect("release last reader");
4419 last.join().expect("last reader");
4420 assert!(
4421 source.upgrade().is_none(),
4422 "accounting must not retain payloads"
4423 );
4424 assert_eq!(held - manager.resident_bytes(), source_bytes);
4425 assert_eq!(
4426 manager.stats().total_memory_bytes as u64,
4427 manager.resident_bytes()
4428 );
4429 });
4430 }
4431
4432 #[test]
4433 fn retained_memory_gates_runtime_growth_until_the_cursor_is_cancelled() {
4434 use crate::storage::memory_pools::MemoryPool;
4435 use crate::{RedDBOptions, RedDBRuntime};
4436
4437 let budget = 128 * 1024;
4438 let runtime =
4439 RedDBRuntime::with_options(RedDBOptions::in_memory().with_memory_budget(budget))
4440 .expect("runtime");
4441 runtime
4442 .db()
4443 .store()
4444 .create_collection("test")
4445 .expect("collection");
4446 let manager = runtime
4447 .db()
4448 .store()
4449 .get_collection("test")
4450 .expect("manager");
4451 let ids = seal_bulk_segment(&manager, 100);
4452 for id in ids.iter().take(40) {
4453 manager.delete(*id).expect("delete before scan");
4454 }
4455 let source_bytes = manager.resident_bytes();
4456 let mut growth = 0;
4457 let mut held_used = 0;
4458 assert!(!manager.scan_batches(
4459 None,
4460 || true,
4461 |_| {
4462 drain_maintenance(&manager);
4463 runtime.refresh_memory_accounting();
4464 held_used = runtime.memory_accounting().total_used_bytes();
4467 growth = budget
4468 .checked_sub(held_used)
4469 .expect("fixture fits while the cursor is open")
4470 + source_bytes / 2;
4471 assert!(growth > 0);
4472 assert!(
4473 runtime
4474 .admit_non_evictable_growth(
4475 MemoryPool::SegmentArena,
4476 "growth with pinned source",
4477 growth,
4478 )
4479 .is_err(),
4480 "retained source must participate in runtime admission"
4481 );
4482 false
4483 }
4484 ));
4485 runtime.refresh_memory_accounting();
4486 assert_eq!(
4487 held_used - runtime.memory_accounting().total_used_bytes(),
4488 source_bytes
4489 );
4490 let _reservation = runtime
4491 .admit_non_evictable_growth(
4492 MemoryPool::SegmentArena,
4493 "growth after cursor cancellation",
4494 growth,
4495 )
4496 .expect("the same growth fits once the cursor releases its source");
4497 }
4498
4499 #[test]
4500 fn retained_memory_includes_an_inflight_consolidation_copy() {
4501 let manager = consolidating_manager(1);
4502 let ids = seal_bulk_segment(&manager, 100);
4503 for id in ids.iter().take(40) {
4504 manager.delete(*id).expect("delete");
4505 }
4506 let before = manager.resident_bytes();
4507 assert!(manager.pressure_consolidation_tick());
4508 let copied = manager
4509 .consolidation
4510 .read()
4511 .as_ref()
4512 .expect("paced merge")
4513 .merged
4514 .resident_bytes();
4515 assert!(copied > 0);
4516 assert!(
4517 manager.resident_bytes() >= before + copied,
4518 "unpublished copy consumes memory"
4519 );
4520 drain_maintenance(&manager);
4521 assert!(
4522 manager.resident_bytes() < before,
4523 "completed merge releases the old storage"
4524 );
4525 }
4526
4527 #[test]
4528 fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
4529 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4530 let ids = seal_bulk_segment(&manager, 100);
4531
4532 let bytes_before_delete = manager.resident_bytes();
4533
4534 for id in ids.iter().take(40) {
4536 assert!(manager.delete(*id).expect("delete"));
4537 }
4538
4539 let bytes_with_tombstones = manager.resident_bytes();
4542 assert!(bytes_with_tombstones > bytes_before_delete);
4543
4544 let survivors = live_ids(&manager);
4545 assert_eq!(survivors.len(), 60);
4546
4547 drain_maintenance(&manager);
4548
4549 let stats = manager.stats();
4550 assert_eq!(stats.consolidation.runs_started, 1);
4551 assert_eq!(stats.consolidation.runs_completed, 1);
4552 assert_eq!(stats.consolidation.segments_merged, 1);
4553 assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
4554 assert!(stats.consolidation.bytes_reclaimed > 0);
4555
4556 let bytes_after = manager.resident_bytes();
4558 assert!(
4559 bytes_after <= bytes_before_delete,
4560 "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
4561 );
4562 assert_eq!(
4563 bytes_with_tombstones - bytes_after,
4564 stats.consolidation.bytes_reclaimed
4565 );
4566 assert_eq!(stats.total_memory_bytes, bytes_after as usize);
4567
4568 assert_eq!(live_ids(&manager), survivors);
4569 assert_eq!(manager.sealed.read().len(), 1);
4570 assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
4571 }
4572
4573 #[test]
4574 fn query_results_are_identical_before_during_and_after_consolidation() {
4575 let manager = consolidating_manager(3);
4576 let ids = seal_bulk_segment(&manager, 20);
4577 seal_bulk_segment(&manager, 20);
4578 for id in ids.iter().take(10) {
4579 assert!(manager.delete(*id).expect("delete"));
4580 }
4581
4582 let before = live_ids(&manager);
4583 let survivor = *ids.last().expect("at least one row");
4584 let tombstoned = ids[0];
4585
4586 let mut mid_tick_observations = 0;
4589 while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
4590 manager.run_maintenance().expect("maintenance");
4591 assert_eq!(live_ids(&manager), before);
4592 assert!(manager.get(survivor).is_some());
4593 assert!(manager.get(tombstoned).is_none());
4594 assert_eq!(manager.get_by_kind("table").len(), before.len());
4595 mid_tick_observations += 1;
4596 assert!(mid_tick_observations < 1_000);
4597 }
4598
4599 assert!(
4600 mid_tick_observations > 2,
4601 "expected the run to span several ticks, saw {mid_tick_observations}"
4602 );
4603 assert_eq!(live_ids(&manager), before);
4604 assert!(manager.get(survivor).is_some());
4605 assert!(manager.get(tombstoned).is_none());
4606 assert_eq!(manager.get_by_kind("table").len(), before.len());
4607 }
4608
4609 #[test]
4610 fn get_by_kind_reaches_sealed_segments() {
4611 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4612 let ids = seal_bulk_segment(&manager, 5);
4613 assert!(manager.delete(ids[0]).expect("delete"));
4614
4615 assert_eq!(manager.get_by_kind("table").len(), 4);
4619 assert_eq!(manager.get_by_kind("vector").len(), 0);
4620 }
4621
4622 #[test]
4623 fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4624 const PER_TICK: usize = 4;
4625 let manager = consolidating_manager(PER_TICK);
4626 let ids = seal_bulk_segment(&manager, 60);
4627 for id in ids.iter().take(30) {
4628 assert!(manager.delete(*id).expect("delete"));
4629 }
4630
4631 manager.run_maintenance().expect("maintenance");
4633 assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4634
4635 let mut previous = PER_TICK as u64;
4638 let mut ticks = 1;
4639 while let Some(copied) = manager.consolidation_progress() {
4640 assert!(
4641 copied - previous <= PER_TICK as u64,
4642 "tick copied {} entities, bound is {PER_TICK}",
4643 copied - previous
4644 );
4645 previous = copied;
4646 manager.run_maintenance().expect("maintenance");
4647 ticks += 1;
4648 assert!(ticks < 100);
4649 }
4650
4651 assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4654 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4655 assert_eq!(live_ids(&manager).len(), 30);
4656 }
4657
4658 #[test]
4659 fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4660 let manager = SegmentManager::with_config(
4661 "test",
4662 ManagerConfig {
4663 max_sealed_segments: 3,
4664 ..Default::default()
4665 },
4666 );
4667 for _ in 0..4 {
4668 seal_bulk_segment(&manager, 5);
4669 }
4670 assert_eq!(manager.sealed.read().len(), 4);
4671
4672 let before = live_ids(&manager);
4673 drain_maintenance(&manager);
4674
4675 assert_eq!(manager.sealed.read().len(), 1);
4678 assert_eq!(manager.stats().consolidation.segments_merged, 4);
4679 assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4680 assert_eq!(live_ids(&manager), before);
4681 }
4682
4683 #[test]
4684 fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4685 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4686 seal_bulk_segment(&manager, 10);
4687 seal_bulk_segment(&manager, 10);
4688
4689 for _ in 0..5 {
4690 manager.run_maintenance().expect("maintenance");
4691 }
4692
4693 assert_eq!(manager.stats().consolidation.runs_started, 0);
4694 assert_eq!(manager.sealed.read().len(), 2);
4695 }
4696
4697 #[test]
4698 fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4699 use std::sync::atomic::AtomicBool;
4700
4701 let manager = Arc::new(consolidating_manager(2));
4702 let ids = seal_bulk_segment(&manager, 40);
4703 seal_bulk_segment(&manager, 40);
4704 for id in ids.iter().take(20) {
4705 assert!(manager.delete(*id).expect("delete"));
4706 }
4707
4708 let expected = live_ids(&manager);
4709 let stop = Arc::new(AtomicBool::new(false));
4710
4711 let readers: Vec<_> = (0..4)
4712 .map(|_| {
4713 let manager = Arc::clone(&manager);
4714 let stop = Arc::clone(&stop);
4715 let expected = expected.clone();
4716 std::thread::spawn(move || {
4717 let mut scans = 0u64;
4718 loop {
4721 let mut seen: Vec<u64> = manager
4722 .query_all(|_| true)
4723 .into_iter()
4724 .map(|entity| entity.id.raw())
4725 .collect();
4726 seen.sort_unstable();
4727 assert_eq!(seen, expected, "reader saw a torn sealed set");
4730 scans += 1;
4731 if stop.load(Ordering::Relaxed) {
4732 break;
4733 }
4734 }
4735 scans
4736 })
4737 })
4738 .collect();
4739
4740 drain_maintenance(&manager);
4741 stop.store(true, Ordering::Relaxed);
4742
4743 for reader in readers {
4744 let scans = reader.join().expect("reader thread");
4745 assert!(scans > 0, "reader never got to scan");
4746 }
4747
4748 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4749 assert_eq!(live_ids(&manager), expected);
4750 }
4751
4752 #[test]
4753 fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4754 let manager = consolidating_manager(4);
4755 let ids = seal_bulk_segment(&manager, 20);
4756 for id in ids.iter().take(6) {
4757 assert!(manager.delete(*id).expect("delete"));
4758 }
4759
4760 manager.run_maintenance().expect("maintenance");
4762 assert_eq!(manager.consolidation_progress(), Some(4));
4763 let doomed = ids[6];
4764 assert!(manager.delete(doomed).expect("delete"));
4765
4766 let expected = live_ids(&manager);
4767 drain_maintenance(&manager);
4768
4769 assert!(manager.get(doomed).is_none(), "deleted row came back");
4770 assert_eq!(live_ids(&manager), expected);
4771 }
4772
4773 #[test]
4774 fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
4775 let manager = consolidating_manager(3);
4776 let ids = seal_bulk_segment(&manager, 30);
4777 for id in ids.iter().take(12) {
4778 assert!(manager.delete(*id).expect("delete"));
4779 }
4780
4781 let before = live_ids(&manager);
4782 let sealed_before: Vec<SegmentId> = manager
4783 .sealed
4784 .read()
4785 .iter()
4786 .map(|segment| segment.read().id())
4787 .collect();
4788
4789 manager.run_maintenance().expect("maintenance");
4790 manager.run_maintenance().expect("maintenance");
4791 assert!(manager.consolidation.read().is_some());
4792
4793 *manager.consolidation.write() = None;
4797
4798 assert_eq!(live_ids(&manager), before);
4799 let sealed_after: Vec<SegmentId> = manager
4800 .sealed
4801 .read()
4802 .iter()
4803 .map(|segment| segment.read().id())
4804 .collect();
4805 assert_eq!(sealed_after, sealed_before);
4806 assert_eq!(manager.stats().consolidation.runs_completed, 0);
4807
4808 drain_maintenance(&manager);
4809 assert_eq!(live_ids(&manager), before);
4810 assert_eq!(manager.stats().consolidation.runs_completed, 1);
4811 }
4812
4813 #[test]
4814 fn consolidation_preserves_entity_payload_metadata_and_sequence() {
4815 let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4816
4817 let entities: Vec<UnifiedEntity> = (0..10)
4821 .map(|i| {
4822 UnifiedEntity::table_row(
4823 manager.next_entity_id(),
4824 "users",
4825 i as u64,
4826 vec![Value::Integer(i as i64)],
4827 )
4828 })
4829 .collect();
4830 let ids = manager.bulk_insert(entities).expect("bulk insert");
4831
4832 let mut meta = Metadata::new();
4833 meta.set(
4834 "os",
4835 super::super::metadata::MetadataValue::String("linux".to_string()),
4836 );
4837 manager.set_metadata(ids[9], meta).expect("set metadata");
4838 manager.force_seal().expect("seal");
4839
4840 assert!(manager.delete(ids[0]).expect("delete"));
4841 assert!(manager.delete(ids[1]).expect("delete"));
4842 assert!(manager.delete(ids[2]).expect("delete"));
4843
4844 let before: Vec<UnifiedEntity> = {
4845 let mut entities = manager.query_all(|_| true);
4846 entities.sort_by_key(|entity| entity.id.raw());
4847 entities
4848 };
4849
4850 drain_maintenance(&manager);
4851
4852 let after: Vec<UnifiedEntity> = {
4853 let mut entities = manager.query_all(|_| true);
4854 entities.sort_by_key(|entity| entity.id.raw());
4855 entities
4856 };
4857
4858 let columns = |entity: &UnifiedEntity| match &entity.data {
4859 super::super::entity::EntityData::Row(row) => row.columns.clone(),
4860 _ => Vec::new(),
4861 };
4862
4863 assert_eq!(before.len(), after.len());
4864 for (old, new) in before.iter().zip(after.iter()) {
4865 assert_eq!(old.id, new.id);
4866 assert_eq!(columns(old), columns(new));
4867 assert_eq!(
4868 old.sequence_id, new.sequence_id,
4869 "merge must not re-issue sequence ids"
4870 );
4871 }
4872 assert!(manager
4873 .get_metadata(ids[9])
4874 .expect("metadata survives the merge")
4875 .has("os"));
4876 }
4877
4878 #[test]
4879 #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
4880 fn test_lifecycle_events() {
4881 let manager = SegmentManager::new("test");
4882
4883 manager
4884 .insert(UnifiedEntity::vector(
4885 manager.next_entity_id(),
4886 "v",
4887 vec![0.1],
4888 ))
4889 .unwrap();
4890
4891 let events = manager.drain_events();
4892
4893 assert!(events
4895 .iter()
4896 .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
4897 assert!(events
4898 .iter()
4899 .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
4900 }
4901}