Skip to main content

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