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