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    /// Hydrate one bounded graph batch at a time. Payloads and policy callbacks
2272    /// leave storage locks; pinned cursors survive sealing and consolidation.
2273    /// Work is charged for pruned sources and every inspected physical slot.
2274    #[doc(hidden)]
2275    pub fn scan_graph_batches(
2276        &self,
2277        snapshot: Option<&SnapshotContext>,
2278        kind: GraphEntityKind,
2279        mut before_work: impl FnMut() -> bool,
2280        mut consume: impl FnMut(Vec<UnifiedEntity>) -> bool,
2281    ) -> bool {
2282        let segments = {
2283            let growing = self.growing.read();
2284            let sealed = self.sealed.read();
2285            let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
2286            for segment in growing.iter().chain(sealed.iter()) {
2287                if !before_work() {
2288                    return false;
2289                }
2290                let view = segment.read();
2291                if view.may_contain_graph_kind(kind) {
2292                    segments.push((Arc::clone(segment), view.scan_cursor()));
2293                }
2294            }
2295            segments
2296        };
2297        let mut ids = [EntityId::new(0); super::segment::SCAN_BATCH_SIZE];
2298        for (segment, mut cursor) in segments {
2299            while !cursor.finished() {
2300                let mut count = 0;
2301                {
2302                    let view = segment.read();
2303                    if !view.scan_batch(&mut cursor, &mut before_work, &mut |id| {
2304                        ids[count] = id;
2305                        count += 1;
2306                    }) {
2307                        return false;
2308                    }
2309                }
2310                if !before_work() {
2311                    return false;
2312                }
2313                // Hydration groups lookups by their current segment. Restore the
2314                // cursor's order when an update moved a candidate between sources.
2315                let mut batch = vec![None; count];
2316                self.for_each_id(&ids[..count], |position, entity| {
2317                    if kind.matches(&entity.kind) && Self::scan_entity_visible(snapshot, entity) {
2318                        batch[position] = Some(entity.clone());
2319                    }
2320                });
2321                if !before_work() || !consume(batch.into_iter().flatten().collect()) {
2322                    return false;
2323                }
2324            }
2325        }
2326        true
2327    }
2328
2329    /// Visit visible candidates in segments that may contain the requested kind.
2330    /// Non-graph items in those segments still reach the callback so work budgets
2331    /// charge every inspected visible candidate. RLS must run outside this callback.
2332    /// `before_segment` runs even for pruned segments; false stops the entire scan.
2333    #[doc(hidden)]
2334    pub fn scan_graph_candidates_for_each<F>(
2335        &self,
2336        snapshot: Option<&SnapshotContext>,
2337        kind: GraphEntityKind,
2338        before_segment: impl FnMut() -> bool,
2339        mut callback: F,
2340    ) where
2341        F: FnMut(&UnifiedEntity) -> bool,
2342    {
2343        self.for_each_entity_in_segments(
2344            |segment| segment.may_contain_graph_kind(kind),
2345            before_segment,
2346            |entity| !Self::scan_entity_visible(snapshot, entity) || callback(entity),
2347        );
2348    }
2349
2350    /// Visit every entity visible under an explicit MVCC snapshot.
2351    ///
2352    /// Returning `false` from `callback` stops the scan early. Hidden entities
2353    /// never reach the callback and therefore cannot stop the scan.
2354    pub fn scan_for_each<F>(&self, snapshot: Option<&SnapshotContext>, mut callback: F)
2355    where
2356        F: FnMut(&UnifiedEntity) -> bool,
2357    {
2358        self.for_each_entity(|entity| {
2359            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2360        });
2361    }
2362
2363    #[doc(hidden)]
2364    pub fn physical_scan_cursor(&self) -> PhysicalScanCursor {
2365        let growing = self.growing.read();
2366        let sealed = self.sealed.read();
2367        PhysicalScanCursor {
2368            sources: growing
2369                .iter()
2370                .chain(sealed.iter())
2371                .map(|segment| (Arc::downgrade(segment), segment.read().scan_cursor()))
2372                .collect(),
2373            source: 0,
2374        }
2375    }
2376
2377    /// At most one small ID batch; every inspected slot or retired source
2378    /// consumes work, including deleted entries that produce no candidate.
2379    #[doc(hidden)]
2380    pub fn physical_scan_batch(
2381        &self,
2382        cursor: &mut PhysicalScanCursor,
2383        work_left: &mut usize,
2384    ) -> Vec<EntityId> {
2385        let mut ids = Vec::with_capacity(super::segment::SCAN_BATCH_SIZE.min(*work_left));
2386        while !cursor.finished() && *work_left > 0 {
2387            let (source, position) = &mut cursor.sources[cursor.source];
2388            if position.finished() {
2389                *work_left -= 1;
2390                cursor.source += 1;
2391                continue;
2392            }
2393            let Some(segment) = source.upgrade() else {
2394                *work_left -= 1;
2395                cursor.source += 1;
2396                continue;
2397            };
2398            segment.read().scan_batch(
2399                position,
2400                &mut || {
2401                    if *work_left == 0 {
2402                        return false;
2403                    }
2404                    *work_left -= 1;
2405                    true
2406                },
2407                &mut |id| ids.push(id),
2408            );
2409            if position.finished() {
2410                cursor.source += 1;
2411            }
2412            if !ids.is_empty() {
2413                break;
2414            }
2415        }
2416        ids
2417    }
2418
2419    /// Capture segment membership and append boundaries, then hydrate bounded
2420    /// batches. The consumer runs outside topology and segment locks, so scoring
2421    /// and RLS may re-enter storage and writers can progress between batches.
2422    /// Retired sources pin positions only; hydration uses current segments so
2423    /// mutations after consolidation are visible to the snapshot predicate.
2424    #[doc(hidden)]
2425    pub fn scan_batches(
2426        &self,
2427        snapshot: Option<&SnapshotContext>,
2428        mut before_work: impl FnMut() -> bool,
2429        mut consume: impl FnMut(&[UnifiedEntity]) -> bool,
2430    ) -> bool {
2431        let segments = {
2432            let growing = self.growing.read();
2433            let sealed = self.sealed.read();
2434            let mut segments = Vec::with_capacity(sealed.len() + usize::from(growing.is_some()));
2435            for segment in growing.iter().chain(sealed.iter()) {
2436                if !before_work() {
2437                    return false;
2438                }
2439                let cursor = segment.read().scan_cursor();
2440                segments.push((Arc::clone(segment), cursor));
2441            }
2442            segments
2443        };
2444        let mut ids = Vec::new();
2445        let mut batch = Vec::new();
2446        for (segment, mut cursor) in segments {
2447            while !cursor.finished() {
2448                ids.clear();
2449                batch.clear();
2450                let batch_size = cursor.remaining().min(super::segment::SCAN_BATCH_SIZE);
2451                ids.reserve_exact(batch_size);
2452                batch.reserve_exact(batch_size);
2453                {
2454                    let segment = segment.read();
2455                    if !segment.scan_batch(&mut cursor, &mut before_work, &mut |id| ids.push(id)) {
2456                        return false;
2457                    }
2458                }
2459                // Sources can retire between batches. Consult authoritative storage
2460                // after releasing the source guard, just as the former ID-list scan did.
2461                if !before_work() {
2462                    return false;
2463                }
2464                self.for_each_id(&ids, |_, entity| {
2465                    if Self::scan_entity_visible(snapshot, entity) {
2466                        batch.push(entity.clone());
2467                    }
2468                });
2469                if !before_work() {
2470                    return false;
2471                }
2472                if !consume(&batch) {
2473                    return false;
2474                }
2475            }
2476        }
2477        true
2478    }
2479
2480    /// Fold entities visible under an explicit MVCC snapshot in parallel.
2481    pub fn scan_fold_parallel<T, FInit, FFold, FReduce>(
2482        &self,
2483        snapshot: Option<&SnapshotContext>,
2484        init: FInit,
2485        fold: FFold,
2486        reduce: FReduce,
2487    ) -> T
2488    where
2489        T: Send,
2490        FInit: Fn() -> T + Send + Sync,
2491        FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
2492        FReduce: Fn(T, T) -> T + Send + Sync,
2493    {
2494        self.fold_entities_parallel(
2495            init,
2496            |acc, entity| {
2497                if Self::scan_entity_visible(snapshot, entity) {
2498                    fold(acc, entity)
2499                } else {
2500                    acc
2501                }
2502            },
2503            reduce,
2504        )
2505    }
2506
2507    /// Visit zone-map candidates visible under an explicit MVCC snapshot.
2508    pub fn scan_for_each_zoned_with_stats<F>(
2509        &self,
2510        snapshot: Option<&SnapshotContext>,
2511        zone_preds: &[(&str, ZoneColPred<'_>)],
2512        mut callback: F,
2513    ) -> SegmentScanStats
2514    where
2515        F: FnMut(&UnifiedEntity) -> bool,
2516    {
2517        self.for_each_entity_zoned_with_stats(zone_preds, |entity| {
2518            !Self::scan_entity_visible(snapshot, entity) || callback(entity)
2519        })
2520    }
2521
2522    /// Collect zone-map candidates visible under an explicit MVCC snapshot.
2523    pub fn scan_zoned_with_stats<F>(
2524        &self,
2525        snapshot: Option<&SnapshotContext>,
2526        zone_preds: &[(&str, ZoneColPred<'_>)],
2527        filter: F,
2528    ) -> (Vec<UnifiedEntity>, SegmentScanStats)
2529    where
2530        F: Fn(&UnifiedEntity) -> bool + Sync,
2531    {
2532        self.query_all_zoned_with_stats(zone_preds, |entity| {
2533            Self::scan_entity_visible(snapshot, entity) && filter(entity)
2534        })
2535    }
2536
2537    /// Project visible zone-map candidates while borrowing each source entity.
2538    /// Only the owned projection leaves the segment read guard. The callback
2539    /// must not mutate or re-enter the source collection.
2540    pub fn scan_project_zoned_with_stats<T, F>(
2541        &self,
2542        snapshot: Option<&SnapshotContext>,
2543        zone_preds: &[(&str, ZoneColPred<'_>)],
2544        project: F,
2545    ) -> (Vec<T>, SegmentScanStats)
2546    where
2547        T: Send,
2548        F: Fn(&UnifiedEntity) -> Option<T> + Sync,
2549    {
2550        self.project_all_zoned_with_stats(zone_preds, |entity| {
2551            if Self::scan_entity_visible(snapshot, entity) {
2552                project(entity)
2553            } else {
2554                None
2555            }
2556        })
2557    }
2558
2559    /// Query across all segments. Uses parallel scanning for sealed segments
2560    /// when more than one sealed segment exists.
2561    pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
2562    where
2563        F: Fn(&UnifiedEntity) -> bool + Sync,
2564    {
2565        let mut entities = self.query_all_in_segments(|_| true, filter);
2566        self.restore_owned_row_schemas(entities.iter_mut());
2567        entities
2568    }
2569
2570    fn query_all_in_segments<F>(
2571        &self,
2572        segment_filter: impl Fn(&GrowingSegment) -> bool + Sync,
2573        filter: F,
2574    ) -> Vec<UnifiedEntity>
2575    where
2576        F: Fn(&UnifiedEntity) -> bool + Sync,
2577    {
2578        let mut results = Vec::new();
2579
2580        // Query growing segment — try non-blocking read first (avoids stalling
2581        // behind an in-progress insert; falls back to blocking if writer is active).
2582        if let Some(growing_arc) = self.growing.read().as_ref() {
2583            let growing = if let Some(g) = growing_arc.try_read() {
2584                g
2585            } else {
2586                growing_arc.read()
2587            };
2588            if segment_filter(&growing) {
2589                results.extend(growing.iter().filter(|e| filter(e)).cloned());
2590            }
2591        }
2592
2593        // Query sealed segments — parallel when multiple exist AND multi-core
2594        let sealed = self.sealed.read();
2595        let use_parallel = sealed.len() > 1 && crate::execution::should_parallelize();
2596        if use_parallel {
2597            let filter_ref = &filter;
2598            let segment_filter_ref = &segment_filter;
2599            let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
2600                sealed
2601                    .iter()
2602                    .map(|segment| {
2603                        s.spawn(move || {
2604                            let segment = segment.read();
2605                            if segment_filter_ref(&segment) {
2606                                segment
2607                                    .iter()
2608                                    .filter(|e| filter_ref(e))
2609                                    .cloned()
2610                                    .collect::<Vec<_>>()
2611                            } else {
2612                                Vec::new()
2613                            }
2614                        })
2615                    })
2616                    .collect::<Vec<_>>()
2617                    .into_iter()
2618                    .map(|handle| handle.join().unwrap_or_default())
2619                    .collect()
2620            });
2621            for batch in segment_results {
2622                results.extend(batch);
2623            }
2624        } else {
2625            for segment in sealed.iter() {
2626                let seg = segment.read();
2627                if segment_filter(&seg) {
2628                    results.extend(seg.iter().filter(|e| filter(e)).cloned());
2629                }
2630            }
2631        }
2632
2633        results
2634    }
2635
2636    /// Probe exact segment membership for a key without materializing matching entities.
2637    ///
2638    /// Returns `false` only when every consulted segment proves absence.
2639    /// Missing segment state is conservative and returns `true`.
2640    pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
2641        if let Some(growing_arc) = self.growing.read().as_ref() {
2642            let growing = growing_arc.read();
2643            if growing.may_contain_exact_key(key) {
2644                return true;
2645            }
2646        } else {
2647            return true;
2648        }
2649
2650        let sealed = self.sealed.read();
2651        for segment_arc in sealed.iter() {
2652            let segment = segment_arc.read();
2653            if segment.may_contain_exact_key(key) {
2654                return true;
2655            }
2656        }
2657
2658        false
2659    }
2660
2661    /// Filter by metadata across all segments
2662    pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2663        let mut results = Vec::new();
2664
2665        // Growing segment
2666        if let Some(growing_arc) = self.growing.read().as_ref() {
2667            let growing = growing_arc.read();
2668            results.extend(growing.filter_metadata(filters));
2669        }
2670
2671        // Sealed segments
2672        let sealed = self.sealed.read();
2673        for segment in sealed.iter() {
2674            results.extend(segment.filter_metadata(filters));
2675        }
2676
2677        results
2678    }
2679
2680    /// Get entities by kind
2681    pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
2682        let mut results = Vec::new();
2683
2684        // Growing segment
2685        if let Some(growing_arc) = self.growing.read().as_ref() {
2686            let growing = growing_arc.read();
2687            for entity in growing.iter_kind(kind) {
2688                results.push(entity.clone());
2689            }
2690        }
2691
2692        // Sealed segments. Iterate through the read guard, not the
2693        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` — that one cannot
2694        // hand out references across the lock, so it returns an empty iterator
2695        // and every sealed entity was silently missing from the result.
2696        let sealed = self.sealed.read();
2697        for segment_arc in sealed.iter() {
2698            let segment = segment_arc.read();
2699            for entity in segment.iter_kind(kind) {
2700                results.push(entity.clone());
2701            }
2702        }
2703
2704        results
2705    }
2706
2707    /// Count entities
2708    pub fn count(&self) -> usize {
2709        self.total_entities_atomic.load(Ordering::Relaxed) as usize
2710    }
2711
2712    /// Get all segment IDs
2713    pub fn segment_ids(&self) -> Vec<SegmentId> {
2714        let mut ids = Vec::new();
2715
2716        if let Some(growing_arc) = self.growing.read().as_ref() {
2717            ids.push(growing_arc.read().id());
2718        }
2719
2720        let sealed = self.sealed.read();
2721        for segment in sealed.iter() {
2722            ids.push(segment.id());
2723        }
2724
2725        ids.extend(self.archived.read().iter().copied());
2726
2727        ids
2728    }
2729
2730    /// Emit a lifecycle event.
2731    ///
2732    /// Perf: this used to push onto a `RwLock<Vec<LifecycleEvent>>`
2733    /// on every insert / delete / seal. Nobody consumes that vec
2734    /// today (no subscription API, `drain_events` has no callers),
2735    /// so the write lock + push was pure tax — and the vec grew
2736    /// unbounded in long-running processes.
2737    ///
2738    /// Current behaviour: no-op. If we ever want the hooks back,
2739    /// replace this with a bounded channel or an actual subscriber
2740    /// registry; the callers (`insert`, `delete`, `maybe_seal_growing`)
2741    /// already pass well-typed events.
2742    #[inline]
2743    #[allow(clippy::unused_self)]
2744    fn emit(&self, _event: LifecycleEvent) {}
2745
2746    /// Drain events. Kept for API compatibility; always returns
2747    /// empty because `emit` no longer buffers.
2748    pub fn drain_events(&self) -> Vec<LifecycleEvent> {
2749        std::mem::take(&mut *self.events.write())
2750    }
2751
2752    /// Run maintenance (would be called periodically in production)
2753    pub fn run_maintenance(&self) -> Result<(), SegmentError> {
2754        // Auto-seal idle segments
2755        self.maybe_seal_growing()?;
2756
2757        if self.config.enable_consolidation {
2758            self.consolidation_tick();
2759        }
2760
2761        Ok(())
2762    }
2763
2764    // ========================================================================
2765    // Consolidation — ADR 0073 §5
2766    // ========================================================================
2767
2768    /// One paced step of sealed-segment consolidation.
2769    ///
2770    /// Starts a run when a threshold has been crossed, copies at most
2771    /// `consolidation_entities_per_tick` live entities into the merged segment,
2772    /// and swaps once every source has been drained. Returns without finishing
2773    /// otherwise — the half-built merged segment waits for the next tick.
2774    fn consolidation_tick(&self) {
2775        // Drop the read guard before `start_consolidation` takes the write
2776        // guard: `parking_lot` locks are not reentrant.
2777        let running = self.consolidation.read().is_some();
2778        if !running && !self.start_consolidation() {
2779            return;
2780        }
2781
2782        // Keep the accounting guard through publication: the merged allocation
2783        // must never disappear between the in-flight and active inventories.
2784        let mut guard = self.consolidation.write();
2785        let Some(run) = guard.as_mut() else {
2786            return;
2787        };
2788        self.copy_bounded(run);
2789        if run.cursor >= run.sources.len() {
2790            if let Some(run) = guard.take() {
2791                self.finish_consolidation(run);
2792            }
2793        }
2794    }
2795
2796    /// Evaluate the trigger and, when it fires, open a consolidation run.
2797    ///
2798    /// Returns `true` when a run was started.
2799    fn start_consolidation(&self) -> bool {
2800        let sources = self.select_candidates();
2801        self.start_consolidation_from_sources(sources)
2802    }
2803
2804    fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
2805        if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
2806            return false;
2807        }
2808
2809        let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
2810        let merged = GrowingSegment::new(merged_id, &self.collection);
2811
2812        *self.consolidation.write() = Some(Consolidation {
2813            sources: sources
2814                .into_iter()
2815                .map(|id| SourceProgress {
2816                    id,
2817                    copied: Vec::new(),
2818                    tombstones_at_open: None,
2819                })
2820                .collect(),
2821            merged_id,
2822            merged,
2823            cursor: 0,
2824            pending_ids: Vec::new(),
2825            pending_cursor: 0,
2826            entities_copied: 0,
2827        });
2828
2829        self.stats.write().consolidation.runs_started += 1;
2830        true
2831    }
2832
2833    /// Run one paced consolidation tick because admission pressure needs
2834    /// reclaimable bytes, even if the background thresholds have not fired.
2835    ///
2836    /// Returns true when the call started or advanced a consolidation run.
2837    pub fn pressure_consolidation_tick(&self) -> bool {
2838        if !self.config.enable_consolidation {
2839            return false;
2840        }
2841
2842        let running = self.consolidation.read().is_some();
2843        if !running {
2844            // Foreground churn can exhaust the budget before a growing segment
2845            // reaches its ordinary size threshold. Publish only a dirty source;
2846            // the existing paced consolidation then reclaims its dead slots.
2847            if self
2848                .seal_current_if(|segment| segment.reclaimable_bytes() > 0)
2849                .is_err()
2850            {
2851                return false;
2852            }
2853            let sources = self.dirty_sealed_segments();
2854            if !self.start_consolidation_from_sources(sources) {
2855                return false;
2856            }
2857        }
2858
2859        self.consolidation_tick();
2860        true
2861    }
2862
2863    /// Which sealed segments this collection should merge, if any.
2864    ///
2865    /// Three thresholds, any of which arms consolidation:
2866    ///
2867    /// * **sealed-segment count** above `max_sealed_segments` — every sealed
2868    ///   segment merges, which is what that knob always claimed to do.
2869    /// * **tombstone ratio** — dead entities over total entities across the
2870    ///   sealed set, past [`CONSOLIDATION_TOMBSTONE_RATIO`].
2871    /// * **fragmentation** — reclaimable bytes over held bytes, past
2872    ///   [`CONSOLIDATION_FRAGMENTATION_RATIO`].
2873    ///
2874    /// The count trigger takes every sealed segment; the two budget triggers
2875    /// take only the segments that actually carry tombstones, so a clean
2876    /// segment is never rewritten for nothing.
2877    fn select_candidates(&self) -> Vec<SegmentId> {
2878        let sealed = self.sealed.read();
2879        if sealed.is_empty() {
2880            return Vec::new();
2881        }
2882
2883        if sealed.len() > self.config.max_sealed_segments {
2884            return sealed.iter().map(|segment| segment.read().id()).collect();
2885        }
2886
2887        let mut live = 0usize;
2888        let mut tombstones = 0usize;
2889        let mut held = 0u64;
2890        let mut reclaimable = 0u64;
2891        let mut dirty = Vec::new();
2892
2893        for segment_arc in sealed.iter() {
2894            let segment = segment_arc.read();
2895            live += segment.live_entity_count();
2896            tombstones += segment.tombstone_count();
2897            held += segment.resident_bytes();
2898            reclaimable += segment.reclaimable_bytes();
2899            if segment.tombstone_count() > 0 {
2900                dirty.push(segment.id());
2901            }
2902        }
2903
2904        let total = live + tombstones;
2905        let tombstone_ratio = if total == 0 {
2906            0.0
2907        } else {
2908            tombstones as f64 / total as f64
2909        };
2910        let fragmentation = if held == 0 {
2911            0.0
2912        } else {
2913            reclaimable as f64 / held as f64
2914        };
2915
2916        if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
2917            || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
2918        {
2919            return dirty;
2920        }
2921
2922        Vec::new()
2923    }
2924
2925    fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
2926        self.sealed
2927            .read()
2928            .iter()
2929            .filter_map(|segment_arc| {
2930                let segment = segment_arc.read();
2931                (segment.reclaimable_bytes() > 0).then(|| segment.id())
2932            })
2933            .collect()
2934    }
2935
2936    /// A single candidate is still worth merging when it carries tombstones —
2937    /// the merge drops them. A single clean candidate is not.
2938    fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
2939        if sources.is_empty() {
2940            return false;
2941        }
2942        let sealed = self.sealed.read();
2943        sealed.iter().any(|segment_arc| {
2944            let segment = segment_arc.read();
2945            sources.contains(&segment.id()) && segment.tombstone_count() > 0
2946        })
2947    }
2948
2949    fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
2950        let sealed = self.sealed.read();
2951        sealed
2952            .iter()
2953            .find(|segment| segment.read().id() == id)
2954            .map(Arc::clone)
2955    }
2956
2957    /// Copy at most one tick's worth of live entities into the merged segment.
2958    ///
2959    /// The per-tick bound is the pacing contract: a tick's cost is O(bound),
2960    /// independent of how many entities the run still has to move.
2961    fn copy_bounded(&self, run: &mut Consolidation) {
2962        let mut budget = self.config.consolidation_entities_per_tick;
2963
2964        while budget > 0 && run.cursor < run.sources.len() {
2965            let source_id = run.sources[run.cursor].id;
2966            let Some(source_arc) = self.sealed_segment(source_id) else {
2967                // The source vanished. Only consolidation removes sealed
2968                // segments and it is single-threaded per collection, so this
2969                // cannot happen today — skip it rather than resurrect its rows.
2970                run.cursor += 1;
2971                run.pending_ids.clear();
2972                run.pending_cursor = 0;
2973                continue;
2974            };
2975
2976            if run.sources[run.cursor].tombstones_at_open.is_none() {
2977                let source = source_arc.read();
2978                run.pending_ids = source.live_entity_ids();
2979                run.pending_cursor = 0;
2980                run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
2981            }
2982
2983            {
2984                let source = source_arc.read();
2985                while budget > 0 && run.pending_cursor < run.pending_ids.len() {
2986                    let id = run.pending_ids[run.pending_cursor];
2987                    run.pending_cursor += 1;
2988                    budget -= 1;
2989
2990                    // Tombstoned between the snapshot and now — it must not be
2991                    // resurrected into the merged segment.
2992                    let Some(entity) = source.get(id) else {
2993                        continue;
2994                    };
2995                    let metadata = source.get_metadata(id);
2996                    run.merged.adopt_entity(entity.clone(), metadata);
2997                    run.sources[run.cursor].copied.push(id);
2998                    run.entities_copied += 1;
2999                }
3000            }
3001
3002            if run.pending_cursor >= run.pending_ids.len() {
3003                run.cursor += 1;
3004                run.pending_ids.clear();
3005                run.pending_cursor = 0;
3006            }
3007        }
3008    }
3009
3010    /// Atomically replace the source segments with the merged one.
3011    ///
3012    /// The `sealed` write lock is the boundary: a reader holds the read lock
3013    /// for the whole of its scan, so it observes either the old set or the new
3014    /// set, never a gap and never both copies of a row.
3015    fn finish_consolidation(&self, mut run: Consolidation) {
3016        let mut sealed = self.sealed.write();
3017
3018        let mut positions = Vec::with_capacity(run.sources.len());
3019        for source in &run.sources {
3020            if let Some(index) = sealed
3021                .iter()
3022                .position(|segment| segment.read().id() == source.id)
3023            {
3024                positions.push(index);
3025            }
3026        }
3027        if positions.is_empty() {
3028            return;
3029        }
3030        positions.sort_unstable();
3031
3032        // Rows deleted from a source after we copied them must not come back.
3033        // A source whose tombstone count is unchanged saw no deletes while the
3034        // merge ran, so its copied rows need no per-row check.
3035        let mut stale = Vec::new();
3036        for source in &run.sources {
3037            let Some(source_arc) = sealed
3038                .iter()
3039                .find(|segment| segment.read().id() == source.id)
3040            else {
3041                continue;
3042            };
3043            let segment = source_arc.read();
3044            if source.tombstones_at_open == Some(segment.tombstone_count()) {
3045                continue;
3046            }
3047            stale.extend(
3048                source
3049                    .copied
3050                    .iter()
3051                    .copied()
3052                    .filter(|id| !segment.contains(*id)),
3053            );
3054        }
3055        for id in stale {
3056            run.merged.evict_entity(id);
3057        }
3058
3059        let reclaimed_bytes: u64 = positions
3060            .iter()
3061            .map(|&index| sealed[index].read().resident_bytes())
3062            .sum();
3063        let reclaimed_tombstones: u64 = positions
3064            .iter()
3065            .map(|&index| sealed[index].read().tombstone_count() as u64)
3066            .sum();
3067
3068        if run.merged.seal().is_err() {
3069            return;
3070        }
3071        let merged_bytes = run.merged.resident_bytes();
3072        let merged_arc = Arc::new(RwLock::new(run.merged));
3073
3074        {
3075            let mut retired = self.retired.write();
3076            retired.retain(|source| source.strong_count() > 0);
3077            for &index in positions.iter().rev() {
3078                let source = sealed.remove(index);
3079                if Arc::strong_count(&source) > 1 {
3080                    retired.push(Arc::downgrade(&source));
3081                }
3082            }
3083        }
3084        let insert_at = positions[0].min(sealed.len());
3085        sealed.insert(insert_at, Arc::clone(&merged_arc));
3086        drop(sealed);
3087
3088        // Remap the entity→segment hints that pointed at the retired sources.
3089        let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
3090        {
3091            let mut entity_segment = self.entity_segment.write();
3092            for segment_id in entity_segment.values_mut() {
3093                if source_ids.contains(segment_id) {
3094                    *segment_id = run.merged_id;
3095                }
3096            }
3097        }
3098
3099        {
3100            let mut stats = self.stats.write();
3101            stats.sealed_count = stats
3102                .sealed_count
3103                .saturating_sub(positions.len())
3104                .saturating_add(1);
3105            let consolidation = &mut stats.consolidation;
3106            consolidation.runs_completed += 1;
3107            consolidation.segments_merged += positions.len() as u64;
3108            consolidation.tombstones_reclaimed += reclaimed_tombstones;
3109            consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
3110        }
3111
3112        self.emit(LifecycleEvent::SegmentConsolidated {
3113            source: source_ids,
3114            target: run.merged_id,
3115        });
3116    }
3117
3118    /// Entities copied by the consolidation currently in flight, or `None` when
3119    /// none is running. Exposed for the pacing tests.
3120    #[cfg(test)]
3121    fn consolidation_progress(&self) -> Option<u64> {
3122        self.consolidation
3123            .read()
3124            .as_ref()
3125            .map(|run| run.entities_copied)
3126    }
3127}
3128
3129#[cfg(test)]
3130mod tests {
3131    use std::collections::HashSet;
3132
3133    use super::*;
3134    use crate::storage::entity::{EntityData, EntityKind, RowData};
3135    use crate::transaction::snapshot::{Snapshot, SnapshotManager};
3136    use reddb_types::Value;
3137
3138    #[test]
3139    fn recovery_view_projection_keeps_schema_bounds_without_retaining_rows_or_identities() {
3140        for collection in ["rows", "red_internal"] {
3141            let manager = SegmentManager::new(collection);
3142            let identities = manager.identity_counters();
3143            for id in 1..=18 {
3144                let field = format!("field_{id}");
3145                let mut entity =
3146                    UnifiedEntity::table_row(EntityId::new(id), collection, id, vec![]);
3147                entity.data = EntityData::Row(RowData {
3148                    columns: Vec::new(),
3149                    named: Some(HashMap::from([
3150                        (field.clone(), Value::Integer(id as i64)),
3151                        ("id".into(), Value::Integer(id as i64)),
3152                    ])),
3153                    schema: None,
3154                });
3155                manager.restore_snapshot_row_layout(&mut entity);
3156                let row = entity.data.as_row().expect("row");
3157                assert_eq!(row.get_field(&field), Some(&Value::Integer(id as i64)));
3158                assert_eq!(row.get_field("id"), Some(&Value::Integer(id as i64)));
3159                assert_eq!(
3160                    row.named.is_some(),
3161                    collection.starts_with("red_") || id > 16
3162                );
3163            }
3164            assert_eq!(manager.count(), 0, "a projection never adopts payloads");
3165            assert_eq!(manager.identity_counters(), identities);
3166            assert_eq!(
3167                manager.recovery_row_schemas.read().len(),
3168                if collection == "rows" { 16 } else { 0 }
3169            );
3170        }
3171    }
3172
3173    #[test]
3174    fn physical_pages_keep_global_order_across_segments_and_deleted_slots() {
3175        let manager = SegmentManager::new("pages");
3176        for group in [[50, 10], [30, 20]] {
3177            for id in group {
3178                let mut entity = UnifiedEntity::table_row(EntityId::new(id), "pages", id, vec![]);
3179                entity.data = EntityData::Row(RowData::with_names(
3180                    vec![Value::Integer(id as i64)],
3181                    vec!["id".into()],
3182                ));
3183                manager.insert(entity).unwrap();
3184            }
3185            manager.force_seal().unwrap();
3186        }
3187        manager
3188            .insert(UnifiedEntity::table_row(
3189                EntityId::new(40),
3190                "pages",
3191                40,
3192                vec![],
3193            ))
3194            .unwrap();
3195        let (total, page) = manager.page_in_id_order(1, 2);
3196        assert_eq!(total, 5);
3197        assert_eq!(
3198            page.iter()
3199                .map(|entity| entity.id.raw())
3200                .collect::<Vec<_>>(),
3201            [20, 30]
3202        );
3203        let EntityData::Row(row) = &page[0].data else {
3204            panic!("row")
3205        };
3206        assert_eq!(row.get_field("id"), Some(&Value::Integer(20)));
3207        manager.delete(EntityId::new(30)).unwrap();
3208        let (total, page) = manager.page_in_id_order(1, 2);
3209        assert_eq!(total, 4);
3210        assert_eq!(
3211            page.iter()
3212                .map(|entity| entity.id.raw())
3213                .collect::<Vec<_>>(),
3214            [20, 40]
3215        );
3216        for offset in [4, usize::MAX] {
3217            let (total, page) = manager.page_in_id_order(offset, 2);
3218            assert_eq!(total, 4);
3219            assert!(page.is_empty());
3220        }
3221    }
3222
3223    fn lookup_version(id: u64, key: &str, xmin: u64, xmax: u64) -> UnifiedEntity {
3224        let mut entity = UnifiedEntity::table_row(EntityId::new(id), "keys", id, vec![]);
3225        entity.data = EntityData::Row(RowData::with_names(
3226            vec![Value::text(key)],
3227            vec!["key".into()],
3228        ));
3229        entity.set_xmin(xmin);
3230        entity.set_xmax(xmax);
3231        entity
3232    }
3233
3234    #[test]
3235    fn key_lookup_matches_snapshot_scan_across_segments() {
3236        let manager = Arc::new(SegmentManager::new("keys"));
3237        for entity in [
3238            lookup_version(1, "changed", 3, 12),
3239            lookup_version(2, "deleted", 3, 7),
3240        ] {
3241            manager.insert(entity).expect("old version");
3242        }
3243        manager.force_seal().expect("seal old versions");
3244        for entity in [
3245            lookup_version(3, "changed", 12, 0),
3246            lookup_version(4, "pending", 4, 0),
3247            lookup_version(5, "own", 15, 0),
3248            lookup_version(6, "live", 3, 0),
3249        ] {
3250            manager.insert(entity).expect("new version");
3251        }
3252        std::thread::spawn(move || {
3253            let snapshot = SnapshotContext {
3254                snapshot: Snapshot {
3255                    xid: 10,
3256                    in_progress: HashSet::from([4]),
3257                },
3258                manager: Arc::new(SnapshotManager::new()),
3259                own_xids: HashSet::from([15]),
3260                requires_index_fallback: false,
3261                serializable_reader: None,
3262            };
3263            for snapshot in [Some(&snapshot), None] {
3264                for key in ["changed", "deleted", "pending", "own", "live", "absent"] {
3265                    let expected = manager
3266                        .scan(snapshot, |entity| {
3267                            crate::storage::segment_key_lookup::SegmentKeyLookup::key(entity)
3268                                == Some(key)
3269                        })
3270                        .into_iter()
3271                        .max_by_key(|entity| entity.xmin)
3272                        .map(|entity| entity.id);
3273                    assert_eq!(
3274                        manager.project_key_entity(key, snapshot, true, |entity| entity.id),
3275                        expected,
3276                        "key={key}"
3277                    );
3278                }
3279            }
3280            assert_eq!(
3281                manager.project_key_entity("changed", Some(&snapshot), true, |entity| entity.id),
3282                Some(EntityId::new(1))
3283            );
3284            assert_eq!(
3285                manager.project_key_entity("changed", None, true, |entity| entity.id),
3286                Some(EntityId::new(3))
3287            );
3288            assert!(manager
3289                .project_key_entity("deleted", None, true, |entity| entity.id)
3290                .is_none());
3291        })
3292        .join()
3293        .expect("snapshot worker");
3294    }
3295
3296    #[test]
3297    fn key_lookup_survives_paced_consolidation() {
3298        let manager = consolidating_manager(1);
3299        manager
3300            .bulk_insert(
3301                (1..=10)
3302                    .map(|id| lookup_version(id, &format!("key-{id}"), 1, 0))
3303                    .collect(),
3304            )
3305            .expect("rows");
3306        manager.force_seal().expect("seal");
3307        for id in 1..=4 {
3308            manager.delete(EntityId::new(id)).expect("delete");
3309        }
3310        assert!(manager.pressure_consolidation_tick());
3311        drain_maintenance(&manager);
3312        for id in 1..=10 {
3313            assert_eq!(
3314                manager.project_key_entity(&format!("key-{id}"), None, false, |entity| entity.id),
3315                (id > 4).then_some(EntityId::new(id))
3316            );
3317        }
3318    }
3319
3320    #[test]
3321    #[ignore = "diagnostic of segment fragmentation and retained history; no timing assertions"]
3322    fn key_lookup_fragmentation_history_diagnostic() {
3323        use std::{hint::black_box, time::Instant};
3324        for segments in [1, 16, 64] {
3325            for versions in [1, 100, 1000] {
3326                let manager = SegmentManager::new("keys");
3327                let mut id = 1;
3328                for segment in 0..segments {
3329                    let mut rows: Vec<_> = (0..128)
3330                        .map(|_| {
3331                            let entity = lookup_version(id, &format!("other-{id}"), 1, 0);
3332                            id += 1;
3333                            entity
3334                        })
3335                        .collect();
3336                    for version in (segment..versions).step_by(segments) {
3337                        rows.push(lookup_version(
3338                            id,
3339                            "history",
3340                            version as u64 + 1,
3341                            if version + 1 == versions {
3342                                0
3343                            } else {
3344                                version as u64 + 2
3345                            },
3346                        ));
3347                        id += 1;
3348                    }
3349                    manager.bulk_insert(rows).expect("rows");
3350                    manager.force_seal().expect("seal");
3351                }
3352                let started = Instant::now();
3353                for _ in 0..2000 {
3354                    let xmin =
3355                        manager.project_key_entity(black_box("history"), None, true, |entity| {
3356                            entity.xmin
3357                        });
3358                    assert_eq!(xmin, Some(versions as u64));
3359                }
3360                eprintln!("segments={segments} versions={versions} reads=2000 elapsed_us={} resident_bytes={}", started.elapsed().as_micros(), manager.resident_bytes());
3361            }
3362        }
3363    }
3364
3365    #[test]
3366    fn ordered_fingerprint_scan_matches_legacy_sort_without_cloning_rows() {
3367        let manager = SegmentManager::new("fingerprint");
3368        let row = |id, label: &str| {
3369            UnifiedEntity::table_row(
3370                EntityId::new(id),
3371                "fingerprint",
3372                id,
3373                vec![Value::text(label)],
3374            )
3375        };
3376        manager
3377            .bulk_insert(vec![row(8, "eight"), row(2, "sealed-two"), row(5, "five")])
3378            .expect("sealed rows");
3379        manager.force_seal().expect("seal");
3380        manager
3381            .bulk_insert(vec![row(7, "seven"), row(2, "growing-two"), row(1, "one")])
3382            .expect("growing rows");
3383
3384        let mut expected = manager.query_all(|_| true);
3385        expected.sort_by_key(|entity| entity.id.raw());
3386        let expected: Vec<_> = expected
3387            .iter()
3388            .map(|entity| format!("{entity:?}"))
3389            .collect();
3390        let mut stored_addresses = HashSet::new();
3391        manager.for_each_entity(|entity| {
3392            stored_addresses.insert(entity as *const UnifiedEntity as usize);
3393            true
3394        });
3395
3396        let mut actual = Vec::new();
3397        manager.for_each_entity_in_id_order(|entity| {
3398            assert!(stored_addresses.contains(&(entity as *const UnifiedEntity as usize)));
3399            assert!(
3400                manager.growing.try_write().is_some(),
3401                "topology guard retained"
3402            );
3403            assert!(
3404                manager.sealed.try_write().is_some(),
3405                "topology guard retained"
3406            );
3407            actual.push(format!("{entity:?}"));
3408            true
3409        });
3410        assert_eq!(
3411            actual, expected,
3412            "equal IDs retain their original segment order"
3413        );
3414    }
3415
3416    #[test]
3417    fn ordered_fingerprint_scan_stops_early_and_releases_sources() {
3418        let manager = SegmentManager::new("fingerprint");
3419        for id in [3, 1, 2] {
3420            manager
3421                .insert(UnifiedEntity::table_row(
3422                    EntityId::new(id),
3423                    "fingerprint",
3424                    id,
3425                    vec![Value::Integer(id as i64)],
3426                ))
3427                .expect("row");
3428        }
3429        let source = manager.growing.read().as_ref().expect("source").clone();
3430        let handles = Arc::strong_count(&source);
3431        let mut seen = Vec::new();
3432        manager.for_each_entity_in_id_order(|entity| {
3433            seen.push(entity.id.raw());
3434            false
3435        });
3436        assert_eq!(seen, vec![1]);
3437        assert_eq!(Arc::strong_count(&source), handles);
3438        assert!(
3439            source.try_write().is_some(),
3440            "segment guard retained after early stop"
3441        );
3442    }
3443
3444    #[test]
3445    fn bounded_scan_survives_sealing_consolidation_and_new_writes() {
3446        let manager = SegmentManager::with_config(
3447            "vectors",
3448            ManagerConfig {
3449                max_sealed_segments: 1,
3450                consolidation_entities_per_tick: 10_000,
3451                ..Default::default()
3452            },
3453        );
3454        for part in 0..3 {
3455            manager
3456                .bulk_insert(
3457                    (1..=300)
3458                        .map(|offset| {
3459                            UnifiedEntity::vector(
3460                                EntityId::new(part * 300 + offset),
3461                                "vectors",
3462                                vec![1.0, 0.0],
3463                            )
3464                        })
3465                        .collect(),
3466                )
3467                .expect("batch");
3468            if part < 2 {
3469                manager.force_seal().expect("seal");
3470            }
3471        }
3472        let original_growing = manager.growing.read().as_ref().expect("growing").clone();
3473        let mut changed = false;
3474        let mut seen = Vec::new();
3475        assert!(manager.scan_batches(
3476            None,
3477            || true,
3478            |batch| {
3479                assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
3480                assert!(
3481                    manager.growing.try_write().is_some(),
3482                    "consumer holds no topology guard"
3483                );
3484                assert!(
3485                    original_growing.try_write().is_some(),
3486                    "consumer holds no segment guard"
3487                );
3488                for entity in batch {
3489                    assert!(manager.get(entity.id).is_some(), "reentrant storage lookup");
3490                    seen.push(entity.id.raw());
3491                }
3492                if !changed {
3493                    changed = true;
3494                    manager.force_seal().expect("seal during scan");
3495                    manager
3496                        .insert(UnifiedEntity::vector(
3497                            EntityId::new(5000),
3498                            "vectors",
3499                            vec![1.0, 0.0],
3500                        ))
3501                        .expect("new write");
3502                    drain_maintenance(&manager);
3503                    assert!(manager.stats().consolidation.runs_completed > 0);
3504                    assert!(manager
3505                        .delete(EntityId::new(1))
3506                        .expect("delete after source retirement"));
3507                    assert!(manager.get(EntityId::new(1)).is_none());
3508                }
3509                true
3510            }
3511        ));
3512        seen.sort_unstable();
3513        assert_eq!(
3514            seen,
3515            (2..=900).collect::<Vec<_>>(),
3516            "no duplicate, missing or late candidates"
3517        );
3518        let mut after = Vec::new();
3519        assert!(manager.scan_batches(
3520            None,
3521            || true,
3522            |batch| {
3523                after.extend(batch.iter().map(|entity| entity.id.raw()));
3524                true
3525            }
3526        ));
3527        after.sort_unstable();
3528        let mut expected: Vec<_> = (2..=900).collect();
3529        expected.push(5000);
3530        assert_eq!(after, expected, "adopted entities have scan positions");
3531    }
3532
3533    #[test]
3534    fn bounded_scan_applies_snapshot_to_relocated_versions() {
3535        for (consolidate, bulk) in [(false, false), (false, true), (true, false), (true, true)] {
3536            let manager = SegmentManager::with_config(
3537                "vectors",
3538                ManagerConfig {
3539                    max_sealed_segments: 1,
3540                    ..Default::default()
3541                },
3542            );
3543            for ids in [[1, 2], [3, 4]] {
3544                let entities = ids.map(|id| {
3545                    let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3546                    entity.set_xmin(3);
3547                    entity
3548                });
3549                if bulk {
3550                    manager
3551                        .bulk_insert(entities.into())
3552                        .expect("initial versions");
3553                } else {
3554                    for entity in entities {
3555                        manager.insert(entity).expect("initial version");
3556                    }
3557                }
3558                manager.force_seal().expect("seal source");
3559            }
3560            let snapshot = SnapshotContext {
3561                snapshot: Snapshot {
3562                    xid: 10,
3563                    in_progress: HashSet::new(),
3564                },
3565                manager: Arc::new(SnapshotManager::new()),
3566                own_xids: HashSet::new(),
3567                requires_index_fallback: false,
3568                serializable_reader: None,
3569            };
3570            let mut changed = false;
3571            let mut seen = Vec::new();
3572            assert!(manager.scan_batches(
3573                Some(&snapshot),
3574                || true,
3575                |batch| {
3576                    seen.extend(batch.iter().map(|entity| entity.id.raw()));
3577                    if !changed {
3578                        changed = true;
3579                        if consolidate {
3580                            drain_maintenance(&manager);
3581                            assert!(manager.stats().consolidation.runs_completed > 0);
3582                        }
3583                        for (id, xmax) in [(3, 8), (4, 12)] {
3584                            let mut entity =
3585                                manager.get(EntityId::new(id)).expect("retained version");
3586                            entity.set_xmax(xmax);
3587                            manager
3588                                .update(entity)
3589                                .expect("update after source retirement");
3590                        }
3591                    }
3592                    true
3593                }
3594            ));
3595            seen.sort_unstable();
3596            assert_eq!(seen, vec![1, 2, 4], "snapshot uses authoritative xmax");
3597            let mut again = Vec::new();
3598            assert!(manager.scan_batches(
3599                Some(&snapshot),
3600                || true,
3601                |batch| {
3602                    again.extend(batch.iter().map(|entity| entity.id.raw()));
3603                    true
3604                }
3605            ));
3606            again.sort_unstable();
3607            assert_eq!(
3608                again,
3609                vec![1, 2, 4],
3610                "old slots do not duplicate relocated versions"
3611            );
3612            let mut current = Vec::new();
3613            assert!(manager.scan_batches(
3614                None,
3615                || true,
3616                |batch| {
3617                    current.extend(batch.iter().map(|entity| entity.id.raw()));
3618                    true
3619                }
3620            ));
3621            current.sort_unstable();
3622            assert_eq!(
3623                current,
3624                vec![1, 2],
3625                "current view excludes both deleted versions"
3626            );
3627        }
3628    }
3629
3630    #[test]
3631    fn bounded_scan_keeps_flat_and_hash_positions_through_mutation() {
3632        let manager = SegmentManager::new("mixed");
3633        let vector = |id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![1.0]);
3634        manager
3635            .bulk_insert((1..=300).map(vector).collect())
3636            .expect("flat");
3637        manager.insert(vector(500)).expect("hash insert");
3638        manager
3639            .bulk_insert(vec![vector(502), vector(505)])
3640            .expect("gapped bulk");
3641        manager.delete(EntityId::new(2)).expect("flat delete");
3642        manager.delete(EntityId::new(505)).expect("hash delete");
3643        let mut changed = false;
3644        let mut seen = Vec::new();
3645        assert!(manager.scan_batches(
3646            None,
3647            || true,
3648            |batch| {
3649                seen.extend(batch.iter().map(|entity| entity.id.raw()));
3650                if !changed {
3651                    changed = true;
3652                    manager
3653                        .insert(vector(700))
3654                        .expect("append after cursor boundary");
3655                    manager
3656                        .update(UnifiedEntity::graph_node(
3657                            EntityId::new(500),
3658                            "mixed",
3659                            "Node",
3660                            HashMap::new(),
3661                        ))
3662                        .expect("structural update");
3663                }
3664                true
3665            }
3666        ));
3667        seen.sort_unstable();
3668        let mut expected: Vec<_> = (1..=300).filter(|id| *id != 2).collect();
3669        expected.extend([500, 502]);
3670        assert_eq!(seen, expected);
3671    }
3672
3673    #[test]
3674    fn bounded_scan_preserves_snapshot_and_discards_interrupted_batches() {
3675        let manager = SegmentManager::new("vectors");
3676        let mut entities = Vec::new();
3677        for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4)] {
3678            let mut entity = UnifiedEntity::vector(EntityId::new(id), "vectors", vec![1.0]);
3679            entity.set_xmin(xmin);
3680            entity.set_xmax(xmax);
3681            entities.push(entity);
3682        }
3683        manager.bulk_insert(entities).expect("versions");
3684        manager.force_seal().expect("seal");
3685        let snapshot = SnapshotContext {
3686            snapshot: Snapshot {
3687                xid: 10,
3688                in_progress: HashSet::from([4]),
3689            },
3690            manager: Arc::new(SnapshotManager::new()),
3691            own_xids: HashSet::new(),
3692            requires_index_fallback: false,
3693            serializable_reader: None,
3694        };
3695        let mut work = 0;
3696        let mut callbacks = 0;
3697        assert!(!manager.scan_batches(
3698            Some(&snapshot),
3699            || {
3700                work += 1;
3701                work <= 2
3702            },
3703            |_| {
3704                callbacks += 1;
3705                true
3706            }
3707        ));
3708        assert_eq!(callbacks, 0, "a stopped batch is not consumed");
3709        for (view, expected) in [(Some(&snapshot), vec![1, 3, 4]), (None, vec![2, 3])] {
3710            let mut seen = Vec::new();
3711            assert!(manager.scan_batches(
3712                view,
3713                || true,
3714                |batch| {
3715                    seen.extend(batch.iter().map(|entity| entity.id.raw()));
3716                    true
3717                }
3718            ));
3719            seen.sort_unstable();
3720            assert_eq!(seen, expected);
3721        }
3722    }
3723
3724    #[test]
3725    fn test_manager_basic() {
3726        let manager = SegmentManager::new("test_collection");
3727
3728        let entity = UnifiedEntity::table_row(
3729            manager.next_entity_id(),
3730            "users",
3731            1,
3732            vec![Value::text("Alice".to_string())],
3733        );
3734
3735        let id = manager.insert(entity).unwrap();
3736        assert!(manager.get(id).is_some());
3737        assert_eq!(manager.count(), 1);
3738    }
3739
3740    #[test]
3741    fn graph_pruning_preserves_physical_versions_and_charges_only_selected_segments() {
3742        let manager = Arc::new(SegmentManager::new("mixed"));
3743        manager
3744            .bulk_insert(
3745                (1..=128)
3746                    .map(|id| UnifiedEntity::vector(EntityId::new(id), "mixed", vec![]))
3747                    .collect(),
3748            )
3749            .expect("vector-only segment");
3750        manager.force_seal().expect("seal vectors");
3751        let mut old =
3752            UnifiedEntity::graph_node(EntityId::new(200), "mixed", "person", Default::default());
3753        old.set_xmin(3);
3754        old.set_xmax(12);
3755        let mut new =
3756            UnifiedEntity::graph_node(EntityId::new(201), "mixed", "person", Default::default());
3757        new.set_xmin(12);
3758        let edge = UnifiedEntity::graph_edge(
3759            EntityId::new(202),
3760            "mixed",
3761            "a",
3762            "b",
3763            1.0,
3764            Default::default(),
3765        );
3766        manager
3767            .bulk_insert(vec![old, new, edge])
3768            .expect("physical graph versions");
3769        manager.force_seal().expect("seal graph");
3770        manager
3771            .insert(UnifiedEntity::graph_node(
3772                EntityId::new(203),
3773                "mixed",
3774                "person",
3775                Default::default(),
3776            ))
3777            .expect("growing node");
3778        let snapshot = SnapshotContext {
3779            snapshot: Snapshot {
3780                xid: 10,
3781                in_progress: HashSet::new(),
3782            },
3783            manager: Arc::new(SnapshotManager::new()),
3784            own_xids: HashSet::new(),
3785            requires_index_fallback: false,
3786            serializable_reader: None,
3787        };
3788        std::thread::spawn(move || {
3789            for view in [None, Some(&snapshot)] {
3790                for kind in [GraphEntityKind::Node, GraphEntityKind::Edge] {
3791                    let expected: HashSet<_> = manager
3792                        .scan(view, |item| kind.matches(&item.kind))
3793                        .into_iter()
3794                        .map(|item| item.id)
3795                        .collect();
3796                    let actual: HashSet<_> = manager
3797                        .scan_graph_kind(view, kind)
3798                        .into_iter()
3799                        .map(|item| item.id)
3800                        .collect();
3801                    assert_eq!(actual, expected);
3802                    let mut batched = HashSet::new();
3803                    assert!(manager.scan_graph_batches(
3804                        view,
3805                        kind,
3806                        || true,
3807                        |batch| {
3808                            batched.extend(batch.into_iter().map(|item| item.id));
3809                            true
3810                        }
3811                    ));
3812                    assert_eq!(batched, expected);
3813                    let mut visited = 0;
3814                    let mut candidates = HashSet::new();
3815                    manager.scan_graph_candidates_for_each(
3816                        view,
3817                        kind,
3818                        || true,
3819                        |item| {
3820                            visited += 1;
3821                            if kind.matches(&item.kind) {
3822                                candidates.insert(item.id);
3823                            }
3824                            true
3825                        },
3826                    );
3827                    assert_eq!(candidates, expected);
3828                    assert_eq!(
3829                        visited,
3830                        if matches!(kind, GraphEntityKind::Node) {
3831                            3
3832                        } else {
3833                            2
3834                        }
3835                    );
3836                }
3837            }
3838            let ids: HashSet<_> = manager
3839                .scan_graph_kind(Some(&snapshot), GraphEntityKind::Node)
3840                .into_iter()
3841                .map(|item| item.id.raw())
3842                .collect();
3843            assert_eq!(ids, HashSet::from([200, 203]));
3844            let ids: HashSet<_> = manager
3845                .scan_graph_kind(None, GraphEntityKind::Node)
3846                .into_iter()
3847                .map(|item| item.id.raw())
3848                .collect();
3849            assert_eq!(ids, HashSet::from([201, 203]));
3850            let mut visits = 0;
3851            manager.scan_graph_candidates_for_each(
3852                None,
3853                GraphEntityKind::Node,
3854                || true,
3855                |_| {
3856                    visits += 1;
3857                    false
3858                },
3859            );
3860            assert_eq!(
3861                visits, 1,
3862                "early termination must also stop subsequent segments"
3863            );
3864        })
3865        .join()
3866        .expect("snapshot worker");
3867    }
3868
3869    #[test]
3870    fn snapshot_scan_applies_table_driven_visibility_on_fresh_thread() {
3871        let manager = Arc::new(SegmentManager::new("accounts"));
3872        let cases = [
3873            ("legacy row", 0, 0, true),
3874            ("committed creator", 3, 0, true),
3875            ("future creator", 11, 0, false),
3876            ("in-progress creator", 4, 0, false),
3877            ("committed deleter", 3, 7, false),
3878            ("future deleter", 3, 11, true),
3879            ("in-progress deleter", 3, 4, true),
3880        ];
3881
3882        for (index, (_, xmin, xmax, _)) in cases.iter().enumerate() {
3883            let row_id = index as u64 + 1;
3884            let mut entity = UnifiedEntity::table_row(
3885                EntityId::new(row_id),
3886                "accounts",
3887                row_id,
3888                vec![Value::Integer(row_id as i64)],
3889            );
3890            entity.set_xmin(*xmin);
3891            entity.set_xmax(*xmax);
3892            manager.insert(entity).unwrap();
3893        }
3894
3895        let snapshot = SnapshotContext {
3896            snapshot: Snapshot {
3897                xid: 10,
3898                in_progress: HashSet::from([4]),
3899            },
3900            manager: Arc::new(SnapshotManager::new()),
3901            own_xids: HashSet::new(),
3902            requires_index_fallback: false,
3903            serializable_reader: None,
3904        };
3905
3906        let raw_visible_row_ids: HashSet<u64> = manager
3907            .query_all(|entity| entity_visible_with_context(Some(&snapshot), entity))
3908            .into_iter()
3909            .filter_map(|entity| match entity.kind {
3910                EntityKind::TableRow { row_id, .. } => Some(row_id),
3911                _ => None,
3912            })
3913            .collect();
3914
3915        let scanned = std::thread::spawn(move || manager.scan(Some(&snapshot), |_| true))
3916            .join()
3917            .unwrap();
3918        let visible_row_ids: HashSet<u64> = scanned
3919            .into_iter()
3920            .filter_map(|entity| match entity.kind {
3921                EntityKind::TableRow { row_id, .. } => Some(row_id),
3922                _ => None,
3923            })
3924            .collect();
3925        assert_eq!(visible_row_ids, raw_visible_row_ids);
3926
3927        for (index, (name, _, _, expected_visible)) in cases.iter().enumerate() {
3928            let row_id = index as u64 + 1;
3929            assert_eq!(
3930                visible_row_ids.contains(&row_id),
3931                *expected_visible,
3932                "{name} visibility mismatch"
3933            );
3934        }
3935    }
3936
3937    #[test]
3938    fn frameless_scan_hides_superseded_versions_and_moderated_rows() {
3939        // A `None` snapshot is the autocommit / prepared-fast-path read: it
3940        // must keep the resolver fallback (xmax == 0), never show every
3941        // version, and never panic (issue #2136 review).
3942        let manager = SegmentManager::new("accounts");
3943        let mut live =
3944            UnifiedEntity::table_row(EntityId::new(1), "accounts", 1, vec![Value::Integer(1)]);
3945        live.set_xmin(2);
3946        live.set_xmax(0);
3947        manager.insert(live).unwrap();
3948
3949        let mut superseded =
3950            UnifiedEntity::table_row(EntityId::new(2), "accounts", 2, vec![Value::Integer(2)]);
3951        superseded.set_xmin(2);
3952        superseded.set_xmax(9);
3953        manager.insert(superseded).unwrap();
3954
3955        let rows: Vec<u64> = manager
3956            .scan(None, |_| true)
3957            .into_iter()
3958            .filter_map(|entity| match entity.kind {
3959                EntityKind::TableRow { row_id, .. } => Some(row_id),
3960                _ => None,
3961            })
3962            .collect();
3963        assert_eq!(
3964            rows,
3965            vec![1],
3966            "frameless scan must hide superseded versions"
3967        );
3968    }
3969
3970    #[test]
3971    fn bloom_may_contain_key_probes_inserted_rid_bytes() {
3972        let manager = SegmentManager::new("test_collection");
3973
3974        let entity = UnifiedEntity::table_row(
3975            manager.next_entity_id(),
3976            "users",
3977            1,
3978            vec![Value::text("Alice".to_string())],
3979        );
3980
3981        let id = manager.insert(entity).unwrap();
3982
3983        assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3984        assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
3985        assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
3986
3987        assert!(manager.delete(id).unwrap());
3988        assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
3989    }
3990
3991    #[test]
3992    fn zoned_scan_reports_pruned_segments_and_preserves_results() {
3993        let config = ManagerConfig {
3994            segment_config: SegmentConfig {
3995                max_entities: 2,
3996                ..Default::default()
3997            },
3998            ..Default::default()
3999        };
4000        let manager = SegmentManager::with_config("events", config);
4001
4002        for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
4003            let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
4004            let entity = UnifiedEntity::new(
4005                manager.next_entity_id(),
4006                EntityKind::TableRow {
4007                    table: "events".into(),
4008                    row_id: (idx + 1) as u64,
4009                },
4010                EntityData::Row(row),
4011            );
4012            manager.insert(entity).unwrap();
4013        }
4014
4015        let full = manager.query_all(|entity| match &entity.data {
4016            EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
4017            _ => false,
4018        });
4019        let probe = Value::Integer(100);
4020        let (pruned, stats) =
4021            manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
4022                match &entity.data {
4023                    EntityData::Row(row) => {
4024                        matches!(row.get_field("ts"), Some(Value::Integer(100)))
4025                    }
4026                    _ => false,
4027                }
4028            });
4029
4030        assert_eq!(pruned.len(), full.len());
4031        assert_eq!(pruned[0].id, full[0].id);
4032        assert_eq!(stats.segments_total, 3);
4033        assert_eq!(stats.segments_scanned, 2);
4034        assert_eq!(stats.segments_pruned, 1);
4035    }
4036
4037    #[test]
4038    fn projected_zoned_scan_preserves_segment_order_and_pruning() {
4039        let manager = SegmentManager::with_config(
4040            "events",
4041            ManagerConfig {
4042                segment_config: SegmentConfig {
4043                    max_entities: 2,
4044                    ..Default::default()
4045                },
4046                ..Default::default()
4047            },
4048        );
4049        for (index, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
4050            let mut entity = UnifiedEntity::table_row(
4051                EntityId::new(index as u64 + 1),
4052                "events",
4053                index as u64 + 1,
4054                vec![],
4055            );
4056            entity.data = EntityData::Row(RowData::with_names(
4057                vec![Value::Integer(value)],
4058                vec!["ts".into()],
4059            ));
4060            manager.insert(entity).expect("test row");
4061        }
4062        assert_eq!(manager.stats().sealed_count, 2);
4063        let project = |entity: &UnifiedEntity| match entity.data.as_row()?.get_field("ts")? {
4064            Value::Integer(value) => Some(*value),
4065            _ => None,
4066        };
4067        let (all, stats) = manager.scan_project_zoned_with_stats(None, &[], project);
4068        let (legacy, _) = manager.scan_zoned_with_stats(None, &[], |_| true);
4069        assert_eq!(all, legacy.iter().filter_map(project).collect::<Vec<_>>());
4070        assert_eq!(all[0], 200, "growing segment first");
4071        assert!(all[1..3].iter().all(|value| [10, 20].contains(value)));
4072        assert!(all[3..5].iter().all(|value| [100, 110].contains(value)));
4073        assert_eq!(stats.segments_total, 3);
4074        assert_eq!(stats.segments_scanned, 3);
4075        assert_eq!(stats.segments_pruned, 0);
4076        let probe = Value::Integer(100);
4077        let (pruned, stats) = manager.scan_project_zoned_with_stats(
4078            None,
4079            &[("ts", ZoneColPred::Gte(&probe))],
4080            |entity| project(entity).filter(|value| *value >= 100),
4081        );
4082        assert_eq!(
4083            pruned,
4084            all.into_iter()
4085                .filter(|value| *value >= 100)
4086                .collect::<Vec<_>>()
4087        );
4088        assert_eq!(stats.segments_total, 3);
4089        assert_eq!(stats.segments_scanned, 2);
4090        assert_eq!(stats.segments_pruned, 1);
4091        let (empty, _) = manager.scan_project_zoned_with_stats(None, &[], |_| None::<u64>);
4092        assert!(empty.is_empty(), "projection may reject every candidate");
4093
4094        let failure = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4095            manager.scan_project_zoned_with_stats(None, &[], |entity| {
4096                let value = project(entity)?;
4097                assert_ne!(value, 10, "injected projection worker failure");
4098                Some(value)
4099            })
4100        }));
4101        assert!(
4102            failure.is_err(),
4103            "a failed worker must not return a successful partial scan"
4104        );
4105    }
4106
4107    #[test]
4108    fn projected_zoned_scan_checks_visibility_before_projection_on_workers() {
4109        let manager = SegmentManager::with_config(
4110            "accounts",
4111            ManagerConfig {
4112                segment_config: SegmentConfig {
4113                    max_entities: 2,
4114                    ..Default::default()
4115                },
4116                ..Default::default()
4117            },
4118        );
4119        for (id, xmin, xmax) in [(1, 3, 12), (2, 12, 0), (3, 3, 0), (4, 3, 4), (5, 3, 0)] {
4120            let mut entity = UnifiedEntity::table_row(EntityId::new(id), "accounts", id, vec![]);
4121            entity.set_xmin(xmin);
4122            entity.set_xmax(xmax);
4123            manager.insert(entity).expect("versioned test row");
4124        }
4125        let mut hidden = UnifiedEntity::table_row(EntityId::new(6), "accounts", 6, vec![]);
4126        hidden.data = EntityData::Row(RowData::with_names(
4127            vec![Value::text("pending")],
4128            vec!["__moderation_status".into()],
4129        ));
4130        manager.insert(hidden).expect("moderated row");
4131        assert_eq!(manager.stats().sealed_count, 2);
4132        let snapshot = SnapshotContext {
4133            snapshot: Snapshot {
4134                xid: 10,
4135                in_progress: HashSet::from([4]),
4136            },
4137            manager: Arc::new(SnapshotManager::new()),
4138            own_xids: HashSet::new(),
4139            requires_index_fallback: false,
4140            serializable_reader: None,
4141        };
4142        std::thread::spawn(move || {
4143            for (view, expected) in [(Some(&snapshot), vec![5, 1, 3, 4]), (None, vec![5, 2, 3])] {
4144                let called = std::sync::Mutex::new(Vec::new());
4145                let (rows, stats) = manager.scan_project_zoned_with_stats(view, &[], |entity| {
4146                    let id = entity.id.raw();
4147                    assert!(
4148                        expected.contains(&id),
4149                        "invisible entity reached projection: {id}"
4150                    );
4151                    called.lock().expect("callback log").push(id);
4152                    Some(id)
4153                });
4154                let (legacy, _) = manager.scan_zoned_with_stats(view, &[], |_| true);
4155                assert_eq!(
4156                    rows,
4157                    legacy
4158                        .iter()
4159                        .map(|entity| entity.id.raw())
4160                        .collect::<Vec<_>>()
4161                );
4162                assert_eq!(rows[0], 5, "visible growing row first");
4163                let mut called = called.into_inner().expect("callback log");
4164                called.sort_unstable();
4165                let mut expected = expected;
4166                expected.sort_unstable();
4167                assert_eq!(called, expected, "each visible row projected exactly once");
4168                assert_eq!(stats.segments_total, 3);
4169                assert_eq!(stats.segments_scanned, 3);
4170            }
4171        })
4172        .join()
4173        .expect("projected scan worker");
4174    }
4175
4176    #[test]
4177    fn test_manager_auto_seal() {
4178        let config = ManagerConfig {
4179            segment_config: SegmentConfig {
4180                max_entities: 2,
4181                ..Default::default()
4182            },
4183            ..Default::default()
4184        };
4185
4186        let manager = SegmentManager::with_config("test", config);
4187
4188        // Insert first entity
4189        manager
4190            .insert(UnifiedEntity::vector(
4191                manager.next_entity_id(),
4192                "v",
4193                vec![0.1],
4194            ))
4195            .unwrap();
4196
4197        // Insert second entity (triggers seal check)
4198        manager
4199            .insert(UnifiedEntity::vector(
4200                manager.next_entity_id(),
4201                "v",
4202                vec![0.2],
4203            ))
4204            .unwrap();
4205
4206        // Insert third entity (should trigger auto-seal of first segment)
4207        manager
4208            .insert(UnifiedEntity::vector(
4209                manager.next_entity_id(),
4210                "v",
4211                vec![0.3],
4212            ))
4213            .unwrap();
4214
4215        let stats = manager.stats();
4216        assert_eq!(stats.total_entities, 3);
4217    }
4218
4219    #[test]
4220    fn test_manager_delete() {
4221        let manager = SegmentManager::new("test");
4222
4223        let id = manager
4224            .insert(UnifiedEntity::vector(
4225                manager.next_entity_id(),
4226                "v",
4227                vec![0.1],
4228            ))
4229            .unwrap();
4230
4231        assert!(manager.get(id).is_some());
4232        assert!(manager.delete(id).unwrap());
4233        assert!(manager.get(id).is_none());
4234    }
4235
4236    #[test]
4237    fn test_manager_metadata() {
4238        let manager = SegmentManager::new("test");
4239
4240        let id = manager
4241            .insert(UnifiedEntity::table_row(
4242                manager.next_entity_id(),
4243                "hosts",
4244                1,
4245                vec![Value::text("192.168.1.1".to_string())],
4246            ))
4247            .unwrap();
4248
4249        let mut meta = Metadata::new();
4250        meta.set(
4251            "os",
4252            super::super::metadata::MetadataValue::String("linux".to_string()),
4253        );
4254
4255        manager.set_metadata(id, meta).unwrap();
4256
4257        let retrieved = manager.get_metadata(id).unwrap();
4258        assert!(retrieved.has("os"));
4259    }
4260
4261    #[test]
4262    fn test_manager_query_by_kind() {
4263        let manager = SegmentManager::new("test");
4264
4265        manager
4266            .insert(UnifiedEntity::table_row(
4267                manager.next_entity_id(),
4268                "hosts",
4269                1,
4270                vec![],
4271            ))
4272            .unwrap();
4273
4274        manager
4275            .insert(UnifiedEntity::vector(
4276                manager.next_entity_id(),
4277                "embeddings",
4278                vec![0.1],
4279            ))
4280            .unwrap();
4281
4282        manager
4283            .insert(UnifiedEntity::table_row(
4284                manager.next_entity_id(),
4285                "hosts",
4286                2,
4287                vec![],
4288            ))
4289            .unwrap();
4290
4291        let rows = manager.get_by_kind("table");
4292        assert_eq!(rows.len(), 2);
4293
4294        let vectors = manager.get_by_kind("vector");
4295        assert_eq!(vectors.len(), 1);
4296    }
4297
4298    // ========================================================================
4299    // Consolidation — ADR 0073 §5
4300    // ========================================================================
4301
4302    /// A manager whose sealed set never trips the count trigger, so tests can
4303    /// exercise the tombstone/fragmentation triggers in isolation.
4304    fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
4305        SegmentManager::with_config(
4306            "test",
4307            ManagerConfig {
4308                max_sealed_segments: 1_000,
4309                consolidation_entities_per_tick: entities_per_tick,
4310                ..Default::default()
4311            },
4312        )
4313    }
4314
4315    /// Bulk-load `count` rows into a fresh segment and seal it. Bulk-inserted
4316    /// entities land in flat storage, where a tombstone keeps its payload
4317    /// resident — which is the memory consolidation reclaims.
4318    fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
4319        let entities: Vec<UnifiedEntity> = (0..count)
4320            .map(|i| {
4321                UnifiedEntity::table_row(
4322                    manager.next_entity_id(),
4323                    "users",
4324                    i as u64,
4325                    vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
4326                )
4327            })
4328            .collect();
4329        let ids = manager.bulk_insert(entities).expect("bulk insert");
4330        manager.force_seal().expect("seal");
4331        ids
4332    }
4333
4334    fn live_ids(manager: &SegmentManager) -> Vec<u64> {
4335        let mut ids: Vec<u64> = manager
4336            .query_all(|_| true)
4337            .into_iter()
4338            .map(|entity| entity.id.raw())
4339            .collect();
4340        ids.sort_unstable();
4341        ids
4342    }
4343
4344    fn drain_maintenance(manager: &SegmentManager) -> usize {
4345        let mut ticks = 0;
4346        while manager.consolidation.read().is_some() || ticks == 0 {
4347            manager.run_maintenance().expect("maintenance");
4348            ticks += 1;
4349            assert!(ticks < 1_000, "consolidation failed to converge");
4350        }
4351        ticks
4352    }
4353
4354    #[test]
4355    fn graph_batches_bound_payloads_and_hydrate_current_entities_after_sealing() {
4356        let manager = SegmentManager::new("graph");
4357        let node =
4358            |id| UnifiedEntity::graph_node(EntityId::new(id), "graph", "node", HashMap::new());
4359        manager
4360            .bulk_insert((1..=1024).map(node).collect())
4361            .expect("nodes");
4362        let mut hidden = node(1025);
4363        hidden.set_xmax(5);
4364        manager.insert(hidden).expect("hidden version");
4365        manager
4366            .insert(UnifiedEntity::vector(EntityId::new(1026), "graph", vec![]))
4367            .expect("mixed kind");
4368        let mut work = 0;
4369        let mut delivered = 0;
4370        assert!(!manager.scan_graph_batches(
4371            None,
4372            GraphEntityKind::Node,
4373            || {
4374                work += 1;
4375                work < 100
4376            },
4377            |batch| {
4378                delivered += batch.len();
4379                true
4380            }
4381        ));
4382        assert_eq!(delivered, 0, "cancelled partial batch must not escape");
4383        let mut seen = Vec::new();
4384        assert!(manager.scan_graph_batches(None, GraphEntityKind::Node, || true, |batch| {
4385            assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
4386            assert!(manager.growing.try_write().is_some(), "no topology guard");
4387            assert!(manager.sealed.try_write().is_some(), "no sealed guard");
4388            if seen.is_empty() {
4389                manager.force_seal().expect("seal during delivery");
4390                manager.insert(node(2000)).expect("append beyond captured boundary");
4391                let mut updated = node(900);
4392                if let EntityKind::GraphNode(node) = &mut updated.kind {
4393                    node.label = "updated".into();
4394                }
4395                manager.update(updated).expect("update unread candidate");
4396            }
4397            for entity in batch {
4398                if entity.id.raw() == 900 {
4399                    assert!(matches!(&entity.kind, EntityKind::GraphNode(node) if node.label == "updated"));
4400                }
4401                seen.push(entity.id.raw());
4402            }
4403            true
4404        }));
4405        assert_eq!(seen, (1..=1024).collect::<Vec<_>>());
4406    }
4407
4408    #[test]
4409    fn graph_index_batches_pin_sources_and_hydrate_current_after_consolidation() {
4410        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4411        let ids = manager
4412            .bulk_insert(
4413                (1..=1024)
4414                    .map(|id| {
4415                        UnifiedEntity::graph_edge(
4416                            EntityId::new(id),
4417                            "step",
4418                            "2000",
4419                            "3000",
4420                            1.0,
4421                            HashMap::new(),
4422                        )
4423                    })
4424                    .collect(),
4425            )
4426            .expect("edges");
4427        manager.force_seal().expect("seal");
4428        for id in ids.iter().take(300) {
4429            manager.delete(*id).expect("fragment source");
4430        }
4431        let source = Arc::downgrade(&manager.sealed.read()[0]);
4432        let mut seen = Vec::new();
4433        let mut changed = false;
4434        let mut metadata_bytes = 0;
4435        assert!(manager.visit_graph_index_batches(GraphEntityKind::Edge, &[EntityId::new(2000)],
4436            || true,
4437            |bytes| { metadata_bytes += bytes; Ok::<_, ()>(()) },
4438            |batch| {
4439                assert!(batch.len() <= super::super::segment::SCAN_BATCH_SIZE);
4440                assert!(manager.growing.try_write().is_some(), "no growing topology guard");
4441                assert!(manager.sealed.try_write().is_some(), "no sealed topology guard");
4442                assert!(source.upgrade().expect("source pinned").try_write().is_some(), "no source guard");
4443                if !changed {
4444                    changed = true;
4445                    drain_maintenance(&manager);
4446                    assert!(manager.stats().consolidation.runs_completed > 0);
4447                    assert!(source.upgrade().is_some(), "retired cursor source remains pinned");
4448                    let mut updated = manager.get(EntityId::new(1024)).expect("future edge");
4449                    if let EntityKind::GraphEdge(edge) = &mut updated.kind { edge.weight = 2000; }
4450                    manager.update(updated).expect("update authoritative merged copy");
4451                }
4452                for id in batch {
4453                    let entity = manager.get(*id).expect("hydrate current manager");
4454                    if id.raw() == 1024 {
4455                        assert!(matches!(&entity.kind, EntityKind::GraphEdge(edge) if edge.weight == 2000));
4456                    }
4457                    seen.push(id.raw());
4458                }
4459                Ok(true)
4460            }).expect("cursor"));
4461        assert_eq!(seen, (301..=1024).collect::<Vec<_>>());
4462        assert!(
4463            source.upgrade().is_none(),
4464            "finished cursor releases retired source"
4465        );
4466        assert!(
4467            metadata_bytes < 4096,
4468            "fixed ID buffer plus source handles: {metadata_bytes}"
4469        );
4470    }
4471
4472    #[test]
4473    fn graph_index_batches_propagate_admission_and_cancel_before_partial_delivery() {
4474        let manager = SegmentManager::new("graph");
4475        manager
4476            .bulk_insert(
4477                (1..=1024)
4478                    .map(|id| {
4479                        UnifiedEntity::graph_edge(
4480                            EntityId::new(id),
4481                            "step",
4482                            "2000",
4483                            "3000",
4484                            1.0,
4485                            HashMap::new(),
4486                        )
4487                    })
4488                    .collect(),
4489            )
4490            .expect("edges");
4491        let mut delivered = 0;
4492        let denied = manager.visit_graph_index_batches(
4493            GraphEntityKind::Edge,
4494            &[EntityId::new(2000)],
4495            || true,
4496            |_| Err("out of memory"),
4497            |ids| {
4498                delivered += ids.len();
4499                Ok(true)
4500            },
4501        );
4502        assert_eq!(denied, Err("out of memory"));
4503        assert_eq!(delivered, 0);
4504        let mut work = 0;
4505        assert!(!manager
4506            .visit_graph_index_batches(
4507                GraphEntityKind::Edge,
4508                &[EntityId::new(2000)],
4509                || {
4510                    work += 1;
4511                    work < 100
4512                },
4513                |_| Ok::<_, ()>(()),
4514                |ids| {
4515                    delivered += ids.len();
4516                    Ok(true)
4517                }
4518            )
4519            .expect("cancelled cursor"));
4520        assert_eq!(
4521            delivered, 0,
4522            "cancelled partial batch cannot reach consumer"
4523        );
4524        assert!(!manager
4525            .visit_graph_index_batches(
4526                GraphEntityKind::Edge,
4527                &[EntityId::new(2000)],
4528                || true,
4529                |_| Ok::<_, ()>(()),
4530                |ids| {
4531                    delivered += ids.len();
4532                    Ok(false)
4533                }
4534            )
4535            .expect("consumer stop"));
4536        assert_eq!(delivered, super::super::segment::SCAN_BATCH_SIZE);
4537    }
4538
4539    #[test]
4540    fn retained_memory_tracks_two_readers_until_the_last_cursor_finishes() {
4541        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4542        let ids = seal_bulk_segment(&manager, 100);
4543        for id in ids.iter().take(40) {
4544            manager.delete(*id).expect("delete before scan");
4545        }
4546        let source_bytes = manager.resident_bytes();
4547        let source = Arc::downgrade(&manager.sealed.read()[0]);
4548        std::thread::scope(|scope| {
4549            let (ready, waiting) = std::sync::mpsc::channel();
4550            let (release_first, first_wait) = std::sync::mpsc::channel();
4551            let (release_last, last_wait) = std::sync::mpsc::channel();
4552            let reader = |resume: std::sync::mpsc::Receiver<()>,
4553                          ready: std::sync::mpsc::Sender<()>| {
4554                assert!(manager.scan_batches(
4555                    None,
4556                    || true,
4557                    |_| {
4558                        ready.send(()).expect("signal captured cursor");
4559                        let _ = resume.recv();
4560                        true
4561                    }
4562                ));
4563            };
4564            let ready_first = ready.clone();
4565            let first = scope.spawn(move || reader(first_wait, ready_first));
4566            let last = scope.spawn(move || reader(last_wait, ready));
4567            for _ in 0..2 {
4568                waiting
4569                    .recv_timeout(std::time::Duration::from_secs(10))
4570                    .expect("reader ready");
4571            }
4572            drain_maintenance(&manager);
4573            assert!(
4574                source.upgrade().is_some(),
4575                "both cursors pin the retired source"
4576            );
4577            let active_bytes = manager.sealed.read()[0].read().resident_bytes();
4578            let held = manager.resident_bytes();
4579            assert!(
4580                held >= active_bytes + source_bytes,
4581                "retired source remains resident"
4582            );
4583            release_first.send(()).expect("release first reader");
4584            first.join().expect("first reader");
4585            assert_eq!(
4586                manager.resident_bytes(),
4587                held,
4588                "one reader still pins the source"
4589            );
4590            release_last.send(()).expect("release last reader");
4591            last.join().expect("last reader");
4592            assert!(
4593                source.upgrade().is_none(),
4594                "accounting must not retain payloads"
4595            );
4596            assert_eq!(held - manager.resident_bytes(), source_bytes);
4597            assert_eq!(
4598                manager.stats().total_memory_bytes as u64,
4599                manager.resident_bytes()
4600            );
4601        });
4602    }
4603
4604    #[test]
4605    fn physical_cursor_tracks_individual_and_mixed_insertions() {
4606        for mode in 0..3 {
4607            let manager = SegmentManager::new("physical_rows");
4608            let row = |id| {
4609                UnifiedEntity::table_row(
4610                    EntityId::new(id),
4611                    "physical_rows",
4612                    id,
4613                    vec![Value::Integer(id as i64)],
4614                )
4615            };
4616            match mode {
4617                0 => {
4618                    for id in [10, 30, 20] {
4619                        manager.insert(row(id)).expect("individual row");
4620                    }
4621                }
4622                1 => {
4623                    manager.insert(row(10)).expect("individual prefix");
4624                    manager
4625                        .bulk_insert(vec![row(30), row(31)])
4626                        .expect("batch suffix");
4627                }
4628                _ => {
4629                    manager
4630                        .bulk_insert(vec![row(30), row(31)])
4631                        .expect("batch prefix");
4632                    manager.insert(row(10)).expect("individual suffix");
4633                }
4634            }
4635            let mut cursor = manager.physical_scan_cursor();
4636            let mut ids = Vec::new();
4637            for _ in 0..32 {
4638                if cursor.finished() {
4639                    break;
4640                }
4641                let mut work = 1;
4642                let batch = manager.physical_scan_batch(&mut cursor, &mut work);
4643                assert!(batch.len() <= 1);
4644                ids.extend(batch.into_iter().map(|id| id.raw()));
4645            }
4646            assert!(cursor.finished());
4647            ids.sort_unstable();
4648            assert_eq!(
4649                ids,
4650                if mode == 0 {
4651                    vec![10, 20, 30]
4652                } else {
4653                    vec![10, 30, 31]
4654                },
4655                "mode={mode}"
4656            );
4657        }
4658    }
4659
4660    fn growing_tombstones_fixture() -> SegmentManager {
4661        let manager = consolidating_manager(1);
4662        let ids = manager
4663            .bulk_insert(
4664                (0..64)
4665                    .map(|id| {
4666                        UnifiedEntity::table_row(
4667                            manager.next_entity_id(),
4668                            "users",
4669                            id,
4670                            vec![Value::Blob(vec![7; 2048])],
4671                        )
4672                    })
4673                    .collect(),
4674            )
4675            .expect("growing rows");
4676        for id in ids.iter().take(48) {
4677            manager.delete(*id).expect("physical tombstone");
4678        }
4679        assert_eq!(manager.stats().sealed_count, 0);
4680        manager
4681    }
4682
4683    #[test]
4684    fn pressure_consolidation_reclaims_unsealed_tombstones() {
4685        let manager = growing_tombstones_fixture();
4686        let before = manager.resident_bytes();
4687        assert!(manager.reclaimable_bytes() > 0);
4688        assert!(manager.pressure_consolidation_tick());
4689        drain_maintenance(&manager);
4690        assert_eq!(live_ids(&manager), (49..=64).collect::<Vec<_>>());
4691        assert!(manager.resident_bytes() < before);
4692        assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 48);
4693    }
4694
4695    #[test]
4696    fn pressure_consolidation_preserves_pinned_growing_source_accounting() {
4697        let manager = growing_tombstones_fixture();
4698        let source = Arc::clone(manager.growing.read().as_ref().expect("growing source"));
4699        let weak = Arc::downgrade(&source);
4700        assert!(manager.pressure_consolidation_tick());
4701        drain_maintenance(&manager);
4702        assert_eq!(live_ids(&manager), (49..=64).collect::<Vec<_>>());
4703        let source_bytes = source.read().resident_bytes();
4704        let held = manager.resident_bytes();
4705        assert!(held >= source_bytes);
4706        drop(source);
4707        assert!(weak.upgrade().is_none());
4708        assert_eq!(held - manager.resident_bytes(), source_bytes);
4709    }
4710
4711    #[test]
4712    fn pressure_consolidation_keeps_clean_and_disabled_growing_sources_open() {
4713        let clean = consolidating_manager(1);
4714        clean
4715            .insert(UnifiedEntity::table_row(
4716                clean.next_entity_id(),
4717                "users",
4718                1,
4719                vec![Value::Integer(1)],
4720            ))
4721            .expect("clean row");
4722        assert!(!clean.pressure_consolidation_tick());
4723        assert_eq!(clean.stats().sealed_count, 0);
4724        let mut disabled = growing_tombstones_fixture();
4725        disabled.config.enable_consolidation = false;
4726        assert!(!disabled.pressure_consolidation_tick());
4727        assert_eq!(disabled.stats().sealed_count, 0);
4728        assert_eq!(live_ids(&disabled), (49..=64).collect::<Vec<_>>());
4729    }
4730
4731    #[test]
4732    fn retained_memory_includes_an_inflight_consolidation_copy() {
4733        let manager = consolidating_manager(1);
4734        let ids = seal_bulk_segment(&manager, 100);
4735        for id in ids.iter().take(40) {
4736            manager.delete(*id).expect("delete");
4737        }
4738        let before = manager.resident_bytes();
4739        assert!(manager.pressure_consolidation_tick());
4740        let copied = manager
4741            .consolidation
4742            .read()
4743            .as_ref()
4744            .expect("paced merge")
4745            .merged
4746            .resident_bytes();
4747        assert!(copied > 0);
4748        assert!(
4749            manager.resident_bytes() >= before + copied,
4750            "unpublished copy consumes memory"
4751        );
4752        drain_maintenance(&manager);
4753        assert!(
4754            manager.resident_bytes() < before,
4755            "completed merge releases the old storage"
4756        );
4757    }
4758
4759    #[test]
4760    fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
4761        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4762        let ids = seal_bulk_segment(&manager, 100);
4763
4764        let bytes_before_delete = manager.resident_bytes();
4765
4766        // 40% tombstones — past CONSOLIDATION_TOMBSTONE_RATIO.
4767        for id in ids.iter().take(40) {
4768            assert!(manager.delete(*id).expect("delete"));
4769        }
4770
4771        // Tombstones cost memory: the payload stays resident in flat storage
4772        // and the tombstone set grows.
4773        let bytes_with_tombstones = manager.resident_bytes();
4774        assert!(bytes_with_tombstones > bytes_before_delete);
4775
4776        let survivors = live_ids(&manager);
4777        assert_eq!(survivors.len(), 60);
4778
4779        drain_maintenance(&manager);
4780
4781        let stats = manager.stats();
4782        assert_eq!(stats.consolidation.runs_started, 1);
4783        assert_eq!(stats.consolidation.runs_completed, 1);
4784        assert_eq!(stats.consolidation.segments_merged, 1);
4785        assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
4786        assert!(stats.consolidation.bytes_reclaimed > 0);
4787
4788        // The reclaimed memory is at least the tombstoned entities' share.
4789        let bytes_after = manager.resident_bytes();
4790        assert!(
4791            bytes_after <= bytes_before_delete,
4792            "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
4793        );
4794        assert_eq!(
4795            bytes_with_tombstones - bytes_after,
4796            stats.consolidation.bytes_reclaimed
4797        );
4798        assert_eq!(stats.total_memory_bytes, bytes_after as usize);
4799
4800        assert_eq!(live_ids(&manager), survivors);
4801        assert_eq!(manager.sealed.read().len(), 1);
4802        assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
4803    }
4804
4805    #[test]
4806    fn query_results_are_identical_before_during_and_after_consolidation() {
4807        let manager = consolidating_manager(3);
4808        let ids = seal_bulk_segment(&manager, 20);
4809        seal_bulk_segment(&manager, 20);
4810        for id in ids.iter().take(10) {
4811            assert!(manager.delete(*id).expect("delete"));
4812        }
4813
4814        let before = live_ids(&manager);
4815        let survivor = *ids.last().expect("at least one row");
4816        let tombstoned = ids[0];
4817
4818        // Every mid-tick observation matches the pre-consolidation snapshot:
4819        // point reads, full scans, and the kind index (rebuilt by the merge).
4820        let mut mid_tick_observations = 0;
4821        while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
4822            manager.run_maintenance().expect("maintenance");
4823            assert_eq!(live_ids(&manager), before);
4824            assert!(manager.get(survivor).is_some());
4825            assert!(manager.get(tombstoned).is_none());
4826            assert_eq!(manager.get_by_kind("table").len(), before.len());
4827            mid_tick_observations += 1;
4828            assert!(mid_tick_observations < 1_000);
4829        }
4830
4831        assert!(
4832            mid_tick_observations > 2,
4833            "expected the run to span several ticks, saw {mid_tick_observations}"
4834        );
4835        assert_eq!(live_ids(&manager), before);
4836        assert!(manager.get(survivor).is_some());
4837        assert!(manager.get(tombstoned).is_none());
4838        assert_eq!(manager.get_by_kind("table").len(), before.len());
4839    }
4840
4841    #[test]
4842    fn get_by_kind_reaches_sealed_segments() {
4843        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4844        let ids = seal_bulk_segment(&manager, 5);
4845        assert!(manager.delete(ids[0]).expect("delete"));
4846
4847        // The sealed list is walked through the segment read guard; the
4848        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` cannot hand out
4849        // references across the lock and yields nothing.
4850        assert_eq!(manager.get_by_kind("table").len(), 4);
4851        assert_eq!(manager.get_by_kind("vector").len(), 0);
4852    }
4853
4854    #[test]
4855    fn consolidation_is_paced_by_the_per_tick_entity_bound() {
4856        const PER_TICK: usize = 4;
4857        let manager = consolidating_manager(PER_TICK);
4858        let ids = seal_bulk_segment(&manager, 60);
4859        for id in ids.iter().take(30) {
4860            assert!(manager.delete(*id).expect("delete"));
4861        }
4862
4863        // First tick opens the run and copies exactly one bound's worth.
4864        manager.run_maintenance().expect("maintenance");
4865        assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
4866
4867        // Each subsequent tick advances by no more than the bound, and the run
4868        // is still in flight — it never completes in one unbounded pass.
4869        let mut previous = PER_TICK as u64;
4870        let mut ticks = 1;
4871        while let Some(copied) = manager.consolidation_progress() {
4872            assert!(
4873                copied - previous <= PER_TICK as u64,
4874                "tick copied {} entities, bound is {PER_TICK}",
4875                copied - previous
4876            );
4877            previous = copied;
4878            manager.run_maintenance().expect("maintenance");
4879            ticks += 1;
4880            assert!(ticks < 100);
4881        }
4882
4883        // 30 survivors at 4 per tick: the run cannot have finished before the
4884        // eighth tick.
4885        assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
4886        assert_eq!(manager.stats().consolidation.runs_completed, 1);
4887        assert_eq!(live_ids(&manager).len(), 30);
4888    }
4889
4890    #[test]
4891    fn sealed_segment_count_above_the_limit_triggers_a_merge() {
4892        let manager = SegmentManager::with_config(
4893            "test",
4894            ManagerConfig {
4895                max_sealed_segments: 3,
4896                ..Default::default()
4897            },
4898        );
4899        for _ in 0..4 {
4900            seal_bulk_segment(&manager, 5);
4901        }
4902        assert_eq!(manager.sealed.read().len(), 4);
4903
4904        let before = live_ids(&manager);
4905        drain_maintenance(&manager);
4906
4907        // The knob finally does what it claimed: the sealed set collapses even
4908        // though not a single row was deleted.
4909        assert_eq!(manager.sealed.read().len(), 1);
4910        assert_eq!(manager.stats().consolidation.segments_merged, 4);
4911        assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
4912        assert_eq!(live_ids(&manager), before);
4913    }
4914
4915    #[test]
4916    fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
4917        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
4918        seal_bulk_segment(&manager, 10);
4919        seal_bulk_segment(&manager, 10);
4920
4921        for _ in 0..5 {
4922            manager.run_maintenance().expect("maintenance");
4923        }
4924
4925        assert_eq!(manager.stats().consolidation.runs_started, 0);
4926        assert_eq!(manager.sealed.read().len(), 2);
4927    }
4928
4929    #[test]
4930    fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
4931        use std::sync::atomic::AtomicBool;
4932
4933        let manager = Arc::new(consolidating_manager(2));
4934        let ids = seal_bulk_segment(&manager, 40);
4935        seal_bulk_segment(&manager, 40);
4936        for id in ids.iter().take(20) {
4937            assert!(manager.delete(*id).expect("delete"));
4938        }
4939
4940        let expected = live_ids(&manager);
4941        let stop = Arc::new(AtomicBool::new(false));
4942
4943        let readers: Vec<_> = (0..4)
4944            .map(|_| {
4945                let manager = Arc::clone(&manager);
4946                let stop = Arc::clone(&stop);
4947                let expected = expected.clone();
4948                std::thread::spawn(move || {
4949                    let mut scans = 0u64;
4950                    // Do-while: every reader scans at least once even when the
4951                    // drain finishes before this thread is first scheduled.
4952                    loop {
4953                        let mut seen: Vec<u64> = manager
4954                            .query_all(|_| true)
4955                            .into_iter()
4956                            .map(|entity| entity.id.raw())
4957                            .collect();
4958                        seen.sort_unstable();
4959                        // Sorting is not enough: a duplicated row would keep
4960                        // the set equal but change the length.
4961                        assert_eq!(seen, expected, "reader saw a torn sealed set");
4962                        scans += 1;
4963                        if stop.load(Ordering::Relaxed) {
4964                            break;
4965                        }
4966                    }
4967                    scans
4968                })
4969            })
4970            .collect();
4971
4972        drain_maintenance(&manager);
4973        stop.store(true, Ordering::Relaxed);
4974
4975        for reader in readers {
4976            let scans = reader.join().expect("reader thread");
4977            assert!(scans > 0, "reader never got to scan");
4978        }
4979
4980        assert_eq!(manager.stats().consolidation.runs_completed, 1);
4981        assert_eq!(live_ids(&manager), expected);
4982    }
4983
4984    #[test]
4985    fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
4986        let manager = consolidating_manager(4);
4987        let ids = seal_bulk_segment(&manager, 20);
4988        for id in ids.iter().take(6) {
4989            assert!(manager.delete(*id).expect("delete"));
4990        }
4991
4992        // Copy the first bound's worth, then delete a row we just copied.
4993        manager.run_maintenance().expect("maintenance");
4994        assert_eq!(manager.consolidation_progress(), Some(4));
4995        let doomed = ids[6];
4996        assert!(manager.delete(doomed).expect("delete"));
4997
4998        let expected = live_ids(&manager);
4999        drain_maintenance(&manager);
5000
5001        assert!(manager.get(doomed).is_none(), "deleted row came back");
5002        assert_eq!(live_ids(&manager), expected);
5003    }
5004
5005    #[test]
5006    fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
5007        let manager = consolidating_manager(3);
5008        let ids = seal_bulk_segment(&manager, 30);
5009        for id in ids.iter().take(12) {
5010            assert!(manager.delete(*id).expect("delete"));
5011        }
5012
5013        let before = live_ids(&manager);
5014        let sealed_before: Vec<SegmentId> = manager
5015            .sealed
5016            .read()
5017            .iter()
5018            .map(|segment| segment.read().id())
5019            .collect();
5020
5021        manager.run_maintenance().expect("maintenance");
5022        manager.run_maintenance().expect("maintenance");
5023        assert!(manager.consolidation.read().is_some());
5024
5025        // A crash between ticks drops the half-built merged segment. Sealed
5026        // segments are the durable truth, so nothing was lost — and the run is
5027        // simply started over.
5028        *manager.consolidation.write() = None;
5029
5030        assert_eq!(live_ids(&manager), before);
5031        let sealed_after: Vec<SegmentId> = manager
5032            .sealed
5033            .read()
5034            .iter()
5035            .map(|segment| segment.read().id())
5036            .collect();
5037        assert_eq!(sealed_after, sealed_before);
5038        assert_eq!(manager.stats().consolidation.runs_completed, 0);
5039
5040        drain_maintenance(&manager);
5041        assert_eq!(live_ids(&manager), before);
5042        assert_eq!(manager.stats().consolidation.runs_completed, 1);
5043    }
5044
5045    #[test]
5046    fn consolidation_preserves_entity_payload_metadata_and_sequence() {
5047        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
5048
5049        // Metadata must be attached before the seal: `set_metadata` on a sealed
5050        // row rewrites it into the growing segment, which would take it out of
5051        // the segment under test.
5052        let entities: Vec<UnifiedEntity> = (0..10)
5053            .map(|i| {
5054                UnifiedEntity::table_row(
5055                    manager.next_entity_id(),
5056                    "users",
5057                    i as u64,
5058                    vec![Value::Integer(i as i64)],
5059                )
5060            })
5061            .collect();
5062        let ids = manager.bulk_insert(entities).expect("bulk insert");
5063
5064        let mut meta = Metadata::new();
5065        meta.set(
5066            "os",
5067            super::super::metadata::MetadataValue::String("linux".to_string()),
5068        );
5069        manager.set_metadata(ids[9], meta).expect("set metadata");
5070        manager.force_seal().expect("seal");
5071
5072        assert!(manager.delete(ids[0]).expect("delete"));
5073        assert!(manager.delete(ids[1]).expect("delete"));
5074        assert!(manager.delete(ids[2]).expect("delete"));
5075
5076        let before: Vec<UnifiedEntity> = {
5077            let mut entities = manager.query_all(|_| true);
5078            entities.sort_by_key(|entity| entity.id.raw());
5079            entities
5080        };
5081
5082        drain_maintenance(&manager);
5083
5084        let after: Vec<UnifiedEntity> = {
5085            let mut entities = manager.query_all(|_| true);
5086            entities.sort_by_key(|entity| entity.id.raw());
5087            entities
5088        };
5089
5090        let columns = |entity: &UnifiedEntity| match &entity.data {
5091            super::super::entity::EntityData::Row(row) => row.columns.clone(),
5092            _ => Vec::new(),
5093        };
5094
5095        assert_eq!(before.len(), after.len());
5096        for (old, new) in before.iter().zip(after.iter()) {
5097            assert_eq!(old.id, new.id);
5098            assert_eq!(columns(old), columns(new));
5099            assert_eq!(
5100                old.sequence_id, new.sequence_id,
5101                "merge must not re-issue sequence ids"
5102            );
5103        }
5104        assert!(manager
5105            .get_metadata(ids[9])
5106            .expect("metadata survives the merge")
5107            .has("os"));
5108    }
5109
5110    #[test]
5111    #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
5112    fn test_lifecycle_events() {
5113        let manager = SegmentManager::new("test");
5114
5115        manager
5116            .insert(UnifiedEntity::vector(
5117                manager.next_entity_id(),
5118                "v",
5119                vec![0.1],
5120            ))
5121            .unwrap();
5122
5123        let events = manager.drain_events();
5124
5125        // Should have: SegmentCreated, EntityInserted
5126        assert!(events
5127            .iter()
5128            .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
5129        assert!(events
5130            .iter()
5131            .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
5132    }
5133}