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