Skip to main content

reddb_server/storage/unified/
manager.rs

1//! Segment Manager
2//!
3//! Manages the lifecycle of unified segments: creation, sealing, consolidation,
4//! and archival. Coordinates writes to growing segments and queries across
5//! all segments.
6//!
7//! # Responsibilities
8//!
9//! - Route writes to the active growing segment
10//! - Auto-seal segments when thresholds are met
11//! - Coordinate queries across multiple segments
12//! - Paced consolidation of sealed segments (ADR 0073 §5)
13//! - Archive old segments to cold storage
14
15mod graph_index_merge;
16
17use parking_lot::RwLock;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Arc, Weak};
21
22use super::entity::{EntityId, EntityKind, UnifiedEntity};
23use super::metadata::{Metadata, MetadataFilter};
24use super::segment::{
25    GraphEntityKind, GrowingSegment, SegmentConfig, SegmentError, SegmentId, SegmentState,
26    SegmentStats, UnifiedSegment, ZoneColPred, ZoneColPredKind,
27};
28use super::visibility_map::VisibilityMap;
29use crate::runtime::mvcc::{entity_visible_with_context, SnapshotContext};
30
31/// Fraction of a collection's sealed entities that must be tombstoned before
32/// consolidation is worth its cost.
33///
34/// Tunable implementation constant, not a contract: it is surfaced read-only in
35/// `red.stats` so an operator can see what the engine decided, and it may move
36/// between releases without notice.
37pub const CONSOLIDATION_TOMBSTONE_RATIO: f64 = 0.20;
38
39/// Fraction of a collection's sealed bytes that consolidation would return to
40/// the budget (dead entity payloads plus the tombstone sets that bury them)
41/// before consolidation is worth its cost. See [`CONSOLIDATION_TOMBSTONE_RATIO`]
42/// on tunability.
43pub const CONSOLIDATION_FRAGMENTATION_RATIO: f64 = 0.30;
44
45/// Entities copied into the merged segment per maintenance tick.
46///
47/// ADR 0038 §3: consolidation is paced. A tick copies at most this many
48/// entities and returns; the half-built merged segment is carried to the next
49/// tick. No consolidation ever runs to completion in one unbounded pass.
50pub const CONSOLIDATION_ENTITIES_PER_TICK: usize = 4_096;
51
52/// Configuration for the segment manager
53#[derive(Debug, Clone)]
54pub struct ManagerConfig {
55    /// Segment configuration
56    pub segment_config: SegmentConfig,
57    /// Maximum number of sealed segments before consolidation
58    pub max_sealed_segments: usize,
59    /// Idle time (seconds) before auto-sealing
60    pub idle_seal_secs: u64,
61    /// Enable paced sealed-segment consolidation
62    pub enable_consolidation: bool,
63    /// Entities copied per maintenance tick while a consolidation is running
64    pub consolidation_entities_per_tick: usize,
65    /// Enable background archival
66    pub enable_archival: bool,
67    /// Age threshold for archival (seconds)
68    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, // 5 minutes
77            enable_consolidation: true,
78            consolidation_entities_per_tick: CONSOLIDATION_ENTITIES_PER_TICK,
79            enable_archival: true,
80            archive_age_secs: 86400 * 7, // 7 days
81        }
82    }
83}
84
85/// Consolidation counters for one collection (ADR 0073 §5).
86///
87/// These replace the `compact_ops` counter, which only ever counted a
88/// do-nothing branch.
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct ConsolidationStats {
91    /// Consolidations that crossed a threshold and began copying.
92    pub runs_started: u64,
93    /// Consolidations that finished the swap.
94    pub runs_completed: u64,
95    /// Source segments retired by a completed swap.
96    pub segments_merged: u64,
97    /// Tombstones garbage-collected by completed swaps.
98    pub tombstones_reclaimed: u64,
99    /// Logical source-to-merged footprint reduction; readers may retain sources.
100    pub bytes_reclaimed: u64,
101}
102
103/// Manager statistics
104#[derive(Debug, Clone, Default)]
105pub struct ManagerStats {
106    /// Total entities across all segments
107    pub total_entities: usize,
108    /// Number of growing segments
109    pub growing_count: usize,
110    /// Number of sealed segments
111    pub sealed_count: usize,
112    /// Number of archived segments
113    pub archived_count: usize,
114    /// Total memory usage
115    pub total_memory_bytes: usize,
116    /// Number of seal operations
117    pub seal_ops: u64,
118    /// Consolidation counters
119    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/// Lifecycle events for monitoring
130#[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
143/// A consolidation in flight, carried across maintenance ticks.
144///
145/// The merged segment is built off to the side and is invisible to readers
146/// until the swap. Nothing here is durable: sealed segments are the truth (via
147/// the normal checkpoint path), so a crash mid-consolidation simply drops this
148/// state and the next boot starts over.
149struct Consolidation {
150    /// Source segments, frozen when the run started.
151    sources: Vec<SourceProgress>,
152    /// Id reserved for the merged segment.
153    merged_id: SegmentId,
154    /// The half-built merged segment.
155    merged: GrowingSegment,
156    /// Index into `sources` of the segment currently being copied.
157    cursor: usize,
158    /// Live ids of `sources[cursor]`, snapshotted when that source was opened.
159    pending_ids: Vec<EntityId>,
160    /// How many of `pending_ids` have been consumed.
161    pending_cursor: usize,
162    /// Entities copied so far, across every tick of this run.
163    entities_copied: u64,
164}
165
166/// Per-source bookkeeping for a consolidation in flight.
167struct SourceProgress {
168    id: SegmentId,
169    /// Ids copied out of this source into the merged segment.
170    copied: Vec<EntityId>,
171    /// The source's tombstone count when the run opened it, and `None` before
172    /// that. Tombstones only ever grow, so a source whose count is unchanged at
173    /// swap time saw no deletes after we started reading it — and every row we
174    /// copied out of it is still live. Anything else needs a per-row check.
175    ///
176    /// Recording this when the source is *opened*, not when it is drained, is
177    /// what makes the check sound: a row deleted between two copy ticks would
178    /// otherwise be invisible to the swap and get resurrected.
179    tombstones_at_open: Option<usize>,
180}
181
182/// Segment manager for a collection
183pub struct SegmentManager {
184    /// Statement-scoped constraint admission. Reentrant because SQL upserts
185    /// invoke the same typed mutation APIs while holding this collection gate.
186    row_constraint_lock: Arc<parking_lot::ReentrantMutex<()>>,
187    /// Collection name
188    collection: String,
189    /// Configuration
190    config: ManagerConfig,
191    /// Next segment ID counter
192    next_segment_id: AtomicU64,
193    /// Next entity ID counter
194    next_entity_id: AtomicU64,
195    /// Per-table auto-increment row ID (1, 2, 3... per collection)
196    next_row_id: AtomicU64,
197    /// Hot-path entity counter — lock-free, updated by every insert/delete.
198    /// Replaces stats.total_entities on the write path to eliminate a lock
199    /// acquisition per row (from 4 lock ops per insert → 2).
200    total_entities_atomic: AtomicU64,
201    /// Currently active growing segment
202    growing: RwLock<Option<Arc<RwLock<GrowingSegment>>>>,
203    /// Sealed segments (immutable, queryable)
204    sealed: RwLock<Vec<Arc<RwLock<GrowingSegment>>>>,
205    /// Retired sources still owned by readers. Weak handles never prolong payload lifetime.
206    retired: RwLock<Vec<Weak<RwLock<GrowingSegment>>>>,
207    /// Archived segment IDs (stored externally)
208    archived: RwLock<Vec<SegmentId>>,
209    /// Entity to segment mapping (for fast lookups by individually-inserted entities).
210    /// Bulk-inserted entities skip this map; their segment is found by sequential scan
211    /// of growing + sealed segments in get()/update()/delete().
212    entity_segment: RwLock<HashMap<EntityId, SegmentId>>,
213    /// Shared column schema: column_name → index in Vec<Value>.
214    /// Populated on first bulk_insert. Enables columnar storage (Vec instead of HashMap per row).
215    column_schema: RwLock<Option<Arc<Vec<String>>>>,
216    /// Statistics (slow path — not updated on every insert).
217    stats: RwLock<ManagerStats>,
218    /// Consolidation in flight, if any. Only `run_maintenance` touches it, so
219    /// at most one consolidation runs per collection at a time.
220    consolidation: RwLock<Option<Consolidation>>,
221    /// Event listeners (simplified - would be channels in production)
222    events: RwLock<Vec<LifecycleEvent>>,
223    /// Visibility map: sealed segment entity ranges marked as all-visible.
224    /// Growing segment is never all-visible (writes are in-flight).
225    /// Used by index-only scan decisions.
226    visibility_map: VisibilityMap,
227}
228
229impl SegmentManager {
230    /// Create a new segment manager
231    pub fn new(collection: impl Into<String>) -> Self {
232        Self::with_config(collection, ManagerConfig::default())
233    }
234
235    /// Create with custom configuration
236    pub fn with_config(collection: impl Into<String>, config: ManagerConfig) -> Self {
237        Self {
238            row_constraint_lock: Arc::new(parking_lot::ReentrantMutex::new(())),
239            collection: collection.into(),
240            config,
241            next_segment_id: AtomicU64::new(1),
242            next_entity_id: AtomicU64::new(1),
243            next_row_id: AtomicU64::new(1),
244            total_entities_atomic: AtomicU64::new(0),
245            growing: RwLock::new(None),
246            sealed: RwLock::new(Vec::new()),
247            retired: RwLock::new(Vec::new()),
248            archived: RwLock::new(Vec::new()),
249            entity_segment: RwLock::new(HashMap::new()),
250            column_schema: RwLock::new(None),
251            stats: RwLock::new(ManagerStats::default()),
252            consolidation: RwLock::new(None),
253            events: RwLock::new(Vec::new()),
254            visibility_map: VisibilityMap::new(),
255        }
256    }
257
258    pub(crate) fn row_constraint_lock(&self) -> Arc<parking_lot::ReentrantMutex<()>> {
259        Arc::clone(&self.row_constraint_lock)
260    }
261
262    /// Get or create the shared column schema from first row's named fields.
263    pub fn get_or_init_schema(
264        &self,
265        named: &HashMap<String, reddb_types::Value>,
266    ) -> Arc<Vec<String>> {
267        {
268            let schema = self.column_schema.read();
269            if let Some(ref s) = *schema {
270                return Arc::clone(s);
271            }
272        }
273        let cols: Vec<String> = named.keys().cloned().collect();
274        let arc = Arc::new(cols);
275        *self.column_schema.write() = Some(Arc::clone(&arc));
276        arc
277    }
278
279    /// Get the column schema if it exists.
280    pub fn column_schema(&self) -> Option<Arc<Vec<String>>> {
281        self.column_schema.read().clone()
282    }
283
284    pub(crate) fn set_column_schema_if_empty(&self, columns: Vec<String>) {
285        if columns.is_empty() {
286            return;
287        }
288        let mut schema = self.column_schema.write();
289        if schema.is_none() {
290            *schema = Some(Arc::new(columns));
291        }
292    }
293
294    /// Get collection name
295    pub fn collection(&self) -> &str {
296        &self.collection
297    }
298
299    /// Get configuration
300    pub fn config(&self) -> &ManagerConfig {
301        &self.config
302    }
303
304    /// Get statistics. Entity counts cover active segments; memory also covers
305    /// retained sources and unpublished consolidation work.
306    pub fn stats(&self) -> ManagerStats {
307        let mut s = self.stats.read().clone();
308        s.total_entities = self.total_entities_atomic.load(Ordering::Relaxed) as usize;
309        s.total_memory_bytes = self.resident_bytes() as usize;
310        s
311    }
312
313    /// Approximate resident segment memory, including tombstones, retired sources,
314    /// and the in-flight merge and its bookkeeping. This feeds budget admission.
315    pub fn resident_bytes(&self) -> u64 {
316        self.memory_usage().1
317    }
318
319    /// Resident segment payload/directory bytes and consolidation bookkeeping,
320    /// including retired sources. Unlike `resident_bytes`, excludes tombstones.
321    pub fn memory_bytes(&self) -> u64 {
322        self.memory_usage().0
323    }
324
325    /// Sample payload and resident bytes under the publication lock order:
326    /// consolidation, growing, sealed, retired. No entity scan or strong handle
327    /// is retained beyond this call; expired weak entries are pruned in place.
328    fn memory_usage(&self) -> (u64, u64) {
329        let consolidation = self.consolidation.read();
330        let growing = self.growing.read();
331        let sealed = self.sealed.read();
332        let mut retired = self.retired.write();
333        let mut payload = 0u64;
334        let mut resident = 0u64;
335        {
336            let mut add = |segment: &GrowingSegment| {
337                payload = payload.saturating_add(segment.memory_bytes());
338                resident = resident.saturating_add(segment.resident_bytes());
339            };
340            for segment in growing.iter().chain(sealed.iter()) {
341                add(&segment.read());
342            }
343            retired.retain(|source| {
344                if let Some(source) = source.upgrade() {
345                    add(&source.read());
346                    true
347                } else {
348                    false
349                }
350            });
351            if let Some(run) = consolidation.as_ref() {
352                add(&run.merged);
353            }
354        }
355        let mut bookkeeping =
356            (retired.capacity() * std::mem::size_of::<Weak<RwLock<GrowingSegment>>>()) as u64;
357        if let Some(run) = consolidation.as_ref() {
358            bookkeeping = bookkeeping
359                .saturating_add(
360                    (run.sources.capacity() * std::mem::size_of::<SourceProgress>()) as u64,
361                )
362                .saturating_add(
363                    (run.pending_ids.capacity() * std::mem::size_of::<EntityId>()) as u64,
364                );
365            for source in &run.sources {
366                bookkeeping = bookkeeping.saturating_add(
367                    (source.copied.capacity() * std::mem::size_of::<EntityId>()) as u64,
368                );
369            }
370        }
371        (
372            payload.saturating_add(bookkeeping),
373            resident.saturating_add(bookkeeping),
374        )
375    }
376
377    /// Bytes a pressure-triggered consolidation could plausibly return.
378    pub fn reclaimable_bytes(&self) -> u64 {
379        self.sealed
380            .read()
381            .iter()
382            .map(|segment| segment.read().reclaimable_bytes())
383            .fold(0, u64::saturating_add)
384    }
385
386    /// Generate a new entity ID
387    pub fn next_entity_id(&self) -> EntityId {
388        EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
389    }
390
391    /// Generate a per-table sequential row ID (1, 2, 3... per collection)
392    pub fn next_row_id(&self) -> u64 {
393        self.next_row_id.fetch_add(1, Ordering::SeqCst)
394    }
395
396    /// Reserve `n` contiguous per-table row IDs with one atomic
397    /// fetch_add. Caller assigns `row_id = start + i` per entity.
398    /// Saves N-1 atomic RMWs on bulk inserts (25k atomics → 1).
399    pub fn reserve_row_ids(&self, n: u64) -> std::ops::Range<u64> {
400        let start = self.next_row_id.fetch_add(n, Ordering::SeqCst);
401        start..start + n
402    }
403
404    /// Advance the per-table row_id counter to at least `id + 1`.
405    /// Called during load to restore the counter from existing data.
406    pub fn register_row_id(&self, id: u64) {
407        let candidate = id.saturating_add(1);
408        let mut current = self.next_row_id.load(Ordering::SeqCst);
409        while candidate > current {
410            match self.next_row_id.compare_exchange(
411                current,
412                candidate,
413                Ordering::SeqCst,
414                Ordering::SeqCst,
415            ) {
416                Ok(_) => break,
417                Err(updated) => current = updated,
418            }
419        }
420    }
421
422    /// Get or create the active growing segment.
423    ///
424    /// Fast path: read lock only — no write contention when the segment already exists.
425    /// Concurrent writers each clone the `Arc` under a shared read lock, then compete
426    /// on the segment's own write lock. This eliminates the global write-lock serialisation
427    /// that previously throttled concurrent inserts to ~238 ops/s.
428    fn get_or_create_growing(&self) -> Arc<RwLock<GrowingSegment>> {
429        // Common case: segment already exists — shared read lock, zero contention.
430        {
431            let growing = self.growing.read();
432            if let Some(segment) = growing.as_ref() {
433                return Arc::clone(segment);
434            }
435        }
436
437        // Slow path: segment missing — take exclusive write lock to create it.
438        let mut growing = self.growing.write();
439        // Double-check: another thread may have created it between the two lock acquisitions.
440        if let Some(segment) = growing.as_ref() {
441            return Arc::clone(segment);
442        }
443
444        let id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
445        let segment = GrowingSegment::new(id, &self.collection);
446        let segment_arc = Arc::new(RwLock::new(segment));
447        *growing = Some(Arc::clone(&segment_arc));
448
449        self.emit(LifecycleEvent::SegmentCreated(id));
450
451        // Update growing_count in the slow-path stats struct.
452        // This is the rare segment-creation path — locking is fine here.
453        self.stats.write().growing_count += 1;
454
455        segment_arc
456    }
457
458    /// Insert a new entity
459    pub fn insert(&self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
460        // Check if we need to seal the current segment first
461        self.maybe_seal_growing()?;
462
463        let segment_arc = self.get_or_create_growing();
464        let mut segment = segment_arc.write();
465
466        // Assign entity ID if not already set
467        if entity.id.raw() == 0 {
468            entity.id = self.next_entity_id();
469        }
470
471        let entity_id = entity.id;
472        let segment_id = segment.id();
473
474        segment.insert(entity)?;
475
476        // Lock-free counter update — eliminates the stats write lock on the hot path.
477        self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
478
479        // entity_segment map is intentionally NOT updated here.
480        // update() and update_hot() first probe the growing segment directly
481        // (growing.contains(entity.id)) before consulting this map, so entities
482        // that were just inserted are found without entity_segment. The map is
483        // only consulted for entities that may have been moved to sealed segments,
484        // which can't be updated anyway (state().is_writable() == false).
485        // Skipping this write removes one exclusive HashMap lock per insert.
486
487        self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
488
489        Ok(entity_id)
490    }
491
492    /// Insert multiple entities (batch) — sequential, one lock per item.
493    pub fn insert_batch(
494        &self,
495        entities: Vec<UnifiedEntity>,
496    ) -> Result<Vec<EntityId>, SegmentError> {
497        let mut ids = Vec::with_capacity(entities.len());
498        for entity in entities {
499            ids.push(self.insert(entity)?);
500        }
501        Ok(ids)
502    }
503
504    /// Turbo bulk insert — single lock acquisition for the entire batch.
505    /// Skips bloom filter and cross-ref indexing for maximum speed.
506    pub fn bulk_insert(
507        &self,
508        mut entities: Vec<UnifiedEntity>,
509    ) -> Result<Vec<EntityId>, SegmentError> {
510        // Assign IDs and per-table row_ids.
511        for entity in &mut entities {
512            if entity.id.raw() == 0 {
513                entity.id = self.next_entity_id();
514            }
515            if let super::entity::EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
516                if *row_id == 0 {
517                    *row_id = self.next_row_id();
518                } else {
519                    self.register_row_id(*row_id);
520                }
521            }
522        }
523
524        // Convert named HashMap → positional Vec (compact memory representation)
525        // The schema (column order) is shared across all rows in the collection.
526        if let Some(first_row) = entities.first() {
527            if let super::entity::EntityData::Row(ref row) = first_row.data {
528                if let Some(ref named) = row.named {
529                    let schema = self.get_or_init_schema(named);
530                    for entity in &mut entities {
531                        if let super::entity::EntityData::Row(ref mut row) = entity.data {
532                            if let Some(named) = row.named.take() {
533                                let mut cols = Vec::with_capacity(schema.len());
534                                for col_name in schema.iter() {
535                                    cols.push(
536                                        named
537                                            .get(col_name)
538                                            .cloned()
539                                            .unwrap_or(reddb_types::Value::Null),
540                                    );
541                                }
542                                row.columns = cols;
543                                row.schema = Some(Arc::clone(&schema));
544                            }
545                        }
546                    }
547                }
548            }
549        }
550
551        let segment_arc = self.get_or_create_growing();
552        let mut segment = segment_arc.write();
553        let segment_id = segment.id();
554
555        // Single call to GrowingSegment.bulk_insert (one lock, no bloom/cross-refs)
556        let ids = segment.bulk_insert(entities)?;
557
558        // Skip entity-segment mapping for bulk inserts (saves ~56 bytes/entity).
559        // The get() method scans growing+sealed segments directly.
560
561        // Lock-free batch counter update.
562        self.total_entities_atomic
563            .fetch_add(ids.len() as u64, Ordering::Relaxed);
564
565        Ok(ids)
566    }
567
568    /// Get an entity by ID — scans growing then sealed segments.
569    pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
570        self.get_with(id, Clone::clone)
571    }
572
573    /// Inspect under the segment lock, for example to bound a payload clone.
574    /// The callback must not re-enter storage or runtime memory admission.
575    pub(crate) fn get_with<T>(
576        &self,
577        id: EntityId,
578        visit: impl FnOnce(&UnifiedEntity) -> T,
579    ) -> Option<T> {
580        // Growing segment first (most likely for recent inserts)
581        if let Some(growing_arc) = self.growing.read().as_ref() {
582            let growing = growing_arc.read();
583            if let Some(entity) = growing.get(id) {
584                return Some(visit(entity));
585            }
586        }
587
588        // Then sealed segments
589        let sealed = self.sealed.read();
590        for segment in sealed.iter() {
591            let seg = segment.read();
592            if let Some(entity) = seg.get(id) {
593                return Some(visit(entity));
594            }
595        }
596
597        None
598    }
599
600    /// Resolve a physical graph node reference without cloning historical payloads.
601    /// This deliberately ignores MVCC: callers must authorize the visible logical
602    /// node separately. The callback bounds every segment probe, including misses.
603    pub(crate) fn graph_node_logical_id(
604        &self,
605        id: EntityId,
606        mut before_segment: impl FnMut() -> bool,
607    ) -> Option<EntityId> {
608        if let Some(growing_arc) = self.growing.read().as_ref() {
609            if !before_segment() {
610                return None;
611            }
612            let growing = growing_arc.read();
613            if let Some(entity) = growing.get(id) {
614                return matches!(entity.kind, EntityKind::GraphNode(_))
615                    .then(|| entity.logical_id());
616            }
617        }
618        let sealed = self.sealed.read();
619        for segment in sealed.iter() {
620            if !before_segment() {
621                return None;
622            }
623            let segment = segment.read();
624            if let Some(entity) = segment.get(id) {
625                return matches!(entity.kind, EntityKind::GraphNode(_))
626                    .then(|| entity.logical_id());
627            }
628        }
629        None
630    }
631
632    /// Pin the source inventory across sealing/consolidation. Consumers hydrate
633    /// IDs through the current manager and run outside all storage locks.
634    /// Admission is also outside storage locks; a growing inventory retries only
635    /// its small handle array. Each indexed candidate buffer has a fixed size.
636    pub(crate) fn visit_graph_index_batches<E>(
637        &self,
638        kind: GraphEntityKind,
639        keys: &[EntityId],
640        mut before_work: impl FnMut() -> bool,
641        mut admit: impl FnMut(usize) -> Result<(), E>,
642        mut consume: impl FnMut(&[EntityId]) -> Result<bool, E>,
643    ) -> Result<bool, E> {
644        use super::segment::{GraphIndexCursor, SCAN_BATCH_SIZE};
645        admit(std::mem::size_of::<[EntityId; SCAN_BATCH_SIZE]>())?;
646        let mut admitted = 0;
647        let sources = loop {
648            let count = {
649                let growing = self.growing.read();
650                let sealed = self.sealed.read();
651                sealed.len() + usize::from(growing.is_some())
652            };
653            let bytes = count.saturating_mul(std::mem::size_of::<(
654                Arc<RwLock<GrowingSegment>>,
655                GraphIndexCursor,
656            )>());
657            admit(bytes.saturating_sub(admitted))?;
658            admitted = admitted.max(bytes);
659            let mut sources = Vec::with_capacity(count);
660            let growing = self.growing.read();
661            let sealed = self.sealed.read();
662            if sealed.len() + usize::from(growing.is_some()) > sources.capacity() {
663                continue;
664            }
665            for segment in growing.iter().chain(sealed.iter()) {
666                if !before_work() {
667                    return Ok(false);
668                }
669                let view = segment.read();
670                if !view.may_contain_graph_kind(kind) {
671                    continue;
672                }
673                let Some(cursor) = view.graph_index_cursor(kind, keys, &mut before_work) else {
674                    return Ok(false);
675                };
676                sources.push((Arc::clone(segment), cursor));
677            }
678            break sources;
679        };
680        let mut ids = [EntityId::new(0); SCAN_BATCH_SIZE];
681        for (source, mut cursor) in sources {
682            while !cursor.finished(keys) {
683                let mut count = 0;
684                {
685                    let segment = source.read();
686                    if !segment.may_contain_graph_kind(kind) {
687                        break;
688                    }
689                    if !segment.graph_index_batch(
690                        kind,
691                        keys,
692                        &mut cursor,
693                        &mut before_work,
694                        &mut |id| {
695                            ids[count] = id;
696                            count += 1;
697                        },
698                    ) {
699                        return Ok(false);
700                    }
701                }
702                if !before_work() || !consume(&ids[..count])? {
703                    return Ok(false);
704                }
705            }
706        }
707        Ok(true)
708    }
709
710    /// Batch-fetch multiple entities by ID in a single lock acquisition per segment.
711    ///
712    /// For indexed-scan result sets (up to ~5000 ids from range/bitmap lookup) this
713    /// is 2-3 lock acquisitions total vs N×3 with individual `get()` calls.
714    pub fn get_many(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
715        let mut out: Vec<Option<UnifiedEntity>> = vec![None; ids.len()];
716        let mut remaining: Vec<usize> = (0..ids.len()).collect(); // indices still unfound
717
718        // Growing segment — one read lock for the entire batch.
719        // Non-blocking first: if a writer is active, fall back to blocking.
720        if let Some(growing_arc) = self.growing.read().as_ref() {
721            let growing = if let Some(g) = growing_arc.try_read() {
722                g
723            } else {
724                growing_arc.read()
725            };
726            remaining.retain(|&i| {
727                if let Some(entity) = growing.get(ids[i]) {
728                    out[i] = Some(entity.clone());
729                    false // remove from remaining
730                } else {
731                    true // keep — not found yet
732                }
733            });
734        }
735
736        if remaining.is_empty() {
737            return out;
738        }
739
740        // Sealed segments — one read lock per segment
741        let sealed = self.sealed.read();
742        for segment in sealed.iter() {
743            if remaining.is_empty() {
744                break;
745            }
746            let seg = segment.read();
747            remaining.retain(|&i| {
748                if let Some(entity) = seg.get(ids[i]) {
749                    out[i] = Some(entity.clone());
750                    false
751                } else {
752                    true
753                }
754            });
755        }
756
757        out
758    }
759
760    /// Visitor-pattern batch fetch. Invokes `f(&UnifiedEntity, usize_index)`
761    /// for each id that resolves, never cloning the entity.
762    ///
763    /// Used by scan hot paths (select_range, select_filtered) that
764    /// materialize each entity into an output record and don't need
765    /// an owned `UnifiedEntity`. Eliminates ~20% of scan CPU spent in
766    /// `UnifiedEntity::clone` when `get_batch` is followed by
767    /// `runtime_table_record_lean(entity)`.
768    ///
769    /// The closure runs while the segment read lock is held, so it
770    /// must be short — avoid doing I/O or taking unrelated locks in
771    /// `f`.
772    pub fn for_each_id<F>(&self, ids: &[EntityId], mut f: F)
773    where
774        F: FnMut(usize, &UnifiedEntity),
775    {
776        // Thread-local scratch buffer for the "pending" index list.
777        // Previous code allocated a fresh `Vec<usize>` of capacity
778        // N on every call — 4200 × 1000 queries / scenario on the
779        // select_range bench path. Take-and-restore pattern (vs
780        // RefCell::borrow_mut) so user closures that recurse into
781        // another `for_each_id` don't panic on a re-borrow; worst
782        // case they allocate a fresh buffer and we lose the caching
783        // win for that nested call.
784        thread_local! {
785            static REMAINING_SCRATCH: std::cell::Cell<Vec<usize>> =
786                const { std::cell::Cell::new(Vec::new()) };
787        }
788
789        let mut remaining: Vec<usize> = REMAINING_SCRATCH.with(|cell| cell.take());
790        remaining.clear();
791
792        if let Some(growing_arc) = self.growing.read().as_ref() {
793            let growing = if let Some(g) = growing_arc.try_read() {
794                g
795            } else {
796                growing_arc.read()
797            };
798            for (i, id) in ids.iter().enumerate() {
799                if let Some(entity) = growing.get(*id) {
800                    f(i, entity);
801                } else {
802                    remaining.push(i);
803                }
804            }
805        } else {
806            remaining.reserve(ids.len());
807            remaining.extend(0..ids.len());
808        }
809
810        if !remaining.is_empty() {
811            let sealed = self.sealed.read();
812            for segment in sealed.iter() {
813                if remaining.is_empty() {
814                    break;
815                }
816                let seg = segment.read();
817                remaining.retain(|&i| {
818                    if let Some(entity) = seg.get(ids[i]) {
819                        f(i, entity);
820                        false
821                    } else {
822                        true
823                    }
824                });
825            }
826        }
827
828        REMAINING_SCRATCH.with(|cell| cell.set(remaining));
829    }
830
831    /// Scan all segments for an entity
832    fn scan_for_entity(&self, id: EntityId) -> Option<UnifiedEntity> {
833        // Check growing
834        if let Some(growing_arc) = self.growing.read().as_ref() {
835            let growing = growing_arc.read();
836            if let Some(entity) = growing.get(id) {
837                return Some(entity.clone());
838            }
839        }
840
841        // Check sealed
842        let sealed = self.sealed.read();
843        for segment in sealed.iter() {
844            if let Some(entity) = segment.get(id) {
845                return Some(entity.clone());
846            }
847        }
848
849        None
850    }
851
852    fn find_sealed_segment_arc(&self, id: EntityId) -> Option<Arc<RwLock<GrowingSegment>>> {
853        let sealed = self.sealed.read();
854        sealed
855            .iter()
856            .find(|segment_arc| segment_arc.read().contains(id))
857            .map(Arc::clone)
858    }
859
860    fn rewrite_sealed_entity_into_growing(
861        &self,
862        entity: UnifiedEntity,
863        metadata: Option<&Metadata>,
864    ) -> Result<(), SegmentError> {
865        let entity_id = entity.id;
866        let sealed_arc = self
867            .find_sealed_segment_arc(entity_id)
868            .ok_or(SegmentError::NotFound(entity_id))?;
869
870        let metadata_to_apply = {
871            let mut sealed = sealed_arc.write();
872            let existing_metadata = sealed.get_metadata(entity_id);
873            if !sealed.force_delete(entity_id) {
874                return Err(SegmentError::NotFound(entity_id));
875            }
876            metadata.cloned().or(existing_metadata)
877        };
878
879        let growing_arc = self.get_or_create_growing();
880        let growing_id = {
881            let mut growing = growing_arc.write();
882            growing.insert(entity)?;
883            if let Some(metadata) = metadata_to_apply {
884                growing.set_metadata(entity_id, metadata)?;
885            }
886            growing.id()
887        };
888
889        self.entity_segment.write().insert(entity_id, growing_id);
890        Ok(())
891    }
892
893    /// Update an entity
894    pub fn update(&self, entity: UnifiedEntity) -> Result<(), SegmentError> {
895        let entity_id = entity.id;
896        let mut entity = Some(entity);
897
898        // Try growing segment directly (covers bulk-inserted entities without entity_segment map)
899        if let Some(growing_arc) = self.growing.read().as_ref() {
900            let mut growing = growing_arc.write();
901            if growing.contains(entity_id) && growing.state().is_writable() {
902                return growing.update(entity.take().expect("entity already moved"));
903            }
904        }
905
906        // Try entity_segment mapping for individually inserted entities
907        let segment_id = self.entity_segment.read().get(&entity_id).copied();
908        if let Some(seg_id) = segment_id {
909            if let Some(growing_arc) = self.growing.read().as_ref() {
910                let mut growing = growing_arc.write();
911                if growing.id() == seg_id && growing.state().is_writable() {
912                    return growing.update(entity.take().expect("entity already moved"));
913                }
914            }
915        }
916
917        if let Some(entity) = entity.take() {
918            return self.rewrite_sealed_entity_into_growing(entity, None);
919        }
920
921        Err(SegmentError::NotFound(entity_id))
922    }
923
924    /// Update an entity and, optionally, replace its metadata while holding
925    /// the segment write lock only once.
926    pub fn update_with_metadata(
927        &self,
928        entity: UnifiedEntity,
929        metadata: Option<&Metadata>,
930    ) -> Result<(), SegmentError> {
931        let entity_id = entity.id;
932        let mut entity = Some(entity);
933
934        // Try growing segment directly (covers bulk-inserted entities without entity_segment map)
935        if let Some(growing_arc) = self.growing.read().as_ref() {
936            let mut growing = growing_arc.write();
937            if growing.contains(entity_id) && growing.state().is_writable() {
938                growing.update(entity.take().expect("entity already moved"))?;
939                if let Some(metadata) = metadata {
940                    growing.set_metadata(entity_id, metadata.clone())?;
941                }
942                return Ok(());
943            }
944        }
945
946        // Try entity_segment mapping for individually inserted entities
947        let segment_id = self.entity_segment.read().get(&entity_id).copied();
948        if let Some(seg_id) = segment_id {
949            if let Some(growing_arc) = self.growing.read().as_ref() {
950                let mut growing = growing_arc.write();
951                if growing.id() == seg_id && growing.state().is_writable() {
952                    growing.update(entity.take().expect("entity already moved"))?;
953                    if let Some(metadata) = metadata {
954                        growing.set_metadata(entity_id, metadata.clone())?;
955                    }
956                    return Ok(());
957                }
958            }
959        }
960
961        if let Some(entity) = entity.take() {
962            return self.rewrite_sealed_entity_into_growing(entity, metadata);
963        }
964
965        Err(SegmentError::NotFound(entity_id))
966    }
967
968    /// HOT-update: like update but skips index work for unchanged columns.
969    /// `modified_columns` is the list of column names actually changed by the
970    /// UPDATE statement — lets us skip pk_index and cross_ref when safe.
971    pub fn update_hot(
972        &self,
973        entity: UnifiedEntity,
974        modified_columns: &[String],
975    ) -> Result<(), SegmentError> {
976        let entity_id = entity.id;
977        let mut entity = Some(entity);
978
979        if let Some(growing_arc) = self.growing.read().as_ref() {
980            let mut growing = growing_arc.write();
981            if growing.contains(entity_id) && growing.state().is_writable() {
982                return growing.update_hot(
983                    entity.take().expect("entity already moved"),
984                    modified_columns,
985                );
986            }
987        }
988
989        let segment_id = self.entity_segment.read().get(&entity_id).copied();
990        if let Some(seg_id) = segment_id {
991            if let Some(growing_arc) = self.growing.read().as_ref() {
992                let mut growing = growing_arc.write();
993                if growing.id() == seg_id && growing.state().is_writable() {
994                    return growing.update_hot(
995                        entity.take().expect("entity already moved"),
996                        modified_columns,
997                    );
998                }
999            }
1000        }
1001
1002        if let Some(entity) = entity.take() {
1003            return self.rewrite_sealed_entity_into_growing(entity, None);
1004        }
1005
1006        Err(SegmentError::NotFound(entity_id))
1007    }
1008
1009    /// HOT-update an entity and, optionally, replace its metadata while
1010    /// holding the segment write lock only once.
1011    pub fn update_hot_with_metadata(
1012        &self,
1013        entity: UnifiedEntity,
1014        modified_columns: &[String],
1015        metadata: Option<&Metadata>,
1016    ) -> Result<(), SegmentError> {
1017        let entity_id = entity.id;
1018        let mut entity = Some(entity);
1019
1020        if let Some(growing_arc) = self.growing.read().as_ref() {
1021            let mut growing = growing_arc.write();
1022            if growing.contains(entity_id) && growing.state().is_writable() {
1023                growing.update_hot(
1024                    entity.take().expect("entity already moved"),
1025                    modified_columns,
1026                )?;
1027                if let Some(metadata) = metadata {
1028                    growing.set_metadata(entity_id, metadata.clone())?;
1029                }
1030                return Ok(());
1031            }
1032        }
1033
1034        let segment_id = self.entity_segment.read().get(&entity_id).copied();
1035        if let Some(seg_id) = segment_id {
1036            if let Some(growing_arc) = self.growing.read().as_ref() {
1037                let mut growing = growing_arc.write();
1038                if growing.id() == seg_id && growing.state().is_writable() {
1039                    growing.update_hot(
1040                        entity.take().expect("entity already moved"),
1041                        modified_columns,
1042                    )?;
1043                    if let Some(metadata) = metadata {
1044                        growing.set_metadata(entity_id, metadata.clone())?;
1045                    }
1046                    return Ok(());
1047                }
1048            }
1049        }
1050
1051        if let Some(entity) = entity.take() {
1052            return self.rewrite_sealed_entity_into_growing(entity, metadata);
1053        }
1054
1055        Err(SegmentError::NotFound(entity_id))
1056    }
1057
1058    /// Batch HOT-update multiple entities while holding the growing-segment
1059    /// write lock only once when possible.
1060    pub fn update_hot_batch_with_metadata<'a, I>(&self, items: I) -> Result<(), SegmentError>
1061    where
1062        I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
1063    {
1064        let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
1065            items.into_iter().collect();
1066        if items.is_empty() {
1067            return Ok(());
1068        }
1069
1070        if let Some(growing_arc) = self.growing.read().as_ref() {
1071            let mut growing = growing_arc.write();
1072            if growing.state().is_writable() {
1073                match growing.update_hot_batch_with_metadata(items.iter().copied()) {
1074                    Ok(()) => return Ok(()),
1075                    Err(SegmentError::NotFound(_)) => {}
1076                    Err(other) => return Err(other),
1077                }
1078            }
1079        }
1080
1081        for (entity, modified_columns, metadata) in items {
1082            self.update_hot_with_metadata(entity.clone(), modified_columns, metadata)?;
1083        }
1084        Ok(())
1085    }
1086
1087    /// Delete an entity
1088    pub fn delete(&self, id: EntityId) -> Result<bool, SegmentError> {
1089        // Fast path: probe the growing segment directly — covers entities inserted via
1090        // insert() which no longer writes to entity_segment, and bulk-inserted entities.
1091        if let Some(growing_arc) = self.growing.read().as_ref() {
1092            let mut growing = growing_arc.write();
1093            if growing.contains(id) && growing.state().is_writable() {
1094                let seg_id = growing.id();
1095                let deleted = growing.delete(id)?;
1096                if deleted {
1097                    self.entity_segment.write().remove(&id);
1098                    self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1099                    self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1100                }
1101                return Ok(deleted);
1102            }
1103        }
1104
1105        // Fallback: check entity_segment map (populated for older insert() paths
1106        // or entities that were in a previous growing segment).
1107        let segment_id = self.entity_segment.read().get(&id).copied();
1108
1109        if let Some(seg_id) = segment_id {
1110            if let Some(growing_arc) = self.growing.read().as_ref() {
1111                let mut growing = growing_arc.write();
1112                if growing.id() == seg_id && growing.state().is_writable() {
1113                    let deleted = growing.delete(id)?;
1114                    if deleted {
1115                        self.entity_segment.write().remove(&id);
1116                        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1117                        self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1118                    }
1119                    return Ok(deleted);
1120                }
1121            }
1122        }
1123
1124        // Fallback: entity is in a sealed segment (bulk-inserted, not in entity_segment map).
1125        // Single write-lock per segment to avoid TOCTOU race between contains() and force_delete().
1126        {
1127            let sealed = self.sealed.read();
1128            for segment_arc in sealed.iter() {
1129                let mut seg = segment_arc.write();
1130                let seg_id = seg.id();
1131                if seg.contains(id) {
1132                    let deleted = seg.force_delete(id);
1133                    drop(seg);
1134                    if deleted {
1135                        self.entity_segment.write().remove(&id);
1136                        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
1137                        self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
1138                    }
1139                    return Ok(deleted);
1140                }
1141            }
1142        }
1143
1144        Ok(false)
1145    }
1146
1147    pub fn delete_batch(&self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
1148        if ids.is_empty() {
1149            return Ok(Vec::new());
1150        }
1151
1152        let mut deleted_ids = Vec::with_capacity(ids.len());
1153
1154        if let Some(growing_arc) = self.growing.read().as_ref() {
1155            let mut growing = growing_arc.write();
1156            if growing.state().is_writable() {
1157                let seg_id = growing.id();
1158                let deleted = growing.delete_batch(ids)?;
1159                if !deleted.is_empty() {
1160                    {
1161                        let mut entity_segment = self.entity_segment.write();
1162                        for id in &deleted {
1163                            entity_segment.remove(id);
1164                        }
1165                    }
1166                    self.total_entities_atomic
1167                        .fetch_sub(deleted.len() as u64, Ordering::Relaxed);
1168                    for id in &deleted {
1169                        self.emit(LifecycleEvent::EntityDeleted(*id, seg_id));
1170                    }
1171                    deleted_ids.extend(deleted);
1172                }
1173            }
1174        }
1175
1176        if deleted_ids.len() == ids.len() {
1177            return Ok(deleted_ids);
1178        }
1179
1180        let deleted_set: std::collections::HashSet<EntityId> =
1181            deleted_ids.iter().copied().collect();
1182        for &id in ids {
1183            if deleted_set.contains(&id) {
1184                continue;
1185            }
1186            if self.delete(id)? {
1187                deleted_ids.push(id);
1188            }
1189        }
1190
1191        Ok(deleted_ids)
1192    }
1193
1194    /// Get metadata for an entity
1195    pub fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1196        // Fast path: probe growing segment directly (no entity_segment needed).
1197        if let Some(growing_arc) = self.growing.read().as_ref() {
1198            let growing = growing_arc.read();
1199            if growing.contains(id) {
1200                return growing.get_metadata(id);
1201            }
1202        }
1203
1204        // Fallback: entity_segment map (for pre-existing or sealed entities)
1205        let segment_id = self.entity_segment.read().get(&id).copied();
1206
1207        if let Some(seg_id) = segment_id {
1208            if let Some(growing_arc) = self.growing.read().as_ref() {
1209                let growing = growing_arc.read();
1210                if growing.id() == seg_id {
1211                    return growing.get_metadata(id);
1212                }
1213            }
1214
1215            let sealed = self.sealed.read();
1216            for segment in sealed.iter() {
1217                if segment.id() == seg_id {
1218                    return segment.get_metadata(id);
1219                }
1220            }
1221        }
1222
1223        if let Some(segment_arc) = self.find_sealed_segment_arc(id) {
1224            return segment_arc.read().get_metadata(id);
1225        }
1226
1227        None
1228    }
1229
1230    /// Set metadata for an entity
1231    pub fn set_metadata(&self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1232        // Fast path: probe growing segment directly — covers entities inserted via
1233        // insert() which no longer writes to entity_segment.
1234        if let Some(growing_arc) = self.growing.read().as_ref() {
1235            let mut growing = growing_arc.write();
1236            if growing.contains(id) && growing.state().is_writable() {
1237                return growing.set_metadata(id, metadata);
1238            }
1239        }
1240
1241        // Fallback: entity_segment map (sealed or pre-atomic-path entities)
1242        let segment_id = self.entity_segment.read().get(&id).copied();
1243
1244        if let Some(seg_id) = segment_id {
1245            if let Some(growing_arc) = self.growing.read().as_ref() {
1246                let mut growing = growing_arc.write();
1247                if growing.id() == seg_id && growing.state().is_writable() {
1248                    return growing.set_metadata(id, metadata);
1249                }
1250            }
1251        }
1252
1253        if let Some(entity) = self.get(id) {
1254            return self.rewrite_sealed_entity_into_growing(entity, Some(&metadata));
1255        }
1256
1257        Err(SegmentError::NotFound(id))
1258    }
1259
1260    /// Check if growing segment should be sealed
1261    fn maybe_seal_growing(&self) -> Result<(), SegmentError> {
1262        let should_seal = {
1263            let growing_opt = self.growing.read();
1264            if let Some(growing_arc) = growing_opt.as_ref() {
1265                let growing = growing_arc.read();
1266                growing.should_seal(&self.config.segment_config)
1267                    || growing.idle_secs() >= self.config.idle_seal_secs
1268            } else {
1269                false
1270            }
1271        };
1272
1273        if should_seal {
1274            self.seal_current()?;
1275        }
1276
1277        Ok(())
1278    }
1279
1280    /// Seal the current growing segment
1281    pub fn seal_current(&self) -> Result<SegmentId, SegmentError> {
1282        // Keep topology publication atomic for cursors that capture growing
1283        // and sealed under the same lock order: growing, then sealed.
1284        let mut growing_slot = self.growing.write();
1285        if let Some(growing_arc) = growing_slot.take() {
1286            let mut growing = growing_arc.write();
1287            let seg_id = growing.id();
1288            let entity_count = growing.stats().entity_count as u64;
1289
1290            // Seal it
1291            growing.seal()?;
1292
1293            // Move to sealed list (we need to extract it from the Arc)
1294            drop(growing); // Release write lock
1295
1296            // In a real implementation, we'd convert to SealedSegment here
1297            // For now, we keep it as-is since GrowingSegment implements UnifiedSegment
1298            self.sealed.write().push(growing_arc);
1299            drop(growing_slot);
1300
1301            // Mark sealed segment pages all-visible — they're now immutable
1302            self.mark_sealed_pages_visible(entity_count);
1303
1304            // Update stats
1305            {
1306                let mut stats = self.stats.write();
1307                stats.growing_count = stats.growing_count.saturating_sub(1);
1308                stats.sealed_count += 1;
1309                stats.seal_ops += 1;
1310            }
1311
1312            self.emit(LifecycleEvent::SegmentSealed(seg_id));
1313
1314            return Ok(seg_id);
1315        }
1316
1317        Err(SegmentError::InvalidState(SegmentState::Sealed))
1318    }
1319
1320    /// Force seal (for testing/manual control)
1321    pub fn force_seal(&self) -> Result<Option<SegmentId>, SegmentError> {
1322        let has_growing = self.growing.read().is_some();
1323        if has_growing {
1324            Ok(Some(self.seal_current()?))
1325        } else {
1326            Ok(None)
1327        }
1328    }
1329
1330    /// Fraction of "pages" in sealed segments that are marked all-visible.
1331    ///
1332    /// Sealed segments are immutable so all their rows are safe for
1333    /// index-only scans. The growing segment is never counted (writes
1334    /// may be in-flight). Uses `rows_per_page = 256` (matching 8 KB pages
1335    /// with ~32-byte rows).
1336    ///
1337    /// Returns a value in `[0.0, 1.0]`. 1.0 when all sealed rows are
1338    /// visible; 0.0 when there are no sealed segments.
1339    pub fn all_visible_fraction(&self) -> f64 {
1340        const ROWS_PER_PAGE: u32 = 256;
1341        let sealed = self.sealed.read();
1342        if sealed.is_empty() {
1343            return 0.0;
1344        }
1345        let mut total_pages: u64 = 0;
1346        for seg_arc in sealed.iter() {
1347            let seg = seg_arc.read();
1348            let entity_count = seg.stats().entity_count as u64;
1349            let pages = entity_count.div_ceil(ROWS_PER_PAGE as u64);
1350            total_pages += pages;
1351        }
1352        if total_pages == 0 {
1353            return 0.0;
1354        }
1355        let visible = self.visibility_map.all_visible_count();
1356        (visible as f64 / total_pages as f64).min(1.0)
1357    }
1358
1359    /// Mark all pages of newly sealed segments as all-visible in the
1360    /// visibility map. Called internally after `seal_current`.
1361    fn mark_sealed_pages_visible(&self, seg_entity_count: u64) {
1362        const ROWS_PER_PAGE: u32 = 256;
1363        let existing_visible = self.visibility_map.all_visible_count();
1364        // Append pages starting after the last known visible page
1365        let start_page = existing_visible as u32;
1366        let new_pages = seg_entity_count.div_ceil(ROWS_PER_PAGE as u64);
1367        let end_page = start_page + new_pages as u32;
1368        self.visibility_map.mark_range_visible(start_page, end_page);
1369    }
1370
1371    /// Probe a unique-key lookup in each segment under the same entity
1372    /// locks used by scans and writes. Lookup state is rebuilt lazily after
1373    /// recovery, consolidation, or a key-changing mutation.
1374    pub(crate) fn find_unique_key_conflict(
1375        &self,
1376        columns: &[Vec<String>],
1377        key_index: usize,
1378        signatures: &[String],
1379        exclude_id: Option<EntityId>,
1380        reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1381    ) -> Option<EntityId> {
1382        if let Some(growing) = self.growing.read().as_ref() {
1383            if let Some(id) = growing.read().find_unique_key_conflict(
1384                columns,
1385                key_index,
1386                signatures,
1387                exclude_id,
1388                reserves_key,
1389            ) {
1390                return Some(id);
1391            }
1392        }
1393        for segment in self.sealed.read().iter() {
1394            if let Some(id) = segment.read().find_unique_key_conflict(
1395                columns,
1396                key_index,
1397                signatures,
1398                exclude_id,
1399                reserves_key,
1400            ) {
1401                return Some(id);
1402            }
1403        }
1404        None
1405    }
1406
1407    /// Iterate over all entities in-place without collecting into a Vec.
1408    ///
1409    /// The callback receives a reference to each entity. Return `true` to
1410    /// continue iteration, `false` to stop early (e.g. when a LIMIT is reached).
1411    /// This avoids the allocation and cloning overhead of `query_all`.
1412    pub fn for_each_entity<F>(&self, callback: F)
1413    where
1414        F: FnMut(&UnifiedEntity) -> bool,
1415    {
1416        self.for_each_entity_in_segments(|_| true, || true, callback);
1417    }
1418
1419    fn for_each_entity_in_segments<F>(
1420        &self,
1421        segment_filter: impl Fn(&GrowingSegment) -> bool,
1422        mut before_segment: impl FnMut() -> bool,
1423        mut callback: F,
1424    ) where
1425        F: FnMut(&UnifiedEntity) -> bool,
1426    {
1427        // Growing segment — direct iteration (no Box<dyn>)
1428        // Try non-blocking read first; fall back to blocking only when a writer
1429        // is actively holding the write lock (rare in read-heavy workloads).
1430        if let Some(growing_arc) = self.growing.read().as_ref() {
1431            if !before_segment() {
1432                return;
1433            }
1434            let growing = if let Some(g) = growing_arc.try_read() {
1435                g
1436            } else {
1437                growing_arc.read()
1438            };
1439            if segment_filter(&growing) && !growing.for_each_fast(&mut callback) {
1440                return;
1441            }
1442        }
1443
1444        // Sealed segments
1445        let sealed = self.sealed.read();
1446        for segment_arc in sealed.iter() {
1447            if !before_segment() {
1448                return;
1449            }
1450            let segment = segment_arc.read();
1451            if segment_filter(&segment) && !segment.for_each_fast(&mut callback) {
1452                return;
1453            }
1454        }
1455    }
1456
1457    /// Parallel fold across all entities. Each sealed segment is
1458    /// processed on its own rayon task; the growing segment stays on
1459    /// the caller thread (its read lock is briefly held).
1460    ///
1461    /// - `init` builds a fresh accumulator per thread.
1462    /// - `fold` mutates an accumulator with one entity at a time.
1463    /// - `reduce` combines two accumulators into one.
1464    ///
1465    /// The returned value is the reduction of every per-thread
1466    /// accumulator. Use this for aggregate-shape workloads (GROUP BY)
1467    /// where per-thread partial state can be merged cheaply.
1468    ///
1469    /// NOTE: when there are 0 or 1 sealed segments, the parallel path
1470    /// is skipped and the work runs sequentially to avoid rayon
1471    /// overhead on tiny tables.
1472    pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1473        &self,
1474        init: FInit,
1475        fold: FFold,
1476        reduce: FReduce,
1477    ) -> T
1478    where
1479        T: Send,
1480        FInit: Fn() -> T + Send + Sync,
1481        FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1482        FReduce: Fn(T, T) -> T + Send + Sync,
1483    {
1484        use rayon::prelude::*;
1485
1486        // Growing segment — always sequential (single writer lock,
1487        // usually small working set).
1488        let mut acc = init();
1489        if let Some(growing_arc) = self.growing.read().as_ref() {
1490            let growing = if let Some(g) = growing_arc.try_read() {
1491                g
1492            } else {
1493                growing_arc.read()
1494            };
1495            growing.for_each_fast(|entity| {
1496                acc = fold(std::mem::replace(&mut acc, init()), entity);
1497                true
1498            });
1499        }
1500
1501        // Sealed segments — snapshot the Arc list under the read lock,
1502        // then drop the lock so rayon workers can fan out without
1503        // blocking writers.
1504        let segments: Vec<_> = {
1505            let sealed = self.sealed.read();
1506            sealed.iter().cloned().collect()
1507        };
1508
1509        if segments.len() <= 1 {
1510            for seg_arc in &segments {
1511                let seg = seg_arc.read();
1512                seg.for_each_fast(|entity| {
1513                    acc = fold(std::mem::replace(&mut acc, init()), entity);
1514                    true
1515                });
1516            }
1517            return acc;
1518        }
1519
1520        let sealed_acc = segments
1521            .into_par_iter()
1522            .map(|seg_arc| {
1523                let mut local = init();
1524                let seg = seg_arc.read();
1525                seg.for_each_fast(|entity| {
1526                    local = fold(std::mem::replace(&mut local, init()), entity);
1527                    true
1528                });
1529                local
1530            })
1531            .reduce(&init, &reduce);
1532
1533        reduce(acc, sealed_acc)
1534    }
1535
1536    /// Zone-map-aware iteration across all segments.
1537    ///
1538    /// Like `for_each_entity`, but checks `zone_preds` against each segment's
1539    /// column zone maps before iterating. Segments where any predicate can
1540    /// definitively prove no rows match are skipped entirely.
1541    ///
1542    /// `zone_preds`: slice of `(column_name, ZoneColPred)` extracted from the WHERE clause.
1543    /// Empty slice → same behaviour as `for_each_entity` (no pruning).
1544    pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1545    where
1546        F: FnMut(&UnifiedEntity) -> bool,
1547    {
1548        let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1549    }
1550
1551    pub fn for_each_entity_zoned_with_stats<F>(
1552        &self,
1553        zone_preds: &[(&str, ZoneColPred<'_>)],
1554        mut callback: F,
1555    ) -> SegmentScanStats
1556    where
1557        F: FnMut(&UnifiedEntity) -> bool,
1558    {
1559        let mut scan_stats = SegmentScanStats::default();
1560        // Growing segment — never skip (it's receiving writes, zones are partial).
1561        // Try a non-blocking read first: if a writer is currently inserting
1562        // (holding the write lock), try_read() returns None and we fall back to
1563        // the blocking read.  In low-contention workloads (reads far outnumber
1564        // writes) the try_read() almost always succeeds and readers never stall.
1565        if let Some(growing_arc) = self.growing.read().as_ref() {
1566            scan_stats.segments_total += 1;
1567            scan_stats.segments_scanned += 1;
1568            let growing = if let Some(g) = growing_arc.try_read() {
1569                g
1570            } else {
1571                growing_arc.read()
1572            };
1573            if !growing.for_each_fast(&mut callback) {
1574                return scan_stats;
1575            }
1576        }
1577
1578        // Sealed segments — check zone maps before iterating
1579        let sealed = self.sealed.read();
1580        for segment_arc in sealed.iter() {
1581            scan_stats.segments_total += 1;
1582            let segment = segment_arc.read();
1583            if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1584                scan_stats.segments_pruned += 1;
1585                continue; // entire segment pruned
1586            }
1587            scan_stats.segments_scanned += 1;
1588            if !segment.for_each_fast(&mut callback) {
1589                return scan_stats;
1590            }
1591        }
1592        scan_stats
1593    }
1594
1595    /// Zone-map-aware parallel query.
1596    ///
1597    /// Like `query_all` but applies `zone_preds` on the main thread to
1598    /// prune sealed segments before spawning workers — segments that
1599    /// provably contain no matching rows are skipped entirely.
1600    ///
1601    /// Zone check runs single-threaded (it reads per-segment metadata,
1602    /// not row data), so it's cheap. Surviving segments are then scanned
1603    /// in parallel using `std::thread::scope` when there are > 1 of them.
1604    pub fn query_all_zoned<F>(
1605        &self,
1606        zone_preds: &[(&str, ZoneColPred<'_>)],
1607        filter: F,
1608    ) -> Vec<UnifiedEntity>
1609    where
1610        F: Fn(&UnifiedEntity) -> bool + Sync,
1611    {
1612        self.query_all_zoned_with_stats(zone_preds, filter).0
1613    }
1614
1615    pub fn query_all_zoned_with_stats<F>(
1616        &self,
1617        zone_preds: &[(&str, ZoneColPred<'_>)],
1618        filter: F,
1619    ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1620    where
1621        F: Fn(&UnifiedEntity) -> bool + Sync,
1622    {
1623        let mut results = Vec::new();
1624        let mut scan_stats = SegmentScanStats::default();
1625
1626        // Growing segment — always scan, no zone skip (zones are partial).
1627        // Non-blocking try_read() avoids stalling behind in-progress inserts.
1628        if let Some(growing_arc) = self.growing.read().as_ref() {
1629            scan_stats.segments_total += 1;
1630            scan_stats.segments_scanned += 1;
1631            let growing = if let Some(g) = growing_arc.try_read() {
1632                g
1633            } else {
1634                growing_arc.read()
1635            };
1636            results.extend(growing.iter().filter(|e| filter(e)).cloned());
1637        }
1638
1639        // Sealed segments: zone-prune on main thread, then scan in parallel.
1640        let sealed = self.sealed.read();
1641        // Collect only the segments that survive zone-predicate pruning.
1642        let surviving: Vec<_> = sealed
1643            .iter()
1644            .filter(|seg_arc| {
1645                scan_stats.segments_total += 1;
1646                if zone_preds.is_empty() {
1647                    scan_stats.segments_scanned += 1;
1648                    return true;
1649                }
1650                let seg = seg_arc.read();
1651                if seg.can_skip_zone_preds(zone_preds) {
1652                    scan_stats.segments_pruned += 1;
1653                    false
1654                } else {
1655                    scan_stats.segments_scanned += 1;
1656                    true
1657                }
1658            })
1659            .collect();
1660
1661        let use_parallel = surviving.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1662
1663        if use_parallel {
1664            let filter_ref = &filter;
1665            let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1666                surviving
1667                    .iter()
1668                    .map(|segment| {
1669                        s.spawn(move || {
1670                            segment
1671                                .read()
1672                                .iter()
1673                                .filter(|e| filter_ref(e))
1674                                .cloned()
1675                                .collect::<Vec<_>>()
1676                        })
1677                    })
1678                    .collect::<Vec<_>>()
1679                    .into_iter()
1680                    .map(|handle| handle.join().unwrap_or_default())
1681                    .collect()
1682            });
1683            for batch in segment_results {
1684                results.extend(batch);
1685            }
1686        } else {
1687            for segment_arc in surviving {
1688                let seg = segment_arc.read();
1689                results.extend(seg.iter().filter(|e| filter(e)).cloned());
1690            }
1691        }
1692
1693        (results, scan_stats)
1694    }
1695
1696    /// Visibility predicate for the `scan_*` family. `Some` delegates to the
1697    /// canonical `entity_visible_with_context`; `None` supports low-level
1698    /// callers that intentionally scan without a statement snapshot and keeps
1699    /// the documented resolver fallback — moderation gate plus hiding
1700    /// superseded physical versions — instead of showing every version.
1701    fn scan_entity_visible(snapshot: Option<&SnapshotContext>, entity: &UnifiedEntity) -> bool {
1702        match snapshot {
1703            Some(ctx) => entity_visible_with_context(Some(ctx), entity),
1704            None => {
1705                !crate::runtime::ai::moderation::entity_moderation_hidden(entity)
1706                    && entity.xmax == 0
1707            }
1708        }
1709    }
1710
1711    /// Scan every segment under an explicit MVCC snapshot.
1712    ///
1713    /// Visibility is evaluated while each segment iterator is active, before
1714    /// the caller's filter. Unlike the raw iteration methods, this entry point
1715    /// does not consult thread-local snapshot state, so it preserves the same
1716    /// view when invoked from a different thread.
1717    pub fn scan<F>(&self, snapshot: Option<&SnapshotContext>, filter: F) -> Vec<UnifiedEntity>
1718    where
1719        F: Fn(&UnifiedEntity) -> bool + Sync,
1720    {
1721        self.query_all(|entity| Self::scan_entity_visible(snapshot, entity) && filter(entity))
1722    }
1723
1724    /// Collect visible graph items, pruning only under the segment read guard.
1725    pub(crate) fn scan_graph_kind(
1726        &self,
1727        snapshot: Option<&SnapshotContext>,
1728        kind: GraphEntityKind,
1729    ) -> Vec<UnifiedEntity> {
1730        self.query_all_in_segments(
1731            |segment| segment.may_contain_graph_kind(kind),
1732            |entity| Self::scan_entity_visible(snapshot, entity) && kind.matches(&entity.kind),
1733        )
1734    }
1735
1736    /// Visit visible candidates in segments that may contain the requested kind.
1737    /// Non-graph items in those segments still reach the callback so work budgets
1738    /// charge every inspected visible candidate. RLS must run outside this callback.
1739    /// `before_segment` runs even for pruned segments; false stops the entire scan.
1740    pub(crate) fn scan_graph_candidates_for_each<F>(
1741        &self,
1742        snapshot: Option<&SnapshotContext>,
1743        kind: GraphEntityKind,
1744        before_segment: impl FnMut() -> bool,
1745        mut callback: F,
1746    ) where
1747        F: FnMut(&UnifiedEntity) -> bool,
1748    {
1749        self.for_each_entity_in_segments(
1750            |segment| segment.may_contain_graph_kind(kind),
1751            before_segment,
1752            |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
1753        );
1754    }
1755
1756    /// Visit every entity visible under an explicit MVCC snapshot.
1757    ///
1758    /// Returning `false` from `callback` stops the scan early. Hidden entities
1759    /// never reach the callback and therefore cannot stop the scan.
1760    pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
1761    where
1762        F: FnMut(&UnifiedEntity) -> bool,
1763    {
1764        self.for_each_entity(|entity| {
1765            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
1766        });
1767    }
1768
1769    /// Capture segment membership and append boundaries, then hydrate bounded
1770    /// batches. The consumer runs outside topology and segment locks, so scoring
1771    /// and RLS may re-enter storage and writers can progress between batches.
1772    /// Retired sources pin positions only; hydration uses current segments so
1773    /// mutations after consolidation are visible to the snapshot predicate.
1774    pub(crate) fn scan_batches(
1775        &self,
1776        snapshot: Option<&SnapshotContext>,
1777        mut before_work: impl FnMut() -> bool,
1778        mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
1779    ) -> bool {
1780        let segments = {
1781            let growing = self.growing.read();
1782            let sealed = self.sealed.read();
1783            let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
1784            for segment in growing.iter().chain(sealed.iter()) {
1785                if !before_work() {
1786                    return false;
1787                }
1788                let cursor = segment.read().scan_cursor();
1789                segments.push((Arc::clone(segment), cursor));
1790            }
1791            segments
1792        };
1793        let mut ids = Vec::new();
1794        let mut batch = Vec::new();
1795        for (segment, mut cursor) in segments {
1796            while !cursor.finished() {
1797                ids.clear();
1798                batch.clear();
1799                let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
1800                ids.reserve_exact(batch_size);
1801                batch.reserve_exact(batch_size);
1802                {
1803                    let segment = segment.read();
1804                    if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
1805                        return false;
1806                    }
1807                }
1808                // Sources can retire between batches. Consult authoritative storage
1809                // after releasing the source guard, just as the former ID-list scan did.
1810                if !before_work() {
1811                    return false;
1812                }
1813                self.for_each_id(&ids, |_, entity| {
1814                    if Self::scan_entity_visible(snapshot, entity) {
1815                        batch.push(entity.clone());
1816                    }
1817                });
1818                if !before_work() {
1819                    return false;
1820                }
1821                if !consume(&batch) {
1822                    return false;
1823                }
1824            }
1825        }
1826        true
1827    }
1828
1829    /// Fold entities visible under an explicit MVCC snapshot in parallel.
1830    pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
1831        &self,
1832        snapshot: Option<&SnapshotContext>,
1833        init: FInit,
1834        fold: FFold,
1835        reduce: FReduce,
1836    ) -> T
1837    where
1838        T: Send,
1839        FInit: Fn() -> T + Send + Sync,
1840        FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1841        FReduce: Fn(T, T) -> T + Send + Sync,
1842    {
1843        self.fold_entities_parallel(
1844            init,
1845            |acc, entity| {
1846                if Self::scan_entity_visible(snapshot, entity) {
1847                    fold(acc, entity)
1848                } else {
1849                    acc
1850                }
1851            },
1852            reduce,
1853        )
1854    }
1855
1856    /// Visit zone-map candidates visible under an explicit MVCC snapshot.
1857    pub fn scan_for_each_zoned_with_stats<F>(
1858        &self,
1859        snapshot: Option<&SnapshotContext>,
1860        zone_preds: &[(&str, ZoneColPred<'_>)],
1861        mut callback: F,
1862    ) -> SegmentScanStats
1863    where
1864        F: FnMut(&UnifiedEntity) -> bool,
1865    {
1866        self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
1867            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
1868        })
1869    }
1870
1871    /// Collect zone-map candidates visible under an explicit MVCC snapshot.
1872    pub fn scan_zoned_with_stats<F>(
1873        &self,
1874        snapshot: Option<&SnapshotContext>,
1875        zone_preds: &[(&str, ZoneColPred<'_>)],
1876        filter: F,
1877    ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1878    where
1879        F: Fn(&UnifiedEntity) -> bool + Sync,
1880    {
1881        self.query_all_zoned_with_stats(zone_preds, |entity| {
1882            Self::scan_entity_visible(snapshot, entity) && filter(entity)
1883        })
1884    }
1885
1886    /// Query across all segments. Uses parallel scanning for sealed segments
1887    /// when more than one sealed segment exists.
1888    pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
1889    where
1890        F: Fn(&UnifiedEntity) -> bool + Sync,
1891    {
1892        self.query_all_in_segments(|_| true, filter)
1893    }
1894
1895    fn query_all_in_segments<F>(
1896        &self,
1897        segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
1898        filter: F,
1899    ) -> Vec<UnifiedEntity>
1900    where
1901        F: Fn(&UnifiedEntity) -> bool + Sync,
1902    {
1903        let mut results = Vec::new();
1904
1905        // Query growing segment — try non-blocking read first (avoids stalling
1906        // behind an in-progress insert; falls back to blocking if writer is active).
1907        if let Some(growing_arc) = self.growing.read().as_ref() {
1908            let growing = if let Some(g) = growing_arc.try_read() {
1909                g
1910            } else {
1911                growing_arc.read()
1912            };
1913            if segment_filter(&growing) {
1914                results.extend(growing.iter().filter(|e| filter(e)).cloned());
1915            }
1916        }
1917
1918        // Query sealed segments — parallel when multiple exist AND multi-core
1919        let sealed = self.sealed.read();
1920        let use_parallel = sealed.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1921        if use_parallel {
1922            let filter_ref = &filter;
1923            let segment_filter_ref = &segment_filter;
1924            let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1925                sealed
1926                    .iter()
1927                    .map(|segment| {
1928                        s.spawn(move || {
1929                            let segment = segment.read();
1930                            if segment_filter_ref(&segment) {
1931                                segment
1932                                    .iter()
1933                                    .filter(|e| filter_ref(e))
1934                                    .cloned()
1935                                    .collect::<Vec<_>>()
1936                            } else {
1937                                Vec::new()
1938                            }
1939                        })
1940                    })
1941                    .collect::<Vec<_>>()
1942                    .into_iter()
1943                    .map(|handle| handle.join().unwrap_or_default())
1944                    .collect()
1945            });
1946            for batch in segment_results {
1947                results.extend(batch);
1948            }
1949        } else {
1950            for segment in sealed.iter() {
1951                let seg = segment.read();
1952                if segment_filter(&seg) {
1953                    results.extend(seg.iter().filter(|e| filter(e)).cloned());
1954                }
1955            }
1956        }
1957
1958        results
1959    }
1960
1961    /// Probe exact segment membership for a key without materializing matching entities.
1962    ///
1963    /// Returns `false` only when every consulted segment proves absence.
1964    /// Missing segment state is conservative and returns `true`.
1965    pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
1966        if let Some(growing_arc) = self.growing.read().as_ref() {
1967            let growing = growing_arc.read();
1968            if growing.may_contain_exact_key(key) {
1969                return true;
1970            }
1971        } else {
1972            return true;
1973        }
1974
1975        let sealed = self.sealed.read();
1976        for segment_arc in sealed.iter() {
1977            let segment = segment_arc.read();
1978            if segment.may_contain_exact_key(key) {
1979                return true;
1980            }
1981        }
1982
1983        false
1984    }
1985
1986    /// Filter by metadata across all segments
1987    pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
1988        let mut results = Vec::new();
1989
1990        // Growing segment
1991        if let Some(growing_arc) = self.growing.read().as_ref() {
1992            let growing = growing_arc.read();
1993            results.extend(growing.filter_metadata(filters));
1994        }
1995
1996        // Sealed segments
1997        let sealed = self.sealed.read();
1998        for segment in sealed.iter() {
1999            results.extend(segment.filter_metadata(filters));
2000        }
2001
2002        results
2003    }
2004
2005    /// Get entities by kind
2006    pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2007        let mut results = Vec::new();
2008
2009        // Growing segment
2010        if let Some(growing_arc) = self.growing.read().as_ref() {
2011            let growing = growing_arc.read();
2012            for entity in growing.iter_kind(kind) {
2013                results.push(entity.clone());
2014            }
2015        }
2016
2017        // Sealed segments. Iterate through the read guard, not the
2018        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` — that one cannot
2019        // hand out references across the lock, so it returns an empty iterator
2020        // and every sealed entity was silently missing from the result.
2021        let sealed = self.sealed.read();
2022        for segment_arc in sealed.iter() {
2023            let segment = segment_arc.read();
2024            for entity in segment.iter_kind(kind) {
2025                results.push(entity.clone());
2026            }
2027        }
2028
2029        results
2030    }
2031
2032    /// Count entities
2033    pub fn count(&self) -> usize {
2034        self.total_entities_atomic.load(Ordering::Relaxed) as usize
2035    }
2036
2037    /// Get all segment IDs
2038    pub fn segment_ids(&self) -> Vec<SegmentId> {
2039        let mut ids = Vec::new();
2040
2041        if let Some(growing_arc) = self.growing.read().as_ref() {
2042            ids.push(growing_arc.read().id());
2043        }
2044
2045        let sealed = self.sealed.read();
2046        for segment in sealed.iter() {
2047            ids.push(segment.id());
2048        }
2049
2050        ids.extend(self.archived.read().iter().copied());
2051
2052        ids
2053    }
2054
2055    /// Emit a lifecycle event.
2056    ///
2057    /// Perf: this used to push onto a `RwLock<Vec<LifecycleEvent>>`
2058    /// on every insert / delete / seal. Nobody consumes that vec
2059    /// today (no subscription API, `drain_events` has no callers),
2060    /// so the write lock + push was pure tax — and the vec grew
2061    /// unbounded in long-running processes.
2062    ///
2063    /// Current behaviour: no-op. If we ever want the hooks back,
2064    /// replace this with a bounded channel or an actual subscriber
2065    /// registry; the callers (`insert`, `delete`, `maybe_seal_growing`)
2066    /// already pass well-typed events.
2067    #[inline]
2068    #[allow(clippy::unused_self)]
2069    fn emit(&self, _event: LifecycleEvent) {}
2070
2071    /// Drain events. Kept for API compatibility; always returns
2072    /// empty because `emit` no longer buffers.
2073    pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2074        std::mem::take(&mut *self.events.write())
2075    }
2076
2077    /// Run maintenance (would be called periodically in production)
2078    pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2079        // Auto-seal idle segments
2080        self.maybe_seal_growing()?;
2081
2082        if self.config.enable_consolidation {
2083            self.consolidation_tick();
2084        }
2085
2086        Ok(())
2087    }
2088
2089    // ========================================================================
2090    // Consolidation — ADR 0073 §5
2091    // ========================================================================
2092
2093    /// One paced step of sealed-segment consolidation.
2094    ///
2095    /// Starts a run when a threshold has been crossed, copies at most
2096    /// `consolidation_entities_per_tick` live entities into the merged segment,
2097    /// and swaps once every source has been drained. Returns without finishing
2098    /// otherwise — the half-built merged segment waits for the next tick.
2099    fn consolidation_tick(&self) {
2100        // Drop the read guard before `start_consolidation` takes the write
2101        // guard: `parking_lot` locks are not reentrant.
2102        let running = self.consolidation.read().is_some();
2103        if !running && !self.start_consolidation() {
2104            return;
2105        }
2106
2107        // Keep the accounting guard through publication: the merged allocation
2108        // must never disappear between the in-flight and active inventories.
2109        let mut guard = self.consolidation.write();
2110        let Some(run) = guard.as_mut() else {
2111            return;
2112        };
2113        self.copy_bounded(run);
2114        if run.cursor >= run.sources.len() {
2115            if let Some(run) = guard.take() {
2116                self.finish_consolidation(run);
2117            }
2118        }
2119    }
2120
2121    /// Evaluate the trigger and, when it fires, open a consolidation run.
2122    ///
2123    /// Returns `true` when a run was started.
2124    fn start_consolidation(&self) -> bool {
2125        let sources = self.select_candidates();
2126        self.start_consolidation_from_sources(sources)
2127    }
2128
2129    fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2130        if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2131            return false;
2132        }
2133
2134        let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2135        let merged = GrowingSegment::new(merged_id, &self.collection);
2136
2137        *self.consolidation.write() = Some(Consolidation {
2138            sources: sources
2139                .into_iter()
2140                .map(|id| SourceProgress {
2141                    id,
2142                    copied: Vec::new(),
2143                    tombstones_at_open: None,
2144                })
2145                .collect(),
2146            merged_id,
2147            merged,
2148            cursor: 0,
2149            pending_ids: Vec::new(),
2150            pending_cursor: 0,
2151            entities_copied: 0,
2152        });
2153
2154        self.stats.write().consolidation.runs_started += 1;
2155        true
2156    }
2157
2158    /// Run one paced consolidation tick because admission pressure needs
2159    /// reclaimable bytes, even if the background thresholds have not fired.
2160    ///
2161    /// Returns true when the call started or advanced a consolidation run.
2162    pub fn pressure_consolidation_tick(&self) -> bool {
2163        if !self.config.enable_consolidation {
2164            return false;
2165        }
2166
2167        let running = self.consolidation.read().is_some();
2168        if !running {
2169            let sources = self.dirty_sealed_segments();
2170            if !self.start_consolidation_from_sources(sources) {
2171                return false;
2172            }
2173        }
2174
2175        self.consolidation_tick();
2176        true
2177    }
2178
2179    /// Which sealed segments this collection should merge, if any.
2180    ///
2181    /// Three thresholds, any of which arms consolidation:
2182    ///
2183    /// * **sealed-segment count** above `max_sealed_segments` — every sealed
2184    ///   segment merges, which is what that knob always claimed to do.
2185    /// * **tombstone ratio** — dead entities over total entities across the
2186    ///   sealed set, past [`CONSOLIDATION_TOMBSTONE_RATIO`].
2187    /// * **fragmentation** — reclaimable bytes over held bytes, past
2188    ///   [`CONSOLIDATION_FRAGMENTATION_RATIO`].
2189    ///
2190    /// The count trigger takes every sealed segment; the two budget triggers
2191    /// take only the segments that actually carry tombstones, so a clean
2192    /// segment is never rewritten for nothing.
2193    fn select_candidates(&self) -> Vec<SegmentId> {
2194        let sealed = self.sealed.read();
2195        if sealed.is_empty() {
2196            return Vec::new();
2197        }
2198
2199        if sealed.len() > self.config.max_sealed_segments {
2200            return sealed.iter().map(|segment| segment.read().id()).collect();
2201        }
2202
2203        let mut live = 0usize;
2204        let mut tombstones = 0usize;
2205        let mut held = 0u64;
2206        let mut reclaimable = 0u64;
2207        let mut dirty = Vec::new();
2208
2209        for segment_arc in sealed.iter() {
2210            let segment = segment_arc.read();
2211            live += segment.live_entity_count();
2212            tombstones += segment.tombstone_count();
2213            held += segment.resident_bytes();
2214            reclaimable += segment.reclaimable_bytes();
2215            if segment.tombstone_count() > 0 {
2216                dirty.push(segment.id());
2217            }
2218        }
2219
2220        let total = live + tombstones;
2221        let tombstone_ratio = if total == 0 {
2222            0.0
2223        } else {
2224            tombstones as f64 / total as f64
2225        };
2226        let fragmentation = if held == 0 {
2227            0.0
2228        } else {
2229            reclaimable as f64 / held as f64
2230        };
2231
2232        if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2233            || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2234        {
2235            return dirty;
2236        }
2237
2238        Vec::new()
2239    }
2240
2241    fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2242        self.sealed
2243            .read()
2244            .iter()
2245            .filter_map(|segment_arc| {
2246                let segment = segment_arc.read();
2247                (segment.reclaimable_bytes() > 0).then(|| segment.id())
2248            })
2249            .collect()
2250    }
2251
2252    /// A single candidate is still worth merging when it carries tombstones —
2253    /// the merge drops them. A single clean candidate is not.
2254    fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2255        if sources.is_empty() {
2256            return false;
2257        }
2258        let sealed = self.sealed.read();
2259        sealed.iter().any(|segment_arc| {
2260            let segment = segment_arc.read();
2261            sources.contains(&segment.id()) && segment.tombstone_count() > 0
2262        })
2263    }
2264
2265    fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2266        let sealed = self.sealed.read();
2267        sealed
2268            .iter()
2269            .find(|segment| segment.read().id() == id)
2270            .map(Arc::clone)
2271    }
2272
2273    /// Copy at most one tick's worth of live entities into the merged segment.
2274    ///
2275    /// The per-tick bound is the pacing contract: a tick's cost is O(bound),
2276    /// independent of how many entities the run still has to move.
2277    fn copy_bounded(&self, run: &mut Consolidation) {
2278        let mut budget = self.config.consolidation_entities_per_tick;
2279
2280        while budget > 0 && run.cursor < run.sources.len() {
2281            let source_id = run.sources[run.cursor].id;
2282            let Some(source_arc) = self.sealed_segment(source_id) else {
2283                // The source vanished. Only consolidation removes sealed
2284                // segments and it is single-threaded per collection, so this
2285                // cannot happen today — skip it rather than resurrect its rows.
2286                run.cursor += 1;
2287                run.pending_ids.clear();
2288                run.pending_cursor = 0;
2289                continue;
2290            };
2291
2292            if run.sources[run.cursor].tombstones_at_open.is_none() {
2293                let source = source_arc.read();
2294                run.pending_ids = source.live_entity_ids();
2295                run.pending_cursor = 0;
2296                run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2297            }
2298
2299            {
2300                let source = source_arc.read();
2301                while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2302                    let id = run.pending_ids[run.pending_cursor];
2303                    run.pending_cursor += 1;
2304                    budget -= 1;
2305
2306                    // Tombstoned between the snapshot and now — it must not be
2307                    // resurrected into the merged segment.
2308                    let Some(entity) = source.get(id) else {
2309                        continue;
2310                    };
2311                    let metadata = source.get_metadata(id);
2312                    run.merged.adopt_entity(entity.clone(), metadata);
2313                    run.sources[run.cursor].copied.push(id);
2314                    run.entities_copied += 1;
2315                }
2316            }
2317
2318            if run.pending_cursor >= run.pending_ids.len() {
2319                run.cursor += 1;
2320                run.pending_ids.clear();
2321                run.pending_cursor = 0;
2322            }
2323        }
2324    }
2325
2326    /// Atomically replace the source segments with the merged one.
2327    ///
2328    /// The `sealed` write lock is the boundary: a reader holds the read lock
2329    /// for the whole of its scan, so it observes either the old set or the new
2330    /// set, never a gap and never both copies of a row.
2331    fn finish_consolidation(&self, mut run: Consolidation) {
2332        let mut sealed = self.sealed.write();
2333
2334        let mut positions = Vec::with_capacity(run.sources.len());
2335        for source in &run.sources {
2336            if let Some(index) = sealed
2337                .iter()
2338                .position(|segment| segment.read().id() == source.id)
2339            {
2340                positions.push(index);
2341            }
2342        }
2343        if positions.is_empty() {
2344            return;
2345        }
2346        positions.sort_unstable();
2347
2348        // Rows deleted from a source after we copied them must not come back.
2349        // A source whose tombstone count is unchanged saw no deletes while the
2350        // merge ran, so its copied rows need no per-row check.
2351        let mut stale = Vec::new();
2352        for source in &run.sources {
2353            let Some(source_arc) = sealed
2354                .iter()
2355                .find(|segment| segment.read().id() == source.id)
2356            else {
2357                continue;
2358            };
2359            let segment = source_arc.read();
2360            if source.tombstones_at_open == Some(segment.tombstone_count()) {
2361                continue;
2362            }
2363            stale.extend(
2364                source
2365                    .copied
2366                    .iter()
2367                    .copied()
2368                    .filter(|id| !segment.contains(*id)),
2369            );
2370        }
2371        for id in stale {
2372            run.merged.evict_entity(id);
2373        }
2374
2375        let reclaimed_bytes: u64 = positions
2376            .iter()
2377            .map(|&index| sealed[index].read().resident_bytes())
2378            .sum();
2379        let reclaimed_tombstones: u64 = positions
2380            .iter()
2381            .map(|&index| sealed[index].read().tombstone_count() as u64)
2382            .sum();
2383
2384        if run.merged.seal().is_err() {
2385            return;
2386        }
2387        let merged_bytes = run.merged.resident_bytes();
2388        let merged_arc = Arc::new(RwLock::new(run.merged));
2389
2390        {
2391            let mut retired = self.retired.write();
2392            retired.retain(|source| source.strong_count() > 0);
2393            for &index in positions.iter().rev() {
2394                let source = sealed.remove(index);
2395                if Arc::strong_count(&source) > 1 {
2396                    retired.push(Arc::downgrade(&source));
2397                }
2398            }
2399        }
2400        let insert_at = positions[0].min(sealed.len());
2401        sealed.insert(insert_at, Arc::clone(&merged_arc));
2402        drop(sealed);
2403
2404        // Remap the entity→segment hints that pointed at the retired sources.
2405        let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
2406        {
2407            let mut entity_segment = self.entity_segment.write();
2408            for segment_id in entity_segment.values_mut() {
2409                if source_ids.contains(segment_id) {
2410                    *segment_id = run.merged_id;
2411                }
2412            }
2413        }
2414
2415        {
2416            let mut stats = self.stats.write();
2417            stats.sealed_count = stats
2418                .sealed_count
2419                .saturating_sub(positions.len())
2420                .saturating_add(1);
2421            let consolidation = &mut stats.consolidation;
2422            consolidation.runs_completed += 1;
2423            consolidation.segments_merged += positions.len() as u64;
2424            consolidation.tombstones_reclaimed += reclaimed_tombstones;
2425            consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
2426        }
2427
2428        self.emit(LifecycleEvent::SegmentConsolidated {
2429            source: source_ids,
2430            target: run.merged_id,
2431        });
2432    }
2433
2434    /// Entities copied by the consolidation currently in flight, or `None` when
2435    /// none is running. Exposed for the pacing tests.
2436    #[cfg(test)]
2437    fn consolidation_progress(&self) -> Option<u64> {
2438        self.consolidation
2439            .read()
2440            .as_ref()
2441            .map(|run| run.entities_copied)
2442    }
2443}
2444
2445// Implement the Arc<RwLock<GrowingSegment>> as UnifiedSegment
2446// This is needed because we store growing segments in the sealed list after sealing
2447impl UnifiedSegment for Arc<RwLock<GrowingSegment>> {
2448    fn id(&self) -> SegmentId {
2449        self.read().id()
2450    }
2451
2452    fn state(&self) -> SegmentState {
2453        self.read().state()
2454    }
2455
2456    fn collection(&self) -> &str {
2457        // This is a limitation - we'd need to store collection in the Arc wrapper
2458        "unknown"
2459    }
2460
2461    fn stats(&self) -> SegmentStats {
2462        self.read().stats()
2463    }
2464
2465    fn entity_count(&self) -> usize {
2466        self.read().entity_count()
2467    }
2468
2469    fn contains(&self, id: EntityId) -> bool {
2470        self.read().contains(id)
2471    }
2472
2473    fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2474        // This is tricky with RwLock - we can't return a reference
2475        // In production, we'd use a different approach
2476        None
2477    }
2478
2479    fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2480        None
2481    }
2482
2483    fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2484        self.write().insert(entity)
2485    }
2486
2487    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2488        self.write().update(entity)
2489    }
2490
2491    fn update_hot(
2492        &mut self,
2493        entity: UnifiedEntity,
2494        modified_columns: &[String],
2495    ) -> Result<(), SegmentError> {
2496        self.write().update_hot(entity, modified_columns)
2497    }
2498
2499    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2500        self.write().delete(id)
2501    }
2502
2503    fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2504        self.read().get_metadata(id)
2505    }
2506
2507    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2508        self.write().set_metadata(id, metadata)
2509    }
2510
2511    fn seal(&mut self) -> Result<(), SegmentError> {
2512        self.write().seal()
2513    }
2514
2515    fn should_seal(&self, config: &SegmentConfig) -> bool {
2516        self.read().should_seal(config)
2517    }
2518
2519    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2520        // Cannot return iterator with RwLock
2521        Box::new(std::iter::empty())
2522    }
2523
2524    fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2525        Box::new(std::iter::empty())
2526    }
2527
2528    fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2529        self.read().filter_metadata(filters)
2530    }
2531}
2532
2533#[cfg(test)]
2534mod tests {
2535    use std::collections::HashSet;
2536
2537    use super::*;
2538    use crate::runtime::mvcc::SnapshotContext;
2539    use crate::storage::transaction::snapshot::{Snapshot, SnapshotManager};
2540    use crate::storage::unified::entity::{EntityData, EntityKind, RowData};
2541    use reddb_types::Value;
2542
2543    #[test]
2544    fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
2545        let manager = SegmentManager::with_config(
2546            "vectors",
2547            ManagerConfig {
2548                max_sealed_segments: 1,
2549                consolidation_entities_per_tick: 10_000,
2550                ..Default::default()
2551            },
2552        );
2553        for part in 0..3 {
2554            manager
2555                .bulk_insert(
2556                    (1..=300)
2557                        .map(|offset| {
2558                            UnifiedEntity::vector(
2559                                EntityId::new(part * 300 + offset),
2560                                "vectors",
2561                                vec![1.0, 0.0],
2562                            )
2563                        })
2564                        .collect(),
2565                )
2566                .expect("batch");
2567            if part < 2 {
2568                manager.force_seal().expect("seal");
2569            }
2570        }
2571        let original_growing = manager.growing.read().as_ref().expect("growing").clone();
2572        let mut changed = false;
2573        let mut seen = Vec::new();
2574        assert!(manager.scan_batches(
2575            None,
2576            || true,
2577            |batch| {
2578                assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
2579                assert!(
2580                    manager.growing.try_write().is_some(),
2581                    "consumer holds no topology guard"
2582                );
2583                assert!(
2584                    original_growing.try_write().is_some(),
2585                    "consumer holds no segment guard"
2586                );
2587                for entity in batch {
2588                    assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
2589                    seen.push(entity.id.raw());
2590                }
2591                if !changed {
2592                    changed = true;
2593                    manager.force_seal().expect("seal during scan");
2594                    manager
2595                        .insert(UnifiedEntity::vector(
2596                            EntityId::new(5000),
2597                            "vectors",
2598                            vec![1.0, 0.0],
2599                        ))
2600                        .expect("new write");
2601                    drain_maintenance(&manager);
2602                    assert!(manager.stats().consolidation.runs_completed > 0);
2603                    assert!(manager
2604                        .delete(EntityId::new(1))
2605                        .expect("delete after source retirement"));
2606                    assert!(manager.get(EntityId::new(1)).is_none());
2607                }
2608                true
2609            }
2610        ));
2611        seen.sort_unstable();
2612        assert_eq!(
2613            seen,
2614            (2..=900).collect::<Vec<_>>(),
2615            "no duplicate, missing or late candidates"
2616        );
2617        let mut after = Vec::new();
2618        assert!(manager.scan_batches(
2619            None,
2620            || true,
2621            |batch| {
2622                after.extend(batch.iter().map(|entity| entity.id.raw()));
2623                true
2624            }
2625        ));
2626        after.sort_unstable();
2627        let mut expected: Vec<_> = (2..=900).collect();
2628        expected.push(5000);
2629        assert_eq!(after, expected, "adopted entities have scan positions");
2630    }
2631
2632    #[test]
2633    fn bounded_scan_applies_snapshot_to_relocated_versions() {
2634        for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
2635            let manager = SegmentManager::with_config(
2636                "vectors",
2637                ManagerConfig {
2638                    max_sealed_segments: 1,
2639                    ..Default::default()
2640                },
2641            );
2642            for ids in [[1, 2], [3, 4]] {
2643                let entities = ids.map(|id| {
2644                    let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
2645                    entity.set_xmin(3);
2646                    entity
2647                });
2648                if bulk {
2649                    manager
2650                        .bulk_insert(entities.into())
2651                        .expect("initial versions");
2652                } else {
2653                    for entity in entities {
2654                        manager.insert(entity).expect("initial version");
2655                    }
2656                }
2657                manager.force_seal().expect("seal source");
2658            }
2659            let snapshot = SnapshotContext {
2660                snapshot: Snapshot {
2661                    xid: 10,
2662                    in_progress: HashSet::new(),
2663                },
2664                manager: Arc::new(SnapshotManager::new()),
2665                own_xids: HashSet::new(),
2666                requires_index_fallback: false,
2667                serializable_reader: None,
2668            };
2669            let mut changed = false;
2670            let mut seen = Vec::new();
2671            assert!(manager.scan_batches(
2672                Some(&snapshot),
2673                || true,
2674                |batch| {
2675                    seen.extend(batch.iter().map(|entity| entity.id.raw()));
2676                    if !changed {
2677                        changed = true;
2678                        if consolidate {
2679                            drain_maintenance(&manager);
2680                            assert!(manager.stats().consolidation.runs_completed > 0);
2681                        }
2682                        for (id, xmax) in [(3, 8), (4, 12)] {
2683                            let mut entity =
2684                                manager.get(EntityId::new(id)).expect("retained version");
2685                            entity.set_xmax(xmax);
2686                            manager
2687                                .update(entity)
2688                                .expect("update after source retirement");
2689                        }
2690                    }
2691                    true
2692                }
2693            ));
2694            seen.sort_unstable();
2695            assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
2696            let mut again = Vec::new();
2697            assert!(manager.scan_batches(
2698                Some(&snapshot),
2699                || true,
2700                |batch| {
2701                    again.extend(batch.iter().map(|entity| entity.id.raw()));
2702                    true
2703                }
2704            ));
2705            again.sort_unstable();
2706            assert_eq!(
2707                again,
2708                vec![1, 2, 4],
2709                "old slots do not duplicate relocated versions"
2710            );
2711            let mut current = Vec::new();
2712            assert!(manager.scan_batches(
2713                None,
2714                || true,
2715                |batch| {
2716                    current.extend(batch.iter().map(|entity| entity.id.raw()));
2717                    true
2718                }
2719            ));
2720            current.sort_unstable();
2721            assert_eq!(
2722                current,
2723                vec![1, 2],
2724                "current view excludes both deleted versions"
2725            );
2726        }
2727    }
2728
2729    #[test]
2730    fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
2731        let manager = SegmentManager::new("mixed");
2732        let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
2733        manager
2734            .bulk_insert((1..=300).map(vector).collect())
2735            .expect("flat");
2736        manager.insert(vector(500)).expect("hash insert");
2737        manager
2738            .bulk_insert(vec![vector(502), vector(505)])
2739            .expect("gapped bulk");
2740        manager.delete(EntityId::new(2)).expect("flat delete");
2741        manager.delete(EntityId::new(505)).expect("hash delete");
2742        let mut changed = false;
2743        let mut seen = Vec::new();
2744        assert!(manager.scan_batches(
2745            None,
2746            || true,
2747            |batch| {
2748                seen.extend(batch.iter().map(|entity| entity.id.raw()));
2749                if !changed {
2750                    changed = true;
2751                    manager
2752                        .insert(vector(700))
2753                        .expect("append after cursor boundary");
2754                    manager
2755                        .update(UnifiedEntity::graph_node(
2756                            EntityId::new(500),
2757                            "mixed",
2758                            "Node",
2759                            HashMap::new(),
2760                        ))
2761                        .expect("structural update");
2762                }
2763                true
2764            }
2765        ));
2766        seen.sort_unstable();
2767        let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
2768        expected.extend([500, 502]);
2769        assert_eq!(seen, expected);
2770    }
2771
2772    #[test]
2773    fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
2774        let manager = SegmentManager::new("vectors");
2775        let mut entities = Vec::new();
2776        for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
2777            let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
2778            entity.set_xmin(xmin);
2779            entity.set_xmax(xmax);
2780            entities.push(entity);
2781        }
2782        manager.bulk_insert(entities).expect("versions");
2783        manager.force_seal().expect("seal");
2784        let snapshot = SnapshotContext {
2785            snapshot: Snapshot {
2786                xid: 10,
2787                in_progress: HashSet::from([4]),
2788            },
2789            manager: Arc::new(SnapshotManager::new()),
2790            own_xids: HashSet::new(),
2791            requires_index_fallback: false,
2792            serializable_reader: None,
2793        };
2794        let mut work = 0;
2795        let mut callbacks = 0;
2796        assert!(!manager.scan_batches(
2797            Some(&snapshot),
2798            || {
2799                work += 1;
2800                work <= 2
2801            },
2802            |_| {
2803                callbacks += 1;
2804                true
2805            }
2806        ));
2807        assert_eq!(callbacks, 0, "a stopped batch is not consumed");
2808        for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
2809            let mut seen = Vec::new();
2810            assert!(manager.scan_batches(
2811                view,
2812                || true,
2813                |batch| {
2814                    seen.extend(batch.iter().map(|entity| entity.id.raw()));
2815                    true
2816                }
2817            ));
2818            seen.sort_unstable();
2819            assert_eq!(seen, expected);
2820        }
2821    }
2822
2823    #[test]
2824    fn test_manager_basic() {
2825        let manager = SegmentManager::new("test_collection");
2826
2827        let entity = UnifiedEntity::table_row(
2828            manager.next_entity_id(),
2829            "users",
2830            1,
2831            vec![Value::text("Alice".to_string())],
2832        );
2833
2834        let id = manager.insert(entity).unwrap();
2835        assert!(manager.get(id).is_some());
2836        assert_eq!(manager.count(), 1);
2837    }
2838
2839    #[test]
2840    fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
2841        let manager = Arc::new(SegmentManager::new("mixed"));
2842        manager
2843            .bulk_insert(
2844                (1..=128)
2845                    .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
2846                    .collect(),
2847            )
2848            .expect("vector-only segment");
2849        manager.force_seal().expect("seal vectors");
2850        let mut old =
2851            UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
2852        old.set_xmin(3);
2853        old.set_xmax(12);
2854        let mut new =
2855            UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
2856        new.set_xmin(12);
2857        let edge = UnifiedEntity::graph_edge(
2858            EntityId::new(202),
2859            "mixed",
2860            "a",
2861            "b",
2862            1.0,
2863            Default::default(),
2864        );
2865        manager
2866            .bulk_insert(vec![old, new, edge])
2867            .expect("physical graph versions");
2868        manager.force_seal().expect("seal graph");
2869        manager
2870            .insert(UnifiedEntity::graph_node(
2871                EntityId::new(203),
2872                "mixed",
2873                "person",
2874                Default::default(),
2875            ))
2876            .expect("growing node");
2877        let snapshot = SnapshotContext {
2878            snapshot: Snapshot {
2879                xid: 10,
2880                in_progress: HashSet::new(),
2881            },
2882            manager: Arc::new(SnapshotManager::new()),
2883            own_xids: HashSet::new(),
2884            requires_index_fallback: false,
2885            serializable_reader: None,
2886        };
2887        std::thread::spawn(move || {
2888            for view in [None, Some(&snapshot)] {
2889                for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
2890                    let expected: HashSet<_> = manager
2891                        .scan(view, |item| kind.matches(&item.kind))
2892                        .into_iter()
2893                        .map(|item| item.id)
2894                        .collect();
2895                    let actual: HashSet<_> = manager
2896                        .scan_graph_kind(view, kind)
2897                        .into_iter()
2898                        .map(|item| item.id)
2899                        .collect();
2900                    assert_eq!(actual, expected);
2901                    let mut visited = 0;
2902                    let mut candidates = HashSet::new();
2903                    manager.scan_graph_candidates_for_each(
2904                        view,
2905                        kind,
2906                        || true,
2907                        |item| {
2908                            visited += 1;
2909                            if kind.matches(&item.kind) {
2910                                candidates.insert(item.id);
2911                            }
2912                            true
2913                        },
2914                    );
2915                    assert_eq!(candidates, expected);
2916                    assert_eq!(
2917                        visited,
2918                        if matches!(kind, GraphEntityKind::Node) {
2919                            3
2920                        } else {
2921                            2
2922                        }
2923                    );
2924                }
2925            }
2926            let ids: HashSet<_> = manager
2927                .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
2928                .into_iter()
2929                .map(|item| item.id.raw())
2930                .collect();
2931            assert_eq!(ids, HashSet::from([200, 203]));
2932            let ids: HashSet<_> = manager
2933                .scan_graph_kind(None, GraphEntityKind::Node)
2934                .into_iter()
2935                .map(|item| item.id.raw())
2936                .collect();
2937            assert_eq!(ids, HashSet::from([201, 203]));
2938            let mut visits = 0;
2939            manager.scan_graph_candidates_for_each(
2940                None,
2941                GraphEntityKind::Node,
2942                || true,
2943                |_| {
2944                    visits += 1;
2945                    false
2946                },
2947            );
2948            assert_eq!(
2949                visits, 1,
2950                "early termination must also stop subsequent segments"
2951            );
2952        })
2953        .join()
2954        .expect("snapshot worker");
2955    }
2956
2957    #[test]
2958    fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
2959        let manager = Arc::new(SegmentManager::new("accounts"));
2960        let cases = [
2961            ("legacy row", 0, 0, true),
2962            ("committed creator", 3, 0, true),
2963            ("future creator", 11, 0, false),
2964            ("in-progress creator", 4, 0, false),
2965            ("committed deleter", 3, 7, false),
2966            ("future deleter", 3, 11, true),
2967            ("in-progress deleter", 3, 4, true),
2968        ];
2969
2970        for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
2971            let row_id = index as u64 + 1;
2972            let mut entity = UnifiedEntity::table_row(
2973                EntityId::new(row_id),
2974                "accounts",
2975                row_id,
2976                vec![Value::Integer(row_id as i64)],
2977            );
2978            entity.set_xmin(*xmin);
2979            entity.set_xmax(*xmax);
2980            manager.insert(entity).unwrap();
2981        }
2982
2983        let snapshot = SnapshotContext {
2984            snapshot: Snapshot {
2985                xid: 10,
2986                in_progress: HashSet::from([4]),
2987            },
2988            manager: Arc::new(SnapshotManager::new()),
2989            own_xids: HashSet::new(),
2990            requires_index_fallback: false,
2991            serializable_reader: None,
2992        };
2993
2994        let raw_visible_row_ids: HashSet<u64> = manager
2995            .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
2996            .into_iter()
2997            .filter_map(|entity| match entity.kind {
2998                EntityKind::TableRow { row_id, .. } => Some(row_id),
2999                _ => None,
3000            })
3001            .collect();
3002
3003        let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3004            .join()
3005            .unwrap();
3006        let visible_row_ids: HashSet<u64> = scanned
3007            .into_iter()
3008            .filter_map(|entity| match entity.kind {
3009                EntityKind::TableRow { row_id, .. } => Some(row_id),
3010                _ => None,
3011            })
3012            .collect();
3013        assert_eq!(visible_row_ids, raw_visible_row_ids);
3014
3015        for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3016            let row_id = index as u64 + 1;
3017            assert_eq!(
3018                visible_row_ids.contains(&row_id),
3019                *expected_visible,
3020                "{name} visibility mismatch"
3021            );
3022        }
3023    }
3024
3025    #[test]
3026    fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3027        // A `None` snapshot is the autocommit / prepared-fast-path read: it
3028        // must keep the resolver fallback (xmax == 0), never show every
3029        // version, and never panic (issue #2136 review).
3030        let manager = SegmentManager::new("accounts");
3031        let mut live =
3032            UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3033        live.set_xmin(2);
3034        live.set_xmax(0);
3035        manager.insert(live).unwrap();
3036
3037        let mut superseded =
3038            UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3039        superseded.set_xmin(2);
3040        superseded.set_xmax(9);
3041        manager.insert(superseded).unwrap();
3042
3043        let rows: Vec<u64> = manager
3044            .scan(None, |_| true)
3045            .into_iter()
3046            .filter_map(|entity| match entity.kind {
3047                EntityKind::TableRow { row_id, .. } => Some(row_id),
3048                _ => None,
3049            })
3050            .collect();
3051        assert_eq!(
3052            rows,
3053            vec![1],
3054            "frameless scan must hide superseded versions"
3055        );
3056    }
3057
3058    #[test]
3059    fn query_scan_callers_use_snapshot_api_for_visibility() {
3060        let table = include_str!("../../runtime/query_exec/table.rs");
3061        let aggregate = include_str!("../../runtime/query_exec/aggregate.rs");
3062
3063        assert_eq!(
3064            table.matches(".for_each_entity(|entity|").count(),
3065            1,
3066            "only the visibility-independent schema inspection may use the raw table iterator"
3067        );
3068        assert!(!table.contains(".query_all_zoned_with_stats("));
3069        assert!(!table.contains(".for_each_entity_zoned_with_stats("));
3070        assert!(!table.contains("TableRowMvccReadResolver::captured"));
3071
3072        assert!(!aggregate.contains("TableRowMvccReadResolver"));
3073        assert!(!aggregate.contains(".for_each_entity("));
3074        assert!(!aggregate.contains(".fold_entities_parallel("));
3075    }
3076
3077    #[test]
3078    fn remaining_visibility_scan_callers_use_snapshot_api() {
3079        let aggregate_pushdown =
3080            include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs");
3081        let ask = include_str!("../../runtime/ask_pipeline.rs");
3082        let dml = include_str!("../../runtime/impl_dml.rs");
3083        let dml_targets = include_str!("../../runtime/dml_target_scan.rs");
3084        let events = include_str!("../../runtime/impl_events.rs");
3085        let graph_commands = include_str!("../../runtime/impl_graph_commands.rs");
3086        let graph_dsl = include_str!("../../runtime/graph_dsl.rs");
3087        let graph_tvf = include_str!("../../runtime/graph_tvf.rs");
3088        let kv = include_str!("../../runtime/impl_kv.rs");
3089        let queue = include_str!("../../runtime/impl_queue.rs");
3090        let primary_queue = include_str!("../../runtime/primary_queue_store.rs");
3091        let record_search = include_str!("../../runtime/record_search.rs");
3092        let search = include_str!("../../runtime/impl_search.rs");
3093        let vector = include_str!("../../runtime/query_exec/vector.rs");
3094
3095        assert!(!aggregate_pushdown.contains("TableRowMvccReadResolver"));
3096        assert!(!aggregate_pushdown.contains(".for_each_entity("));
3097        assert!(!ask.contains("for entity in manager.query_all(|_| true)"));
3098        assert!(!dml.contains("let entities = manager.query_all(|_| true);"));
3099        // The DML target scan keeps raw iteration on purpose: its
3100        // visibility is kind-conditional (live table rows at the tip
3101        // version, everything else under the snapshot), which the uniform
3102        // scan API cannot express. `candidate_visible` is the single gate.
3103        assert_eq!(
3104            dml_targets
3105                .matches("self.candidate_visible(snapshot.as_ref(), entity)")
3106                .count(),
3107            2,
3108            "both raw DML iterations must route through candidate_visible"
3109        );
3110        // The event-backfill dedup set is deliberately visibility-free:
3111        // it must include consumed messages or backfill re-enqueues them.
3112        assert_eq!(
3113            events.matches("manager.query_all(").count(),
3114            1,
3115            "only the visibility-free backfill dedup may use query_all"
3116        );
3117        assert!(!graph_commands.contains("TableRowMvccReadResolver"));
3118        assert!(!graph_dsl.contains("entity_visible_with_context(snap_ctx.as_ref(), e)"));
3119        assert!(!graph_tvf.contains("entity_visible_with_context(snap_ctx.as_ref(), &entity)"));
3120        assert!(!kv.contains("TableRowMvccReadResolver"));
3121        assert!(!queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3122        assert!(!primary_queue.contains("entity_visible_with_context(snap_ctx.as_ref(), entity)"));
3123        assert!(!record_search.contains("TableRowMvccReadResolver"));
3124        assert!(!search.contains("for entity in manager.query_all(|_| true)"));
3125        assert!(!vector.contains("manager.query_all("));
3126    }
3127
3128    /// Strip `#[cfg(test)] mod … { … }` blocks so the raw-iteration gate
3129    /// only sees production code. Blocks are matched at column zero and
3130    /// closed by brace balance, which is why a file may carry a test
3131    /// module in the middle and still keep production code after it.
3132    fn production_source(src: &str) -> String {
3133        let mut kept: Vec<&str> = Vec::new();
3134        let lines: Vec<&str> = src.lines().collect();
3135        let mut idx = 0;
3136        while idx < lines.len() {
3137            let starts_test_mod = lines[idx].starts_with("#[cfg(test)]")
3138                && lines
3139                    .get(idx + 1)
3140                    .is_some_and(|next| next.starts_with("mod "));
3141            if !starts_test_mod {
3142                kept.push(lines[idx]);
3143                idx += 1;
3144                continue;
3145            }
3146            idx += 1;
3147            let mut depth: isize = 0;
3148            while idx < lines.len() {
3149                depth += lines[idx].matches('{').count() as isize;
3150                depth -= lines[idx].matches('}').count() as isize;
3151                idx += 1;
3152                if depth <= 0 {
3153                    break;
3154                }
3155            }
3156        }
3157        kept.join("\n")
3158    }
3159
3160    /// Count raw (visibility-bypassing) iteration entry points.
3161    fn raw_iteration_sites(src: &str) -> usize {
3162        src.matches("query_all(").count()
3163            + src.matches(".for_each_entity").count()
3164            + src.matches("fold_entities_parallel(").count()
3165    }
3166
3167    #[test]
3168    fn raw_iteration_confined_to_maintenance_allowlist() {
3169        // The query paths migrated by Spec #2107 batches 1 and 2. Inside
3170        // this perimeter raw iteration is maintenance-only: it bypasses
3171        // MVCC visibility entirely, so every remaining site is either an
3172        // internal catalog/bookkeeping read that must observe all physical
3173        // versions, or one of the three exceptions the batch reviews
3174        // documented. A new raw-iteration site changes the count and fails
3175        // this gate — route it through `SegmentManager::scan*` instead.
3176        let allowlist: &[(&str, &str, usize, &str)] = &[
3177            (
3178                "runtime/query_exec/table.rs",
3179                include_str!("../../runtime/query_exec/table.rs"),
3180                1,
3181                "documented exception: schema inspection (does the collection ever \
3182                 observe these fields?) is visibility-independent",
3183            ),
3184            (
3185                "runtime/query_exec/aggregate.rs",
3186                include_str!("../../runtime/query_exec/aggregate.rs"),
3187                0,
3188                "fully migrated by batch 1",
3189            ),
3190            (
3191                "runtime/query_exec/aggregate_pushdown_dispatch.rs",
3192                include_str!("../../runtime/query_exec/aggregate_pushdown_dispatch.rs"),
3193                0,
3194                "fully migrated by batch 1",
3195            ),
3196            (
3197                "runtime/query_exec/vector.rs",
3198                include_str!("../../runtime/query_exec/vector.rs"),
3199                0,
3200                "fully migrated by batch 2",
3201            ),
3202            (
3203                "runtime/ask_pipeline.rs",
3204                include_str!("../../runtime/ask_pipeline.rs"),
3205                0,
3206                "fully migrated by batch 2",
3207            ),
3208            (
3209                "runtime/impl_dml.rs",
3210                include_str!("../../runtime/impl_dml.rs"),
3211                1,
3212                "documented exception: ON CONFLICT resolves a unique key against \
3213                 physical versions on purpose. A snapshot scan cannot see a row an \
3214                 uncommitted concurrent writer already holds the key for, which is \
3215                 exactly the case that must raise a serialization conflict rather \
3216                 than insert a duplicate; the same pass reaps index entries left by \
3217                 aborted writers. \
3218                 TODO(#2263 follow-up): narrow the candidates through the unique \
3219                 index instead of walking the collection",
3220            ),
3221            (
3222                "runtime/dml_target_scan.rs",
3223                include_str!("../../runtime/dml_target_scan.rs"),
3224                2,
3225                "documented exception: kind-conditional visibility the uniform scan \
3226                 API cannot express; both iterations gate on candidate_visible",
3227            ),
3228            (
3229                "runtime/impl_events.rs",
3230                include_str!("../../runtime/impl_events.rs"),
3231                1,
3232                "documented exception: backfill dedup must include consumed messages, \
3233                 so it is deliberately visibility-free",
3234            ),
3235            (
3236                "runtime/impl_graph_commands.rs",
3237                include_str!("../../runtime/impl_graph_commands.rs"),
3238                0,
3239                "native node properties resolve one snapshot-visible, RLS-admitted version",
3240            ),
3241            (
3242                "runtime/graph_dsl.rs",
3243                include_str!("../../runtime/graph_dsl.rs"),
3244                1,
3245                "remaining legacy debt: unused edge-property helper; search expansion \
3246                 now uses snapshot visitors and RLS-gated hydration",
3247            ),
3248            (
3249                "runtime/graph_tvf.rs",
3250                include_str!("../../runtime/graph_tvf.rs"),
3251                0,
3252                "fully migrated by batch 2",
3253            ),
3254            (
3255                "runtime/impl_kv.rs",
3256                include_str!("../../runtime/impl_kv.rs"),
3257                4,
3258                "two non-versioned-collection fallbacks (the versioned arms scan under \
3259                 the captured snapshot) plus the two vault-version readers, which exist \
3260                 to expose every physical version. \
3261                 TODO(#2138 follow-up): vault_versions/latest_vault_entries never \
3262                 capture a snapshot",
3263            ),
3264            (
3265                "runtime/impl_queue.rs",
3266                include_str!("../../runtime/impl_queue.rs"),
3267                10,
3268                "maintenance: red_queue_meta bookkeeping (config, groups, acks, \
3269                 pending, dedup TTL reaper, gauge counts) — queue message bodies are \
3270                 read through scan. \
3271                 TODO(#2138 follow-up): QUEUE PENDING projects meta rows to clients",
3272            ),
3273            (
3274                "runtime/primary_queue_store.rs",
3275                include_str!("../../runtime/primary_queue_store.rs"),
3276                3,
3277                "maintenance: generic red_queue_meta read/delete helpers and the \
3278                 next-position allocator, which must not collide with rows invisible \
3279                 to the writer's snapshot",
3280            ),
3281            (
3282                "runtime/record_search.rs",
3283                include_str!("../../runtime/record_search.rs"),
3284                0,
3285                "fully migrated by batch 2",
3286            ),
3287            (
3288                "runtime/search_graph.rs",
3289                include_str!("../../runtime/search_graph.rs"),
3290                0,
3291                "search graph preparation uses snapshot visitors and gated hydration",
3292            ),
3293            (
3294                "runtime/impl_search.rs",
3295                include_str!("../../runtime/impl_search.rs"),
3296                1,
3297                "maintenance: ask-audit retention sweep must see every physical row",
3298            ),
3299        ];
3300
3301        for (path, src, allowed, reason) in allowlist {
3302            let found = raw_iteration_sites(&production_source(src));
3303            assert_eq!(
3304                found, *allowed,
3305                "{path}: {found} raw-iteration sites, allowlist permits {allowed} \
3306                 ({reason}). Raw iteration bypasses MVCC visibility — use \
3307                 SegmentManager::scan / scan_for_each / scan_fold_parallel / \
3308                 scan_zoned_with_stats, or extend the allowlist with a maintenance \
3309                 justification."
3310            );
3311        }
3312    }
3313
3314    #[test]
3315    fn explicit_snapshot_scan_ignores_ambient_thread_local() {
3316        let manager = Arc::new(SegmentManager::new("accounts"));
3317        // (xmin, xmax) — mix of committed, in-progress and future writers.
3318        let versions = [(0, 0), (3, 0), (11, 0), (4, 0), (3, 7), (3, 11), (3, 4)];
3319        for (index, (xmin, xmax)) in versions.iter().enumerate() {
3320            let row_id = index as u64 + 1;
3321            let mut entity = UnifiedEntity::table_row(
3322                EntityId::new(row_id),
3323                "accounts",
3324                row_id,
3325                vec![Value::Integer(row_id as i64)],
3326            );
3327            entity.set_xmin(*xmin);
3328            entity.set_xmax(*xmax);
3329            manager.insert(entity).unwrap();
3330        }
3331
3332        let explicit = SnapshotContext {
3333            snapshot: Snapshot {
3334                xid: 10,
3335                in_progress: HashSet::from([4]),
3336            },
3337            manager: Arc::new(SnapshotManager::new()),
3338            own_xids: HashSet::new(),
3339            requires_index_fallback: false,
3340            serializable_reader: None,
3341        };
3342        // A decoy statement frame that would hide almost everything if the
3343        // scan ever consulted ambient state instead of its argument.
3344        let decoy = SnapshotContext {
3345            snapshot: Snapshot {
3346                xid: 1,
3347                in_progress: HashSet::new(),
3348            },
3349            manager: Arc::new(SnapshotManager::new()),
3350            own_xids: HashSet::new(),
3351            requires_index_fallback: false,
3352            serializable_reader: None,
3353        };
3354
3355        let row_ids = |entities: Vec<UnifiedEntity>| -> HashSet<u64> {
3356            entities
3357                .into_iter()
3358                .filter_map(|entity| match entity.kind {
3359                    EntityKind::TableRow { row_id, .. } => Some(row_id),
3360                    _ => None,
3361                })
3362                .collect()
3363        };
3364
3365        let guard = crate::runtime::execution_context::CurrentSnapshotGuard::install(decoy);
3366        let on_installing_thread = row_ids(manager.scan(Some(&explicit), |_| true));
3367        drop(guard);
3368
3369        let spawned_manager = Arc::clone(&manager);
3370        let spawned_snapshot = explicit.clone();
3371        let on_fresh_thread = std::thread::spawn(move || {
3372            row_ids(spawned_manager.scan(Some(&spawned_snapshot), |_| true))
3373        })
3374        .join()
3375        .unwrap();
3376
3377        assert_eq!(
3378            on_installing_thread, on_fresh_thread,
3379            "an explicit snapshot must produce the same visible set on any thread"
3380        );
3381        assert_eq!(on_installing_thread, HashSet::from([1, 2, 6, 7]));
3382
3383        // The raw iterators are the maintenance contract: no visibility
3384        // filtering at all, on any thread, regardless of installed frames.
3385        let raw_manager = Arc::clone(&manager);
3386        let raw_count = std::thread::spawn(move || raw_manager.query_all(|_| true).len())
3387            .join()
3388            .unwrap();
3389        assert_eq!(
3390            raw_count,
3391            versions.len(),
3392            "raw iteration must expose every physical version"
3393        );
3394    }
3395
3396    #[test]
3397    fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3398        let manager = SegmentManager::new("test_collection");
3399
3400        let entity = UnifiedEntity::table_row(
3401            manager.next_entity_id(),
3402            "users",
3403            1,
3404            vec![Value::text("Alice".to_string())],
3405        );
3406
3407        let id = manager.insert(entity).unwrap();
3408
3409        assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3410        assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
3411        assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
3412
3413        assert!(manager.delete(id).unwrap());
3414        assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3415    }
3416
3417    #[test]
3418    fn zoned_scan_reports_pruned_segments_and_preserves_results() {
3419        let config = ManagerConfig {
3420            segment_config: SegmentConfig {
3421                max_entities: 2,
3422                ..Default::default()
3423            },
3424            ..Default::default()
3425        };
3426        let manager = SegmentManager::with_config("events", config);
3427
3428        for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
3429            let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
3430            let entity = UnifiedEntity::new(
3431                manager.next_entity_id(),
3432                EntityKind::TableRow {
3433                    table: "events".into(),
3434                    row_id: (idx + 1) as u64,
3435                },
3436                EntityData::Row(row),
3437            );
3438            manager.insert(entity).unwrap();
3439        }
3440
3441        let full = manager.query_all(|entity| match &entity.data {
3442            EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
3443            _ => false,
3444        });
3445        let probe = Value::Integer(100);
3446        let (pruned, stats) =
3447            manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
3448                match &entity.data {
3449                    EntityData::Row(row) => {
3450                        matches!(row.get_field("ts"), Some(Value::Integer(100)))
3451                    }
3452                    _ => false,
3453                }
3454            });
3455
3456        assert_eq!(pruned.len(), full.len());
3457        assert_eq!(pruned[0].id, full[0].id);
3458        assert_eq!(stats.segments_total, 3);
3459        assert_eq!(stats.segments_scanned, 2);
3460        assert_eq!(stats.segments_pruned, 1);
3461    }
3462
3463    #[test]
3464    fn test_manager_auto_seal() {
3465        let config = ManagerConfig {
3466            segment_config: SegmentConfig {
3467                max_entities: 2,
3468                ..Default::default()
3469            },
3470            ..Default::default()
3471        };
3472
3473        let manager = SegmentManager::with_config("test", config);
3474
3475        // Insert first entity
3476        manager
3477            .insert(UnifiedEntity::vector(
3478                manager.next_entity_id(),
3479                "v",
3480                vec![0.1],
3481            ))
3482            .unwrap();
3483
3484        // Insert second entity (triggers seal check)
3485        manager
3486            .insert(UnifiedEntity::vector(
3487                manager.next_entity_id(),
3488                "v",
3489                vec![0.2],
3490            ))
3491            .unwrap();
3492
3493        // Insert third entity (should trigger auto-seal of first segment)
3494        manager
3495            .insert(UnifiedEntity::vector(
3496                manager.next_entity_id(),
3497                "v",
3498                vec![0.3],
3499            ))
3500            .unwrap();
3501
3502        let stats = manager.stats();
3503        assert_eq!(stats.total_entities, 3);
3504    }
3505
3506    #[test]
3507    fn test_manager_delete() {
3508        let manager = SegmentManager::new("test");
3509
3510        let id = manager
3511            .insert(UnifiedEntity::vector(
3512                manager.next_entity_id(),
3513                "v",
3514                vec![0.1],
3515            ))
3516            .unwrap();
3517
3518        assert!(manager.get(id).is_some());
3519        assert!(manager.delete(id).unwrap());
3520        assert!(manager.get(id).is_none());
3521    }
3522
3523    #[test]
3524    fn test_manager_metadata() {
3525        let manager = SegmentManager::new("test");
3526
3527        let id = manager
3528            .insert(UnifiedEntity::table_row(
3529                manager.next_entity_id(),
3530                "hosts",
3531                1,
3532                vec![Value::text("192.168.1.1".to_string())],
3533            ))
3534            .unwrap();
3535
3536        let mut meta = Metadata::new();
3537        meta.set(
3538            "os",
3539            super::super::metadata::MetadataValue::String("linux".to_string()),
3540        );
3541
3542        manager.set_metadata(id, meta).unwrap();
3543
3544        let retrieved = manager.get_metadata(id).unwrap();
3545        assert!(retrieved.has("os"));
3546    }
3547
3548    #[test]
3549    fn test_manager_query_by_kind() {
3550        let manager = SegmentManager::new("test");
3551
3552        manager
3553            .insert(UnifiedEntity::table_row(
3554                manager.next_entity_id(),
3555                "hosts",
3556                1,
3557                vec![],
3558            ))
3559            .unwrap();
3560
3561        manager
3562            .insert(UnifiedEntity::vector(
3563                manager.next_entity_id(),
3564                "embeddings",
3565                vec![0.1],
3566            ))
3567            .unwrap();
3568
3569        manager
3570            .insert(UnifiedEntity::table_row(
3571                manager.next_entity_id(),
3572                "hosts",
3573                2,
3574                vec![],
3575            ))
3576            .unwrap();
3577
3578        let rows = manager.get_by_kind("table");
3579        assert_eq!(rows.len(), 2);
3580
3581        let vectors = manager.get_by_kind("vector");
3582        assert_eq!(vectors.len(), 1);
3583    }
3584
3585    // ========================================================================
3586    // Consolidation — ADR 0073 §5
3587    // ========================================================================
3588
3589    /// A manager whose sealed set never trips the count trigger, so tests can
3590    /// exercise the tombstone/fragmentation triggers in isolation.
3591    fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
3592        SegmentManager::with_config(
3593            "test",
3594            ManagerConfig {
3595                max_sealed_segments: 1_000,
3596                consolidation_entities_per_tick: entities_per_tick,
3597                ..Default::default()
3598            },
3599        )
3600    }
3601
3602    /// Bulk-load `count` rows into a fresh segment and seal it. Bulk-inserted
3603    /// entities land in flat storage, where a tombstone keeps its payload
3604    /// resident — which is the memory consolidation reclaims.
3605    fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
3606        let entities: Vec<UnifiedEntity> = (0..count)
3607            .map(|i| {
3608                UnifiedEntity::table_row(
3609                    manager.next_entity_id(),
3610                    "users",
3611                    i as u64,
3612                    vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
3613                )
3614            })
3615            .collect();
3616        let ids = manager.bulk_insert(entities).expect("bulk insert");
3617        manager.force_seal().expect("seal");
3618        ids
3619    }
3620
3621    fn live_ids(manager: &SegmentManager) -> Vec<u64> {
3622        let mut ids: Vec<u64> = manager
3623            .query_all(|_| true)
3624            .into_iter()
3625            .map(|entity| entity.id.raw())
3626            .collect();
3627        ids.sort_unstable();
3628        ids
3629    }
3630
3631    fn drain_maintenance(manager: &SegmentManager) -> usize {
3632        let mut ticks = 0;
3633        while manager.consolidation.read().is_some() || ticks == 0 {
3634            manager.run_maintenance().expect("maintenance");
3635            ticks += 1;
3636            assert!(ticks < 1_000, "consolidation failed to converge");
3637        }
3638        ticks
3639    }
3640
3641    #[test]
3642    fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
3643        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3644        let ids = manager
3645            .bulk_insert(
3646                (1..=1024)
3647                    .map(|id| {
3648                        UnifiedEntity::graph_edge(
3649                            EntityId::new(id),
3650                            "step",
3651                            "2000",
3652                            "3000",
3653                            1.0,
3654                            HashMap::new(),
3655                        )
3656                    })
3657                    .collect(),
3658            )
3659            .expect("edges");
3660        manager.force_seal().expect("seal");
3661        for id in ids.iter().take(300) {
3662            manager.delete(*id).expect("fragment source");
3663        }
3664        let source = Arc::downgrade(&manager.sealed.read()[0]);
3665        let mut seen = Vec::new();
3666        let mut changed = false;
3667        let mut metadata_bytes = 0;
3668        assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
3669            || true,
3670            |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
3671            |batch| {
3672                assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3673                assert!(manager.growing.try_write().is_some(), "no growing topology guard");
3674                assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
3675                assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
3676                if !changed {
3677                    changed = true;
3678                    drain_maintenance(&manager);
3679                    assert!(manager.stats().consolidation.runs_completed > 0);
3680                    assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
3681                    let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
3682                    if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
3683                    manager.update(updated).expect("update authoritative merged copy");
3684                }
3685                for id in batch {
3686                    let entity = manager.get(*id).expect("hydrate current manager");
3687                    if id.raw() == 1024 {
3688                        assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
3689                    }
3690                    seen.push(id.raw());
3691                }
3692                Ok(true)
3693            }).expect("cursor"));
3694        assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
3695        assert!(
3696            source.upgrade().is_none(),
3697            "finished cursor releases retired source"
3698        );
3699        assert!(
3700            metadata_bytes < 4096,
3701            "fixed ID buffer plus source handles: {metadata_bytes}"
3702        );
3703    }
3704
3705    #[test]
3706    fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
3707        let manager = SegmentManager::new("graph");
3708        manager
3709            .bulk_insert(
3710                (1..=1024)
3711                    .map(|id| {
3712                        UnifiedEntity::graph_edge(
3713                            EntityId::new(id),
3714                            "step",
3715                            "2000",
3716                            "3000",
3717                            1.0,
3718                            HashMap::new(),
3719                        )
3720                    })
3721                    .collect(),
3722            )
3723            .expect("edges");
3724        let mut delivered = 0;
3725        let denied = manager.visit_graph_index_batches(
3726            GraphEntityKind::Edge,
3727            &[EntityId::new(2000)],
3728            || true,
3729            |_| Err("out of memory"),
3730            |ids| {
3731                delivered += ids.len();
3732                Ok(true)
3733            },
3734        );
3735        assert_eq!(denied, Err("out of memory"));
3736        assert_eq!(delivered, 0);
3737        let mut work = 0;
3738        assert!(!manager
3739            .visit_graph_index_batches(
3740                GraphEntityKind::Edge,
3741                &[EntityId::new(2000)],
3742                || {
3743                    work += 1;
3744                    work < 100
3745                },
3746                |_| Ok::<_, ()>(()),
3747                |ids| {
3748                    delivered += ids.len();
3749                    Ok(true)
3750                }
3751            )
3752            .expect("cancelled cursor"));
3753        assert_eq!(
3754            delivered, 0,
3755            "cancelled partial batch cannot reach consumer"
3756        );
3757        assert!(!manager
3758            .visit_graph_index_batches(
3759                GraphEntityKind::Edge,
3760                &[EntityId::new(2000)],
3761                || true,
3762                |_| Ok::<_, ()>(()),
3763                |ids| {
3764                    delivered += ids.len();
3765                    Ok(false)
3766                }
3767            )
3768            .expect("consumer stop"));
3769        assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
3770    }
3771
3772    #[test]
3773    fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
3774        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3775        let ids = seal_bulk_segment(&manager, 100);
3776        for id in ids.iter().take(40) {
3777            manager.delete(*id).expect("delete before scan");
3778        }
3779        let source_bytes = manager.resident_bytes();
3780        let source = Arc::downgrade(&manager.sealed.read()[0]);
3781        std::thread::scope(|scope| {
3782            let (ready, waiting) = std::sync::mpsc::channel();
3783            let (release_first, first_wait) = std::sync::mpsc::channel();
3784            let (release_last, last_wait) = std::sync::mpsc::channel();
3785            let reader = |resume: std::sync::mpsc::Receiver<()>,
3786                          ready: std::sync::mpsc::Sender<()>| {
3787                assert!(manager.scan_batches(
3788                    None,
3789                    || true,
3790                    |_| {
3791                        ready.send(()).expect("signal captured cursor");
3792                        let _ = resume.recv();
3793                        true
3794                    }
3795                ));
3796            };
3797            let ready_first = ready.clone();
3798            let first = scope.spawn(move || reader(first_wait, ready_first));
3799            let last = scope.spawn(move || reader(last_wait, ready));
3800            for _ in 0..2 {
3801                waiting
3802                    .recv_timeout(std::time::Duration::from_secs(10))
3803                    .expect("reader ready");
3804            }
3805            drain_maintenance(&manager);
3806            assert!(
3807                source.upgrade().is_some(),
3808                "both cursors pin the retired source"
3809            );
3810            let active_bytes = manager.sealed.read()[0].read().resident_bytes();
3811            let held = manager.resident_bytes();
3812            assert!(
3813                held >= active_bytes + source_bytes,
3814                "retired source remains resident"
3815            );
3816            release_first.send(()).expect("release first reader");
3817            first.join().expect("first reader");
3818            assert_eq!(
3819                manager.resident_bytes(),
3820                held,
3821                "one reader still pins the source"
3822            );
3823            release_last.send(()).expect("release last reader");
3824            last.join().expect("last reader");
3825            assert!(
3826                source.upgrade().is_none(),
3827                "accounting must not retain payloads"
3828            );
3829            assert_eq!(held - manager.resident_bytes(), source_bytes);
3830            assert_eq!(
3831                manager.stats().total_memory_bytes as u64,
3832                manager.resident_bytes()
3833            );
3834        });
3835    }
3836
3837    #[test]
3838    fn retained_memory_gates_runtime_growth_until_the_cursor_is_cancelled() {
3839        use crate::storage::memory_pools::MemoryPool;
3840        use crate::{RedDBOptions, RedDBRuntime};
3841
3842        let budget = 128 * 1024;
3843        let runtime =
3844            RedDBRuntime::with_options(RedDBOptions::in_memory().with_memory_budget(budget))
3845                .expect("runtime");
3846        runtime
3847            .db()
3848            .store()
3849            .create_collection("test")
3850            .expect("collection");
3851        let manager = runtime
3852            .db()
3853            .store()
3854            .get_collection("test")
3855            .expect("manager");
3856        let ids = seal_bulk_segment(&manager, 100);
3857        for id in ids.iter().take(40) {
3858            manager.delete(*id).expect("delete before scan");
3859        }
3860        let source_bytes = manager.resident_bytes();
3861        let mut growth = 0;
3862        let mut held_used = 0;
3863        assert!(!manager.scan_batches(
3864            None,
3865            || true,
3866            |_| {
3867                drain_maintenance(&manager);
3868                runtime.refresh_memory_accounting();
3869                // Other collections (including the runtime catalog) share this
3870                // arena, so derive headroom from the complete runtime sample.
3871                held_used = runtime.memory_accounting().total_used_bytes();
3872                growth = budget
3873                    .checked_sub(held_used)
3874                    .expect("fixture fits while the cursor is open")
3875                    + source_bytes / 2;
3876                assert!(growth > 0);
3877                assert!(
3878                    runtime
3879                        .admit_non_evictable_growth(
3880                            MemoryPool::SegmentArena,
3881                            "growth with pinned source",
3882                            growth,
3883                        )
3884                        .is_err(),
3885                    "retained source must participate in runtime admission"
3886                );
3887                false
3888            }
3889        ));
3890        runtime.refresh_memory_accounting();
3891        assert_eq!(
3892            held_used - runtime.memory_accounting().total_used_bytes(),
3893            source_bytes
3894        );
3895        let _reservation = runtime
3896            .admit_non_evictable_growth(
3897                MemoryPool::SegmentArena,
3898                "growth after cursor cancellation",
3899                growth,
3900            )
3901            .expect("the same growth fits once the cursor releases its source");
3902    }
3903
3904    #[test]
3905    fn retained_memory_includes_an_inflight_consolidation_copy() {
3906        let manager = consolidating_manager(1);
3907        let ids = seal_bulk_segment(&manager, 100);
3908        for id in ids.iter().take(40) {
3909            manager.delete(*id).expect("delete");
3910        }
3911        let before = manager.resident_bytes();
3912        assert!(manager.pressure_consolidation_tick());
3913        let copied = manager
3914            .consolidation
3915            .read()
3916            .as_ref()
3917            .expect("paced merge")
3918            .merged
3919            .resident_bytes();
3920        assert!(copied > 0);
3921        assert!(
3922            manager.resident_bytes() >= before + copied,
3923            "unpublished copy consumes memory"
3924        );
3925        drain_maintenance(&manager);
3926        assert!(
3927            manager.resident_bytes() < before,
3928            "completed merge releases the old storage"
3929        );
3930    }
3931
3932    #[test]
3933    fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
3934        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
3935        let ids = seal_bulk_segment(&manager, 100);
3936
3937        let bytes_before_delete = manager.resident_bytes();
3938
3939        // 40% tombstones — past CONSOLIDATION_TOMBSTONE_RATIO.
3940        for id in ids.iter().take(40) {
3941            assert!(manager.delete(*id).expect("delete"));
3942        }
3943
3944        // Tombstones cost memory: the payload stays resident in flat storage
3945        // and the tombstone set grows.
3946        let bytes_with_tombstones = manager.resident_bytes();
3947        assert!(bytes_with_tombstones > bytes_before_delete);
3948
3949        let survivors = live_ids(&manager);
3950        assert_eq!(survivors.len(), 60);
3951
3952        drain_maintenance(&manager);
3953
3954        let stats = manager.stats();
3955        assert_eq!(stats.consolidation.runs_started, 1);
3956        assert_eq!(stats.consolidation.runs_completed, 1);
3957        assert_eq!(stats.consolidation.segments_merged, 1);
3958        assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
3959        assert!(stats.consolidation.bytes_reclaimed > 0);
3960
3961        // The reclaimed memory is at least the tombstoned entities' share.
3962        let bytes_after = manager.resident_bytes();
3963        assert!(
3964            bytes_after <= bytes_before_delete,
3965            "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
3966        );
3967        assert_eq!(
3968            bytes_with_tombstones - bytes_after,
3969            stats.consolidation.bytes_reclaimed
3970        );
3971        assert_eq!(stats.total_memory_bytes, bytes_after as usize);
3972
3973        assert_eq!(live_ids(&manager), survivors);
3974        assert_eq!(manager.sealed.read().len(), 1);
3975        assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
3976    }
3977
3978    #[test]
3979    fn query_results_are_identical_before_during_and_after_consolidation() {
3980        let manager = consolidating_manager(3);
3981        let ids = seal_bulk_segment(&manager, 20);
3982        seal_bulk_segment(&manager, 20);
3983        for id in ids.iter().take(10) {
3984            assert!(manager.delete(*id).expect("delete"));
3985        }
3986
3987        let before = live_ids(&manager);
3988        let survivor = *ids.last().expect("at least one row");
3989        let tombstoned = ids[0];
3990
3991        // Every mid-tick observation matches the pre-consolidation snapshot:
3992        // point reads, full scans, and the kind index (rebuilt by the merge).
3993        let mut mid_tick_observations = 0;
3994        while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
3995            manager.run_maintenance().expect("maintenance");
3996            assert_eq!(live_ids(&manager), before);
3997            assert!(manager.get(survivor).is_some());
3998            assert!(manager.get(tombstoned).is_none());
3999            assert_eq!(manager.get_by_kind("table").len(), before.len());
4000            mid_tick_observations += 1;
4001            assert!(mid_tick_observations < 1_000);
4002        }
4003
4004        assert!(
4005            mid_tick_observations > 2,
4006            "expected the run to span several ticks, saw {mid_tick_observations}"
4007        );
4008        assert_eq!(live_ids(&manager), before);
4009        assert!(manager.get(survivor).is_some());
4010        assert!(manager.get(tombstoned).is_none());
4011        assert_eq!(manager.get_by_kind("table").len(), before.len());
4012    }
4013
4014    #[test]
4015    fn get_by_kind_reaches_sealed_segments() {
4016        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4017        let ids = seal_bulk_segment(&manager, 5);
4018        assert!(manager.delete(ids[0]).expect("delete"));
4019
4020        // The sealed list is walked through the segment read guard; the
4021        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` cannot hand out
4022        // references across the lock and yields nothing.
4023        assert_eq!(manager.get_by_kind("table").len(), 4);
4024        assert_eq!(manager.get_by_kind("vector").len(), 0);
4025    }
4026
4027    #[test]
4028    fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4029        const PER_TICK: usize = 4;
4030        let manager = consolidating_manager(PER_TICK);
4031        let ids = seal_bulk_segment(&manager, 60);
4032        for id in ids.iter().take(30) {
4033            assert!(manager.delete(*id).expect("delete"));
4034        }
4035
4036        // First tick opens the run and copies exactly one bound's worth.
4037        manager.run_maintenance().expect("maintenance");
4038        assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4039
4040        // Each subsequent tick advances by no more than the bound, and the run
4041        // is still in flight — it never completes in one unbounded pass.
4042        let mut previous = PER_TICK as u64;
4043        let mut ticks = 1;
4044        while let Some(copied) = manager.consolidation_progress() {
4045            assert!(
4046                copied - previous <= PER_TICK as u64,
4047                "tick copied {} entities, bound is {PER_TICK}",
4048                copied - previous
4049            );
4050            previous = copied;
4051            manager.run_maintenance().expect("maintenance");
4052            ticks += 1;
4053            assert!(ticks < 100);
4054        }
4055
4056        // 30 survivors at 4 per tick: the run cannot have finished before the
4057        // eighth tick.
4058        assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4059        assert_eq!(manager.stats().consolidation.runs_completed, 1);
4060        assert_eq!(live_ids(&manager).len(), 30);
4061    }
4062
4063    #[test]
4064    fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4065        let manager = SegmentManager::with_config(
4066            "test",
4067            ManagerConfig {
4068                max_sealed_segments: 3,
4069                ..Default::default()
4070            },
4071        );
4072        for _ in 0..4 {
4073            seal_bulk_segment(&manager, 5);
4074        }
4075        assert_eq!(manager.sealed.read().len(), 4);
4076
4077        let before = live_ids(&manager);
4078        drain_maintenance(&manager);
4079
4080        // The knob finally does what it claimed: the sealed set collapses even
4081        // though not a single row was deleted.
4082        assert_eq!(manager.sealed.read().len(), 1);
4083        assert_eq!(manager.stats().consolidation.segments_merged, 4);
4084        assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4085        assert_eq!(live_ids(&manager), before);
4086    }
4087
4088    #[test]
4089    fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4090        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4091        seal_bulk_segment(&manager, 10);
4092        seal_bulk_segment(&manager, 10);
4093
4094        for _ in 0..5 {
4095            manager.run_maintenance().expect("maintenance");
4096        }
4097
4098        assert_eq!(manager.stats().consolidation.runs_started, 0);
4099        assert_eq!(manager.sealed.read().len(), 2);
4100    }
4101
4102    #[test]
4103    fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4104        use std::sync::atomic::AtomicBool;
4105
4106        let manager = Arc::new(consolidating_manager(2));
4107        let ids = seal_bulk_segment(&manager, 40);
4108        seal_bulk_segment(&manager, 40);
4109        for id in ids.iter().take(20) {
4110            assert!(manager.delete(*id).expect("delete"));
4111        }
4112
4113        let expected = live_ids(&manager);
4114        let stop = Arc::new(AtomicBool::new(false));
4115
4116        let readers: Vec<_> = (0..4)
4117            .map(|_| {
4118                let manager = Arc::clone(&manager);
4119                let stop = Arc::clone(&stop);
4120                let expected = expected.clone();
4121                std::thread::spawn(move || {
4122                    let mut scans = 0u64;
4123                    // Do-while: every reader scans at least once even when the
4124                    // drain finishes before this thread is first scheduled.
4125                    loop {
4126                        let mut seen: Vec<u64> = manager
4127                            .query_all(|_| true)
4128                            .into_iter()
4129                            .map(|entity| entity.id.raw())
4130                            .collect();
4131                        seen.sort_unstable();
4132                        // Sorting is not enough: a duplicated row would keep
4133                        // the set equal but change the length.
4134                        assert_eq!(seen, expected, "reader saw a torn sealed set");
4135                        scans += 1;
4136                        if stop.load(Ordering::Relaxed) {
4137                            break;
4138                        }
4139                    }
4140                    scans
4141                })
4142            })
4143            .collect();
4144
4145        drain_maintenance(&manager);
4146        stop.store(true, Ordering::Relaxed);
4147
4148        for reader in readers {
4149            let scans = reader.join().expect("reader thread");
4150            assert!(scans > 0, "reader never got to scan");
4151        }
4152
4153        assert_eq!(manager.stats().consolidation.runs_completed, 1);
4154        assert_eq!(live_ids(&manager), expected);
4155    }
4156
4157    #[test]
4158    fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4159        let manager = consolidating_manager(4);
4160        let ids = seal_bulk_segment(&manager, 20);
4161        for id in ids.iter().take(6) {
4162            assert!(manager.delete(*id).expect("delete"));
4163        }
4164
4165        // Copy the first bound's worth, then delete a row we just copied.
4166        manager.run_maintenance().expect("maintenance");
4167        assert_eq!(manager.consolidation_progress(), Some(4));
4168        let doomed = ids[6];
4169        assert!(manager.delete(doomed).expect("delete"));
4170
4171        let expected = live_ids(&manager);
4172        drain_maintenance(&manager);
4173
4174        assert!(manager.get(doomed).is_none(), "deleted row came back");
4175        assert_eq!(live_ids(&manager), expected);
4176    }
4177
4178    #[test]
4179    fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
4180        let manager = consolidating_manager(3);
4181        let ids = seal_bulk_segment(&manager, 30);
4182        for id in ids.iter().take(12) {
4183            assert!(manager.delete(*id).expect("delete"));
4184        }
4185
4186        let before = live_ids(&manager);
4187        let sealed_before: Vec<SegmentId> = manager
4188            .sealed
4189            .read()
4190            .iter()
4191            .map(|segment| segment.read().id())
4192            .collect();
4193
4194        manager.run_maintenance().expect("maintenance");
4195        manager.run_maintenance().expect("maintenance");
4196        assert!(manager.consolidation.read().is_some());
4197
4198        // A crash between ticks drops the half-built merged segment. Sealed
4199        // segments are the durable truth, so nothing was lost — and the run is
4200        // simply started over.
4201        *manager.consolidation.write() = None;
4202
4203        assert_eq!(live_ids(&manager), before);
4204        let sealed_after: Vec<SegmentId> = manager
4205            .sealed
4206            .read()
4207            .iter()
4208            .map(|segment| segment.read().id())
4209            .collect();
4210        assert_eq!(sealed_after, sealed_before);
4211        assert_eq!(manager.stats().consolidation.runs_completed, 0);
4212
4213        drain_maintenance(&manager);
4214        assert_eq!(live_ids(&manager), before);
4215        assert_eq!(manager.stats().consolidation.runs_completed, 1);
4216    }
4217
4218    #[test]
4219    fn consolidation_preserves_entity_payload_metadata_and_sequence() {
4220        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4221
4222        // Metadata must be attached before the seal: `set_metadata` on a sealed
4223        // row rewrites it into the growing segment, which would take it out of
4224        // the segment under test.
4225        let entities: Vec<UnifiedEntity> = (0..10)
4226            .map(|i| {
4227                UnifiedEntity::table_row(
4228                    manager.next_entity_id(),
4229                    "users",
4230                    i as u64,
4231                    vec![Value::Integer(i as i64)],
4232                )
4233            })
4234            .collect();
4235        let ids = manager.bulk_insert(entities).expect("bulk insert");
4236
4237        let mut meta = Metadata::new();
4238        meta.set(
4239            "os",
4240            super::super::metadata::MetadataValue::String("linux".to_string()),
4241        );
4242        manager.set_metadata(ids[9], meta).expect("set metadata");
4243        manager.force_seal().expect("seal");
4244
4245        assert!(manager.delete(ids[0]).expect("delete"));
4246        assert!(manager.delete(ids[1]).expect("delete"));
4247        assert!(manager.delete(ids[2]).expect("delete"));
4248
4249        let before: Vec<UnifiedEntity> = {
4250            let mut entities = manager.query_all(|_| true);
4251            entities.sort_by_key(|entity| entity.id.raw());
4252            entities
4253        };
4254
4255        drain_maintenance(&manager);
4256
4257        let after: Vec<UnifiedEntity> = {
4258            let mut entities = manager.query_all(|_| true);
4259            entities.sort_by_key(|entity| entity.id.raw());
4260            entities
4261        };
4262
4263        let columns = |entity: &UnifiedEntity| match &entity.data {
4264            super::super::entity::EntityData::Row(row) => row.columns.clone(),
4265            _ => Vec::new(),
4266        };
4267
4268        assert_eq!(before.len(), after.len());
4269        for (old, new) in before.iter().zip(after.iter()) {
4270            assert_eq!(old.id, new.id);
4271            assert_eq!(columns(old), columns(new));
4272            assert_eq!(
4273                old.sequence_id, new.sequence_id,
4274                "merge must not re-issue sequence ids"
4275            );
4276        }
4277        assert!(manager
4278            .get_metadata(ids[9])
4279            .expect("metadata survives the merge")
4280            .has("os"));
4281    }
4282
4283    #[test]
4284    #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
4285    fn test_lifecycle_events() {
4286        let manager = SegmentManager::new("test");
4287
4288        manager
4289            .insert(UnifiedEntity::vector(
4290                manager.next_entity_id(),
4291                "v",
4292                vec![0.1],
4293            ))
4294            .unwrap();
4295
4296        let events = manager.drain_events();
4297
4298        // Should have: SegmentCreated, EntityInserted
4299        assert!(events
4300            .iter()
4301            .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
4302        assert!(events
4303            .iter()
4304            .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
4305    }
4306}