Skip to main content

reddb_server/storage/unified/
manager.rs

1//! Segment Manager
2//!
3//! Manages the lifecycle of unified segments: creation, sealing, consolidation,
4//! and archival. Coordinates writes to growing segments and queries across
5//! all segments.
6//!
7//! # Responsibilities
8//!
9//! - Route writes to the active growing segment
10//! - Auto-seal segments when thresholds are met
11//! - Coordinate queries across multiple segments
12//! - Paced consolidation of sealed segments (ADR 0073 §5)
13//! - Archive old segments to cold storage
14
15use parking_lot::RwLock;
16use std::collections::HashMap;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::Arc;
19
20use super::entity::{EntityId, UnifiedEntity};
21use super::metadata::{Metadata, MetadataFilter};
22use super::segment::{
23    GrowingSegment, SegmentConfig, SegmentError, SegmentId, SegmentState, SegmentStats,
24    UnifiedSegment, ZoneColPred, ZoneColPredKind,
25};
26use crate::storage::btree::visibility_map::VisibilityMap;
27
28/// Fraction of a collection's sealed entities that must be tombstoned before
29/// consolidation is worth its cost.
30///
31/// Tunable implementation constant, not a contract: it is surfaced read-only in
32/// `red.stats` so an operator can see what the engine decided, and it may move
33/// between releases without notice.
34pub const CONSOLIDATION_TOMBSTONE_RATIO: f64 = 0.20;
35
36/// Fraction of a collection's sealed bytes that consolidation would return to
37/// the budget (dead entity payloads plus the tombstone sets that bury them)
38/// before consolidation is worth its cost. See [`CONSOLIDATION_TOMBSTONE_RATIO`]
39/// on tunability.
40pub const CONSOLIDATION_FRAGMENTATION_RATIO: f64 = 0.30;
41
42/// Entities copied into the merged segment per maintenance tick.
43///
44/// ADR 0038 §3: consolidation is paced. A tick copies at most this many
45/// entities and returns; the half-built merged segment is carried to the next
46/// tick. No consolidation ever runs to completion in one unbounded pass.
47pub const CONSOLIDATION_ENTITIES_PER_TICK: usize = 4_096;
48
49/// Configuration for the segment manager
50#[derive(Debug, Clone)]
51pub struct ManagerConfig {
52    /// Segment configuration
53    pub segment_config: SegmentConfig,
54    /// Maximum number of sealed segments before consolidation
55    pub max_sealed_segments: usize,
56    /// Idle time (seconds) before auto-sealing
57    pub idle_seal_secs: u64,
58    /// Enable paced sealed-segment consolidation
59    pub enable_consolidation: bool,
60    /// Entities copied per maintenance tick while a consolidation is running
61    pub consolidation_entities_per_tick: usize,
62    /// Enable background archival
63    pub enable_archival: bool,
64    /// Age threshold for archival (seconds)
65    pub archive_age_secs: u64,
66}
67
68impl Default for ManagerConfig {
69    fn default() -> Self {
70        Self {
71            segment_config: SegmentConfig::default(),
72            max_sealed_segments: 10,
73            idle_seal_secs: 300, // 5 minutes
74            enable_consolidation: true,
75            consolidation_entities_per_tick: CONSOLIDATION_ENTITIES_PER_TICK,
76            enable_archival: true,
77            archive_age_secs: 86400 * 7, // 7 days
78        }
79    }
80}
81
82/// Consolidation counters for one collection (ADR 0073 §5).
83///
84/// These replace the `compact_ops` counter, which only ever counted a
85/// do-nothing branch.
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub struct ConsolidationStats {
88    /// Consolidations that crossed a threshold and began copying.
89    pub runs_started: u64,
90    /// Consolidations that finished the swap.
91    pub runs_completed: u64,
92    /// Source segments retired by a completed swap.
93    pub segments_merged: u64,
94    /// Tombstones garbage-collected by completed swaps.
95    pub tombstones_reclaimed: u64,
96    /// Bytes returned to the memory budget by completed swaps.
97    pub bytes_reclaimed: u64,
98}
99
100/// Manager statistics
101#[derive(Debug, Clone, Default)]
102pub struct ManagerStats {
103    /// Total entities across all segments
104    pub total_entities: usize,
105    /// Number of growing segments
106    pub growing_count: usize,
107    /// Number of sealed segments
108    pub sealed_count: usize,
109    /// Number of archived segments
110    pub archived_count: usize,
111    /// Total memory usage
112    pub total_memory_bytes: usize,
113    /// Number of seal operations
114    pub seal_ops: u64,
115    /// Consolidation counters
116    pub consolidation: ConsolidationStats,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
120pub struct SegmentScanStats {
121    pub segments_total: u64,
122    pub segments_scanned: u64,
123    pub segments_pruned: u64,
124}
125
126/// Lifecycle events for monitoring
127#[derive(Debug, Clone)]
128pub enum LifecycleEvent {
129    SegmentCreated(SegmentId),
130    SegmentSealed(SegmentId),
131    SegmentConsolidated {
132        source: Vec<SegmentId>,
133        target: SegmentId,
134    },
135    SegmentArchived(SegmentId),
136    EntityInserted(EntityId, SegmentId),
137    EntityDeleted(EntityId, SegmentId),
138}
139
140/// A consolidation in flight, carried across maintenance ticks.
141///
142/// The merged segment is built off to the side and is invisible to readers
143/// until the swap. Nothing here is durable: sealed segments are the truth (via
144/// the normal checkpoint path), so a crash mid-consolidation simply drops this
145/// state and the next boot starts over.
146struct Consolidation {
147    /// Source segments, frozen when the run started.
148    sources: Vec<SourceProgress>,
149    /// Id reserved for the merged segment.
150    merged_id: SegmentId,
151    /// The half-built merged segment.
152    merged: GrowingSegment,
153    /// Index into `sources` of the segment currently being copied.
154    cursor: usize,
155    /// Live ids of `sources[cursor]`, snapshotted when that source was opened.
156    pending_ids: Vec<EntityId>,
157    /// How many of `pending_ids` have been consumed.
158    pending_cursor: usize,
159    /// Entities copied so far, across every tick of this run.
160    entities_copied: u64,
161}
162
163/// Per-source bookkeeping for a consolidation in flight.
164struct SourceProgress {
165    id: SegmentId,
166    /// Ids copied out of this source into the merged segment.
167    copied: Vec<EntityId>,
168    /// The source's tombstone count when the run opened it, and `None` before
169    /// that. Tombstones only ever grow, so a source whose count is unchanged at
170    /// swap time saw no deletes after we started reading it — and every row we
171    /// copied out of it is still live. Anything else needs a per-row check.
172    ///
173    /// Recording this when the source is *opened*, not when it is drained, is
174    /// what makes the check sound: a row deleted between two copy ticks would
175    /// otherwise be invisible to the swap and get resurrected.
176    tombstones_at_open: Option<usize>,
177}
178
179/// Segment manager for a collection
180pub struct SegmentManager {
181    /// Collection name
182    collection: String,
183    /// Configuration
184    config: ManagerConfig,
185    /// Next segment ID counter
186    next_segment_id: AtomicU64,
187    /// Next entity ID counter
188    next_entity_id: AtomicU64,
189    /// Per-table auto-increment row ID (1, 2, 3... per collection)
190    next_row_id: AtomicU64,
191    /// Hot-path entity counter — lock-free, updated by every insert/delete.
192    /// Replaces stats.total_entities on the write path to eliminate a lock
193    /// acquisition per row (from 4 lock ops per insert → 2).
194    total_entities_atomic: AtomicU64,
195    /// Currently active growing segment
196    growing: RwLock<Option<Arc<RwLock<GrowingSegment>>>>,
197    /// Sealed segments (immutable, queryable)
198    sealed: RwLock<Vec<Arc<RwLock<GrowingSegment>>>>,
199    /// Archived segment IDs (stored externally)
200    archived: RwLock<Vec<SegmentId>>,
201    /// Entity to segment mapping (for fast lookups by individually-inserted entities).
202    /// Bulk-inserted entities skip this map; their segment is found by sequential scan
203    /// of growing + sealed segments in get()/update()/delete().
204    entity_segment: RwLock<HashMap<EntityId, SegmentId>>,
205    /// Shared column schema: column_name → index in Vec<Value>.
206    /// Populated on first bulk_insert. Enables columnar storage (Vec instead of HashMap per row).
207    column_schema: RwLock<Option<Arc<Vec<String>>>>,
208    /// Statistics (slow path — not updated on every insert).
209    stats: RwLock<ManagerStats>,
210    /// Consolidation in flight, if any. Only `run_maintenance` touches it, so
211    /// at most one consolidation runs per collection at a time.
212    consolidation: RwLock<Option<Consolidation>>,
213    /// Event listeners (simplified - would be channels in production)
214    events: RwLock<Vec<LifecycleEvent>>,
215    /// Visibility map: sealed segment entity ranges marked as all-visible.
216    /// Growing segment is never all-visible (writes are in-flight).
217    /// Used by index-only scan decisions.
218    visibility_map: VisibilityMap,
219}
220
221impl SegmentManager {
222    /// Create a new segment manager
223    pub fn new(collection: impl Into<String>) -> Self {
224        Self::with_config(collection, ManagerConfig::default())
225    }
226
227    /// Create with custom configuration
228    pub fn with_config(collection: impl Into<String>, config: ManagerConfig) -> Self {
229        Self {
230            collection: collection.into(),
231            config,
232            next_segment_id: AtomicU64::new(1),
233            next_entity_id: AtomicU64::new(1),
234            next_row_id: AtomicU64::new(1),
235            total_entities_atomic: AtomicU64::new(0),
236            growing: RwLock::new(None),
237            sealed: RwLock::new(Vec::new()),
238            archived: RwLock::new(Vec::new()),
239            entity_segment: RwLock::new(HashMap::new()),
240            column_schema: RwLock::new(None),
241            stats: RwLock::new(ManagerStats::default()),
242            consolidation: RwLock::new(None),
243            events: RwLock::new(Vec::new()),
244            visibility_map: VisibilityMap::new(),
245        }
246    }
247
248    /// Get or create the shared column schema from first row's named fields.
249    pub fn get_or_init_schema(
250        &self,
251        named: &HashMap<String, crate::storage::schema::Value>,
252    ) -> Arc<Vec<String>> {
253        {
254            let schema = self.column_schema.read();
255            if let Some(ref s) = *schema {
256                return Arc::clone(s);
257            }
258        }
259        let cols: Vec<String> = named.keys().cloned().collect();
260        let arc = Arc::new(cols);
261        *self.column_schema.write() = Some(Arc::clone(&arc));
262        arc
263    }
264
265    /// Get the column schema if it exists.
266    pub fn column_schema(&self) -> Option<Arc<Vec<String>>> {
267        self.column_schema.read().clone()
268    }
269
270    pub(crate) fn set_column_schema_if_empty(&self, columns: Vec<String>) {
271        if columns.is_empty() {
272            return;
273        }
274        let mut schema = self.column_schema.write();
275        if schema.is_none() {
276            *schema = Some(Arc::new(columns));
277        }
278    }
279
280    /// Get collection name
281    pub fn collection(&self) -> &str {
282        &self.collection
283    }
284
285    /// Get configuration
286    pub fn config(&self) -> &ManagerConfig {
287        &self.config
288    }
289
290    /// Get statistics. total_entities is read from the lock-free atomic;
291    /// `total_memory_bytes` is summed across the live segments so that a
292    /// consolidation's reclamation is visible in the number the budget watches.
293    /// The remaining fields come from the slow-path stats struct.
294    pub fn stats(&self) -> ManagerStats {
295        let mut s = self.stats.read().clone();
296        s.total_entities = self.total_entities_atomic.load(Ordering::Relaxed) as usize;
297        s.total_memory_bytes = self.resident_bytes() as usize;
298        s
299    }
300
301    /// Approximate bytes held by this collection's in-memory segments: resident
302    /// entity payloads plus the tombstone sets. This is the number consolidation
303    /// drives down.
304    pub fn resident_bytes(&self) -> u64 {
305        let mut bytes = 0;
306        if let Some(growing_arc) = self.growing.read().as_ref() {
307            bytes += growing_arc.read().resident_bytes();
308        }
309        for segment in self.sealed.read().iter() {
310            bytes += segment.read().resident_bytes();
311        }
312        bytes
313    }
314
315    /// Approximate resident *payload* bytes across this collection's growing
316    /// and sealed segments — the arena's contribution to the shared accounting
317    /// pool (ADR 0073 §2). Unlike [`Self::resident_bytes`] this excludes the
318    /// tombstone sets: the pool tracks payload memory, not reclaim potential.
319    ///
320    /// One relaxed load per segment under a read lock; no entity is touched.
321    pub fn memory_bytes(&self) -> u64 {
322        let growing = self
323            .growing
324            .read()
325            .as_ref()
326            .map_or(0, |segment| segment.read().memory_bytes());
327
328        self.sealed
329            .read()
330            .iter()
331            .map(|segment| segment.read().memory_bytes())
332            .fold(growing, u64::saturating_add)
333    }
334
335    /// Bytes a pressure-triggered consolidation could plausibly return.
336    pub fn reclaimable_bytes(&self) -> u64 {
337        self.sealed
338            .read()
339            .iter()
340            .map(|segment| segment.read().reclaimable_bytes())
341            .fold(0, u64::saturating_add)
342    }
343
344    /// Generate a new entity ID
345    pub fn next_entity_id(&self) -> EntityId {
346        EntityId::new(self.next_entity_id.fetch_add(1, Ordering::SeqCst))
347    }
348
349    /// Generate a per-table sequential row ID (1, 2, 3... per collection)
350    pub fn next_row_id(&self) -> u64 {
351        self.next_row_id.fetch_add(1, Ordering::SeqCst)
352    }
353
354    /// Reserve `n` contiguous per-table row IDs with one atomic
355    /// fetch_add. Caller assigns `row_id = start + i` per entity.
356    /// Saves N-1 atomic RMWs on bulk inserts (25k atomics → 1).
357    pub fn reserve_row_ids(&self, n: u64) -> std::ops::Range<u64> {
358        let start = self.next_row_id.fetch_add(n, Ordering::SeqCst);
359        start..start + n
360    }
361
362    /// Advance the per-table row_id counter to at least `id + 1`.
363    /// Called during load to restore the counter from existing data.
364    pub fn register_row_id(&self, id: u64) {
365        let candidate = id.saturating_add(1);
366        let mut current = self.next_row_id.load(Ordering::SeqCst);
367        while candidate > current {
368            match self.next_row_id.compare_exchange(
369                current,
370                candidate,
371                Ordering::SeqCst,
372                Ordering::SeqCst,
373            ) {
374                Ok(_) => break,
375                Err(updated) => current = updated,
376            }
377        }
378    }
379
380    /// Get or create the active growing segment.
381    ///
382    /// Fast path: read lock only — no write contention when the segment already exists.
383    /// Concurrent writers each clone the `Arc` under a shared read lock, then compete
384    /// on the segment's own write lock. This eliminates the global write-lock serialisation
385    /// that previously throttled concurrent inserts to ~238 ops/s.
386    fn get_or_create_growing(&self) -> Arc<RwLock<GrowingSegment>> {
387        // Common case: segment already exists — shared read lock, zero contention.
388        {
389            let growing = self.growing.read();
390            if let Some(segment) = growing.as_ref() {
391                return Arc::clone(segment);
392            }
393        }
394
395        // Slow path: segment missing — take exclusive write lock to create it.
396        let mut growing = self.growing.write();
397        // Double-check: another thread may have created it between the two lock acquisitions.
398        if let Some(segment) = growing.as_ref() {
399            return Arc::clone(segment);
400        }
401
402        let id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
403        let segment = GrowingSegment::new(id, &self.collection);
404        let segment_arc = Arc::new(RwLock::new(segment));
405        *growing = Some(Arc::clone(&segment_arc));
406
407        self.emit(LifecycleEvent::SegmentCreated(id));
408
409        // Update growing_count in the slow-path stats struct.
410        // This is the rare segment-creation path — locking is fine here.
411        self.stats.write().growing_count += 1;
412
413        segment_arc
414    }
415
416    /// Insert a new entity
417    pub fn insert(&self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
418        // Check if we need to seal the current segment first
419        self.maybe_seal_growing()?;
420
421        let segment_arc = self.get_or_create_growing();
422        let mut segment = segment_arc.write();
423
424        // Assign entity ID if not already set
425        if entity.id.raw() == 0 {
426            entity.id = self.next_entity_id();
427        }
428
429        let entity_id = entity.id;
430        let segment_id = segment.id();
431
432        segment.insert(entity)?;
433
434        // Lock-free counter update — eliminates the stats write lock on the hot path.
435        self.total_entities_atomic.fetch_add(1, Ordering::Relaxed);
436
437        // entity_segment map is intentionally NOT updated here.
438        // update() and update_hot() first probe the growing segment directly
439        // (growing.contains(entity.id)) before consulting this map, so entities
440        // that were just inserted are found without entity_segment. The map is
441        // only consulted for entities that may have been moved to sealed segments,
442        // which can't be updated anyway (state().is_writable() == false).
443        // Skipping this write removes one exclusive HashMap lock per insert.
444
445        self.emit(LifecycleEvent::EntityInserted(entity_id, segment_id));
446
447        Ok(entity_id)
448    }
449
450    /// Insert multiple entities (batch) — sequential, one lock per item.
451    pub fn insert_batch(
452        &self,
453        entities: Vec<UnifiedEntity>,
454    ) -> Result<Vec<EntityId>, SegmentError> {
455        let mut ids = Vec::with_capacity(entities.len());
456        for entity in entities {
457            ids.push(self.insert(entity)?);
458        }
459        Ok(ids)
460    }
461
462    /// Turbo bulk insert — single lock acquisition for the entire batch.
463    /// Skips bloom filter and cross-ref indexing for maximum speed.
464    pub fn bulk_insert(
465        &self,
466        mut entities: Vec<UnifiedEntity>,
467    ) -> Result<Vec<EntityId>, SegmentError> {
468        // Assign IDs and per-table row_ids.
469        for entity in &mut entities {
470            if entity.id.raw() == 0 {
471                entity.id = self.next_entity_id();
472            }
473            if let super::entity::EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
474                if *row_id == 0 {
475                    *row_id = self.next_row_id();
476                } else {
477                    self.register_row_id(*row_id);
478                }
479            }
480        }
481
482        // Convert named HashMap → positional Vec (compact memory representation)
483        // The schema (column order) is shared across all rows in the collection.
484        if let Some(first_row) = entities.first() {
485            if let super::entity::EntityData::Row(ref row) = first_row.data {
486                if let Some(ref named) = row.named {
487                    let schema = self.get_or_init_schema(named);
488                    for entity in &mut entities {
489                        if let super::entity::EntityData::Row(ref mut row) = entity.data {
490                            if let Some(named) = row.named.take() {
491                                let mut cols = Vec::with_capacity(schema.len());
492                                for col_name in schema.iter() {
493                                    cols.push(
494                                        named
495                                            .get(col_name)
496                                            .cloned()
497                                            .unwrap_or(crate::storage::schema::Value::Null),
498                                    );
499                                }
500                                row.columns = cols;
501                                row.schema = Some(Arc::clone(&schema));
502                            }
503                        }
504                    }
505                }
506            }
507        }
508
509        let segment_arc = self.get_or_create_growing();
510        let mut segment = segment_arc.write();
511        let segment_id = segment.id();
512
513        // Single call to GrowingSegment.bulk_insert (one lock, no bloom/cross-refs)
514        let ids = segment.bulk_insert(entities)?;
515
516        // Skip entity-segment mapping for bulk inserts (saves ~56 bytes/entity).
517        // The get() method scans growing+sealed segments directly.
518
519        // Lock-free batch counter update.
520        self.total_entities_atomic
521            .fetch_add(ids.len() as u64, Ordering::Relaxed);
522
523        Ok(ids)
524    }
525
526    /// Get an entity by ID — scans growing then sealed segments.
527    pub fn get(&self, id: EntityId) -> Option<UnifiedEntity> {
528        // Growing segment first (most likely for recent inserts)
529        if let Some(growing_arc) = self.growing.read().as_ref() {
530            let growing = growing_arc.read();
531            if let Some(entity) = growing.get(id) {
532                return Some(entity.clone());
533            }
534        }
535
536        // Then sealed segments
537        let sealed = self.sealed.read();
538        for segment in sealed.iter() {
539            let seg = segment.read();
540            if let Some(entity) = seg.get(id) {
541                return Some(entity.clone());
542            }
543        }
544
545        None
546    }
547
548    /// Batch-fetch multiple entities by ID in a single lock acquisition per segment.
549    ///
550    /// For indexed-scan result sets (up to ~5000 ids from range/bitmap lookup) this
551    /// is 2-3 lock acquisitions total vs N×3 with individual `get()` calls.
552    pub fn get_many(&self, ids: &[EntityId]) -> Vec<Option<UnifiedEntity>> {
553        let mut out: Vec<Option<UnifiedEntity>> = vec![None; ids.len()];
554        let mut remaining: Vec<usize> = (0..ids.len()).collect(); // indices still unfound
555
556        // Growing segment — one read lock for the entire batch.
557        // Non-blocking first: if a writer is active, fall back to blocking.
558        if let Some(growing_arc) = self.growing.read().as_ref() {
559            let growing = if let Some(g) = growing_arc.try_read() {
560                g
561            } else {
562                growing_arc.read()
563            };
564            remaining.retain(|&i| {
565                if let Some(entity) = growing.get(ids[i]) {
566                    out[i] = Some(entity.clone());
567                    false // remove from remaining
568                } else {
569                    true // keep — not found yet
570                }
571            });
572        }
573
574        if remaining.is_empty() {
575            return out;
576        }
577
578        // Sealed segments — one read lock per segment
579        let sealed = self.sealed.read();
580        for segment in sealed.iter() {
581            if remaining.is_empty() {
582                break;
583            }
584            let seg = segment.read();
585            remaining.retain(|&i| {
586                if let Some(entity) = seg.get(ids[i]) {
587                    out[i] = Some(entity.clone());
588                    false
589                } else {
590                    true
591                }
592            });
593        }
594
595        out
596    }
597
598    /// Visitor-pattern batch fetch. Invokes `f(&UnifiedEntity, usize_index)`
599    /// for each id that resolves, never cloning the entity.
600    ///
601    /// Used by scan hot paths (select_range, select_filtered) that
602    /// materialize each entity into an output record and don't need
603    /// an owned `UnifiedEntity`. Eliminates ~20% of scan CPU spent in
604    /// `UnifiedEntity::clone` when `get_batch` is followed by
605    /// `runtime_table_record_lean(entity)`.
606    ///
607    /// The closure runs while the segment read lock is held, so it
608    /// must be short — avoid doing I/O or taking unrelated locks in
609    /// `f`.
610    pub fn for_each_id<F>(&self, ids: &[EntityId], mut f: F)
611    where
612        F: FnMut(usize, &UnifiedEntity),
613    {
614        // Thread-local scratch buffer for the "pending" index list.
615        // Previous code allocated a fresh `Vec<usize>` of capacity
616        // N on every call — 4200 × 1000 queries / scenario on the
617        // select_range bench path. Take-and-restore pattern (vs
618        // RefCell::borrow_mut) so user closures that recurse into
619        // another `for_each_id` don't panic on a re-borrow; worst
620        // case they allocate a fresh buffer and we lose the caching
621        // win for that nested call.
622        thread_local! {
623            static REMAINING_SCRATCH: std::cell::Cell<Vec<usize>> =
624                const { std::cell::Cell::new(Vec::new()) };
625        }
626
627        let mut remaining: Vec<usize> = REMAINING_SCRATCH.with(|cell| cell.take());
628        remaining.clear();
629
630        if let Some(growing_arc) = self.growing.read().as_ref() {
631            let growing = if let Some(g) = growing_arc.try_read() {
632                g
633            } else {
634                growing_arc.read()
635            };
636            for (i, id) in ids.iter().enumerate() {
637                if let Some(entity) = growing.get(*id) {
638                    f(i, entity);
639                } else {
640                    remaining.push(i);
641                }
642            }
643        } else {
644            remaining.reserve(ids.len());
645            remaining.extend(0..ids.len());
646        }
647
648        if !remaining.is_empty() {
649            let sealed = self.sealed.read();
650            for segment in sealed.iter() {
651                if remaining.is_empty() {
652                    break;
653                }
654                let seg = segment.read();
655                remaining.retain(|&i| {
656                    if let Some(entity) = seg.get(ids[i]) {
657                        f(i, entity);
658                        false
659                    } else {
660                        true
661                    }
662                });
663            }
664        }
665
666        REMAINING_SCRATCH.with(|cell| cell.set(remaining));
667    }
668
669    /// Scan all segments for an entity
670    fn scan_for_entity(&self, id: EntityId) -> Option<UnifiedEntity> {
671        // Check growing
672        if let Some(growing_arc) = self.growing.read().as_ref() {
673            let growing = growing_arc.read();
674            if let Some(entity) = growing.get(id) {
675                return Some(entity.clone());
676            }
677        }
678
679        // Check sealed
680        let sealed = self.sealed.read();
681        for segment in sealed.iter() {
682            if let Some(entity) = segment.get(id) {
683                return Some(entity.clone());
684            }
685        }
686
687        None
688    }
689
690    fn find_sealed_segment_arc(&self, id: EntityId) -> Option<Arc<RwLock<GrowingSegment>>> {
691        let sealed = self.sealed.read();
692        sealed
693            .iter()
694            .find(|segment_arc| segment_arc.read().contains(id))
695            .map(Arc::clone)
696    }
697
698    fn rewrite_sealed_entity_into_growing(
699        &self,
700        entity: UnifiedEntity,
701        metadata: Option<&Metadata>,
702    ) -> Result<(), SegmentError> {
703        let entity_id = entity.id;
704        let sealed_arc = self
705            .find_sealed_segment_arc(entity_id)
706            .ok_or(SegmentError::NotFound(entity_id))?;
707
708        let metadata_to_apply = {
709            let mut sealed = sealed_arc.write();
710            let existing_metadata = sealed.get_metadata(entity_id);
711            if !sealed.force_delete(entity_id) {
712                return Err(SegmentError::NotFound(entity_id));
713            }
714            metadata.cloned().or(existing_metadata)
715        };
716
717        let growing_arc = self.get_or_create_growing();
718        let growing_id = {
719            let mut growing = growing_arc.write();
720            growing.insert(entity)?;
721            if let Some(metadata) = metadata_to_apply {
722                growing.set_metadata(entity_id, metadata)?;
723            }
724            growing.id()
725        };
726
727        self.entity_segment.write().insert(entity_id, growing_id);
728        Ok(())
729    }
730
731    /// Update an entity
732    pub fn update(&self, entity: UnifiedEntity) -> Result<(), SegmentError> {
733        let entity_id = entity.id;
734        let mut entity = Some(entity);
735
736        // Try growing segment directly (covers bulk-inserted entities without entity_segment map)
737        if let Some(growing_arc) = self.growing.read().as_ref() {
738            let mut growing = growing_arc.write();
739            if growing.contains(entity_id) && growing.state().is_writable() {
740                return growing.update(entity.take().expect("entity already moved"));
741            }
742        }
743
744        // Try entity_segment mapping for individually inserted entities
745        let segment_id = self.entity_segment.read().get(&entity_id).copied();
746        if let Some(seg_id) = segment_id {
747            if let Some(growing_arc) = self.growing.read().as_ref() {
748                let mut growing = growing_arc.write();
749                if growing.id() == seg_id && growing.state().is_writable() {
750                    return growing.update(entity.take().expect("entity already moved"));
751                }
752            }
753        }
754
755        if let Some(entity) = entity.take() {
756            return self.rewrite_sealed_entity_into_growing(entity, None);
757        }
758
759        Err(SegmentError::NotFound(entity_id))
760    }
761
762    /// Update an entity and, optionally, replace its metadata while holding
763    /// the segment write lock only once.
764    pub fn update_with_metadata(
765        &self,
766        entity: UnifiedEntity,
767        metadata: Option<&Metadata>,
768    ) -> Result<(), SegmentError> {
769        let entity_id = entity.id;
770        let mut entity = Some(entity);
771
772        // Try growing segment directly (covers bulk-inserted entities without entity_segment map)
773        if let Some(growing_arc) = self.growing.read().as_ref() {
774            let mut growing = growing_arc.write();
775            if growing.contains(entity_id) && growing.state().is_writable() {
776                growing.update(entity.take().expect("entity already moved"))?;
777                if let Some(metadata) = metadata {
778                    growing.set_metadata(entity_id, metadata.clone())?;
779                }
780                return Ok(());
781            }
782        }
783
784        // Try entity_segment mapping for individually inserted entities
785        let segment_id = self.entity_segment.read().get(&entity_id).copied();
786        if let Some(seg_id) = segment_id {
787            if let Some(growing_arc) = self.growing.read().as_ref() {
788                let mut growing = growing_arc.write();
789                if growing.id() == seg_id && growing.state().is_writable() {
790                    growing.update(entity.take().expect("entity already moved"))?;
791                    if let Some(metadata) = metadata {
792                        growing.set_metadata(entity_id, metadata.clone())?;
793                    }
794                    return Ok(());
795                }
796            }
797        }
798
799        if let Some(entity) = entity.take() {
800            return self.rewrite_sealed_entity_into_growing(entity, metadata);
801        }
802
803        Err(SegmentError::NotFound(entity_id))
804    }
805
806    /// HOT-update: like update but skips index work for unchanged columns.
807    /// `modified_columns` is the list of column names actually changed by the
808    /// UPDATE statement — lets us skip pk_index and cross_ref when safe.
809    pub fn update_hot(
810        &self,
811        entity: UnifiedEntity,
812        modified_columns: &[String],
813    ) -> Result<(), SegmentError> {
814        let entity_id = entity.id;
815        let mut entity = Some(entity);
816
817        if let Some(growing_arc) = self.growing.read().as_ref() {
818            let mut growing = growing_arc.write();
819            if growing.contains(entity_id) && growing.state().is_writable() {
820                return growing.update_hot(
821                    entity.take().expect("entity already moved"),
822                    modified_columns,
823                );
824            }
825        }
826
827        let segment_id = self.entity_segment.read().get(&entity_id).copied();
828        if let Some(seg_id) = segment_id {
829            if let Some(growing_arc) = self.growing.read().as_ref() {
830                let mut growing = growing_arc.write();
831                if growing.id() == seg_id && growing.state().is_writable() {
832                    return growing.update_hot(
833                        entity.take().expect("entity already moved"),
834                        modified_columns,
835                    );
836                }
837            }
838        }
839
840        if let Some(entity) = entity.take() {
841            return self.rewrite_sealed_entity_into_growing(entity, None);
842        }
843
844        Err(SegmentError::NotFound(entity_id))
845    }
846
847    /// HOT-update an entity and, optionally, replace its metadata while
848    /// holding the segment write lock only once.
849    pub fn update_hot_with_metadata(
850        &self,
851        entity: UnifiedEntity,
852        modified_columns: &[String],
853        metadata: Option<&Metadata>,
854    ) -> Result<(), SegmentError> {
855        let entity_id = entity.id;
856        let mut entity = Some(entity);
857
858        if let Some(growing_arc) = self.growing.read().as_ref() {
859            let mut growing = growing_arc.write();
860            if growing.contains(entity_id) && growing.state().is_writable() {
861                growing.update_hot(
862                    entity.take().expect("entity already moved"),
863                    modified_columns,
864                )?;
865                if let Some(metadata) = metadata {
866                    growing.set_metadata(entity_id, metadata.clone())?;
867                }
868                return Ok(());
869            }
870        }
871
872        let segment_id = self.entity_segment.read().get(&entity_id).copied();
873        if let Some(seg_id) = segment_id {
874            if let Some(growing_arc) = self.growing.read().as_ref() {
875                let mut growing = growing_arc.write();
876                if growing.id() == seg_id && growing.state().is_writable() {
877                    growing.update_hot(
878                        entity.take().expect("entity already moved"),
879                        modified_columns,
880                    )?;
881                    if let Some(metadata) = metadata {
882                        growing.set_metadata(entity_id, metadata.clone())?;
883                    }
884                    return Ok(());
885                }
886            }
887        }
888
889        if let Some(entity) = entity.take() {
890            return self.rewrite_sealed_entity_into_growing(entity, metadata);
891        }
892
893        Err(SegmentError::NotFound(entity_id))
894    }
895
896    /// Batch HOT-update multiple entities while holding the growing-segment
897    /// write lock only once when possible.
898    pub fn update_hot_batch_with_metadata<'a, I>(&self, items: I) -> Result<(), SegmentError>
899    where
900        I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
901    {
902        let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
903            items.into_iter().collect();
904        if items.is_empty() {
905            return Ok(());
906        }
907
908        if let Some(growing_arc) = self.growing.read().as_ref() {
909            let mut growing = growing_arc.write();
910            if growing.state().is_writable() {
911                match growing.update_hot_batch_with_metadata(items.iter().copied()) {
912                    Ok(()) => return Ok(()),
913                    Err(SegmentError::NotFound(_)) => {}
914                    Err(other) => return Err(other),
915                }
916            }
917        }
918
919        for (entity, modified_columns, metadata) in items {
920            self.update_hot_with_metadata(entity.clone(), modified_columns, metadata)?;
921        }
922        Ok(())
923    }
924
925    /// Delete an entity
926    pub fn delete(&self, id: EntityId) -> Result<bool, SegmentError> {
927        // Fast path: probe the growing segment directly — covers entities inserted via
928        // insert() which no longer writes to entity_segment, and bulk-inserted entities.
929        if let Some(growing_arc) = self.growing.read().as_ref() {
930            let mut growing = growing_arc.write();
931            if growing.contains(id) && growing.state().is_writable() {
932                let seg_id = growing.id();
933                let deleted = growing.delete(id)?;
934                if deleted {
935                    self.entity_segment.write().remove(&id);
936                    self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
937                    self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
938                }
939                return Ok(deleted);
940            }
941        }
942
943        // Fallback: check entity_segment map (populated for older insert() paths
944        // or entities that were in a previous growing segment).
945        let segment_id = self.entity_segment.read().get(&id).copied();
946
947        if let Some(seg_id) = segment_id {
948            if let Some(growing_arc) = self.growing.read().as_ref() {
949                let mut growing = growing_arc.write();
950                if growing.id() == seg_id && growing.state().is_writable() {
951                    let deleted = growing.delete(id)?;
952                    if deleted {
953                        self.entity_segment.write().remove(&id);
954                        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
955                        self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
956                    }
957                    return Ok(deleted);
958                }
959            }
960        }
961
962        // Fallback: entity is in a sealed segment (bulk-inserted, not in entity_segment map).
963        // Single write-lock per segment to avoid TOCTOU race between contains() and force_delete().
964        {
965            let sealed = self.sealed.read();
966            for segment_arc in sealed.iter() {
967                let mut seg = segment_arc.write();
968                let seg_id = seg.id();
969                if seg.contains(id) {
970                    let deleted = seg.force_delete(id);
971                    drop(seg);
972                    if deleted {
973                        self.entity_segment.write().remove(&id);
974                        self.total_entities_atomic.fetch_sub(1, Ordering::Relaxed);
975                        self.emit(LifecycleEvent::EntityDeleted(id, seg_id));
976                    }
977                    return Ok(deleted);
978                }
979            }
980        }
981
982        Ok(false)
983    }
984
985    pub fn delete_batch(&self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
986        if ids.is_empty() {
987            return Ok(Vec::new());
988        }
989
990        let mut deleted_ids = Vec::with_capacity(ids.len());
991
992        if let Some(growing_arc) = self.growing.read().as_ref() {
993            let mut growing = growing_arc.write();
994            if growing.state().is_writable() {
995                let seg_id = growing.id();
996                let deleted = growing.delete_batch(ids)?;
997                if !deleted.is_empty() {
998                    {
999                        let mut entity_segment = self.entity_segment.write();
1000                        for id in &deleted {
1001                            entity_segment.remove(id);
1002                        }
1003                    }
1004                    self.total_entities_atomic
1005                        .fetch_sub(deleted.len() as u64, Ordering::Relaxed);
1006                    for id in &deleted {
1007                        self.emit(LifecycleEvent::EntityDeleted(*id, seg_id));
1008                    }
1009                    deleted_ids.extend(deleted);
1010                }
1011            }
1012        }
1013
1014        if deleted_ids.len() == ids.len() {
1015            return Ok(deleted_ids);
1016        }
1017
1018        let deleted_set: std::collections::HashSet<EntityId> =
1019            deleted_ids.iter().copied().collect();
1020        for &id in ids {
1021            if deleted_set.contains(&id) {
1022                continue;
1023            }
1024            if self.delete(id)? {
1025                deleted_ids.push(id);
1026            }
1027        }
1028
1029        Ok(deleted_ids)
1030    }
1031
1032    /// Get metadata for an entity
1033    pub fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1034        // Fast path: probe growing segment directly (no entity_segment needed).
1035        if let Some(growing_arc) = self.growing.read().as_ref() {
1036            let growing = growing_arc.read();
1037            if growing.contains(id) {
1038                return growing.get_metadata(id);
1039            }
1040        }
1041
1042        // Fallback: entity_segment map (for pre-existing or sealed entities)
1043        let segment_id = self.entity_segment.read().get(&id).copied();
1044
1045        if let Some(seg_id) = segment_id {
1046            if let Some(growing_arc) = self.growing.read().as_ref() {
1047                let growing = growing_arc.read();
1048                if growing.id() == seg_id {
1049                    return growing.get_metadata(id);
1050                }
1051            }
1052
1053            let sealed = self.sealed.read();
1054            for segment in sealed.iter() {
1055                if segment.id() == seg_id {
1056                    return segment.get_metadata(id);
1057                }
1058            }
1059        }
1060
1061        if let Some(segment_arc) = self.find_sealed_segment_arc(id) {
1062            return segment_arc.read().get_metadata(id);
1063        }
1064
1065        None
1066    }
1067
1068    /// Set metadata for an entity
1069    pub fn set_metadata(&self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1070        // Fast path: probe growing segment directly — covers entities inserted via
1071        // insert() which no longer writes to entity_segment.
1072        if let Some(growing_arc) = self.growing.read().as_ref() {
1073            let mut growing = growing_arc.write();
1074            if growing.contains(id) && growing.state().is_writable() {
1075                return growing.set_metadata(id, metadata);
1076            }
1077        }
1078
1079        // Fallback: entity_segment map (sealed or pre-atomic-path entities)
1080        let segment_id = self.entity_segment.read().get(&id).copied();
1081
1082        if let Some(seg_id) = segment_id {
1083            if let Some(growing_arc) = self.growing.read().as_ref() {
1084                let mut growing = growing_arc.write();
1085                if growing.id() == seg_id && growing.state().is_writable() {
1086                    return growing.set_metadata(id, metadata);
1087                }
1088            }
1089        }
1090
1091        if let Some(entity) = self.get(id) {
1092            return self.rewrite_sealed_entity_into_growing(entity, Some(&metadata));
1093        }
1094
1095        Err(SegmentError::NotFound(id))
1096    }
1097
1098    /// Check if growing segment should be sealed
1099    fn maybe_seal_growing(&self) -> Result<(), SegmentError> {
1100        let should_seal = {
1101            let growing_opt = self.growing.read();
1102            if let Some(growing_arc) = growing_opt.as_ref() {
1103                let growing = growing_arc.read();
1104                growing.should_seal(&self.config.segment_config)
1105                    || growing.idle_secs() >= self.config.idle_seal_secs
1106            } else {
1107                false
1108            }
1109        };
1110
1111        if should_seal {
1112            self.seal_current()?;
1113        }
1114
1115        Ok(())
1116    }
1117
1118    /// Seal the current growing segment
1119    pub fn seal_current(&self) -> Result<SegmentId, SegmentError> {
1120        let growing_opt = self.growing.write().take();
1121
1122        if let Some(growing_arc) = growing_opt {
1123            let mut growing = growing_arc.write();
1124            let seg_id = growing.id();
1125            let entity_count = growing.stats().entity_count as u64;
1126
1127            // Seal it
1128            growing.seal()?;
1129
1130            // Move to sealed list (we need to extract it from the Arc)
1131            drop(growing); // Release write lock
1132
1133            // In a real implementation, we'd convert to SealedSegment here
1134            // For now, we keep it as-is since GrowingSegment implements UnifiedSegment
1135            self.sealed.write().push(growing_arc);
1136
1137            // Mark sealed segment pages all-visible — they're now immutable
1138            self.mark_sealed_pages_visible(entity_count);
1139
1140            // Update stats
1141            {
1142                let mut stats = self.stats.write();
1143                stats.growing_count = stats.growing_count.saturating_sub(1);
1144                stats.sealed_count += 1;
1145                stats.seal_ops += 1;
1146            }
1147
1148            self.emit(LifecycleEvent::SegmentSealed(seg_id));
1149
1150            return Ok(seg_id);
1151        }
1152
1153        Err(SegmentError::InvalidState(SegmentState::Sealed))
1154    }
1155
1156    /// Force seal (for testing/manual control)
1157    pub fn force_seal(&self) -> Result<Option<SegmentId>, SegmentError> {
1158        let has_growing = self.growing.read().is_some();
1159        if has_growing {
1160            Ok(Some(self.seal_current()?))
1161        } else {
1162            Ok(None)
1163        }
1164    }
1165
1166    /// Fraction of "pages" in sealed segments that are marked all-visible.
1167    ///
1168    /// Sealed segments are immutable so all their rows are safe for
1169    /// index-only scans. The growing segment is never counted (writes
1170    /// may be in-flight). Uses `rows_per_page = 256` (matching 8 KB pages
1171    /// with ~32-byte rows).
1172    ///
1173    /// Returns a value in `[0.0, 1.0]`. 1.0 when all sealed rows are
1174    /// visible; 0.0 when there are no sealed segments.
1175    pub fn all_visible_fraction(&self) -> f64 {
1176        const ROWS_PER_PAGE: u32 = 256;
1177        let sealed = self.sealed.read();
1178        if sealed.is_empty() {
1179            return 0.0;
1180        }
1181        let mut total_pages: u64 = 0;
1182        for seg_arc in sealed.iter() {
1183            let seg = seg_arc.read();
1184            let entity_count = seg.stats().entity_count as u64;
1185            let pages = entity_count.div_ceil(ROWS_PER_PAGE as u64);
1186            total_pages += pages;
1187        }
1188        if total_pages == 0 {
1189            return 0.0;
1190        }
1191        let visible = self.visibility_map.all_visible_count();
1192        (visible as f64 / total_pages as f64).min(1.0)
1193    }
1194
1195    /// Mark all pages of newly sealed segments as all-visible in the
1196    /// visibility map. Called internally after `seal_current`.
1197    fn mark_sealed_pages_visible(&self, seg_entity_count: u64) {
1198        const ROWS_PER_PAGE: u32 = 256;
1199        let existing_visible = self.visibility_map.all_visible_count();
1200        // Append pages starting after the last known visible page
1201        let start_page = existing_visible as u32;
1202        let new_pages = seg_entity_count.div_ceil(ROWS_PER_PAGE as u64);
1203        let end_page = start_page + new_pages as u32;
1204        self.visibility_map.mark_range_visible(start_page, end_page);
1205    }
1206
1207    /// Iterate over all entities in-place without collecting into a Vec.
1208    ///
1209    /// The callback receives a reference to each entity. Return `true` to
1210    /// continue iteration, `false` to stop early (e.g. when a LIMIT is reached).
1211    /// This avoids the allocation and cloning overhead of `query_all`.
1212    pub fn for_each_entity<F>(&self, mut callback: F)
1213    where
1214        F: FnMut(&UnifiedEntity) -> bool,
1215    {
1216        // Growing segment — direct iteration (no Box<dyn>)
1217        // Try non-blocking read first; fall back to blocking only when a writer
1218        // is actively holding the write lock (rare in read-heavy workloads).
1219        if let Some(growing_arc) = self.growing.read().as_ref() {
1220            let growing = if let Some(g) = growing_arc.try_read() {
1221                g
1222            } else {
1223                growing_arc.read()
1224            };
1225            if !growing.for_each_fast(&mut callback) {
1226                return;
1227            }
1228        }
1229
1230        // Sealed segments
1231        let sealed = self.sealed.read();
1232        for segment_arc in sealed.iter() {
1233            let segment = segment_arc.read();
1234            if !segment.for_each_fast(&mut callback) {
1235                return;
1236            }
1237        }
1238    }
1239
1240    /// Parallel fold across all entities. Each sealed segment is
1241    /// processed on its own rayon task; the growing segment stays on
1242    /// the caller thread (its read lock is briefly held).
1243    ///
1244    /// - `init` builds a fresh accumulator per thread.
1245    /// - `fold` mutates an accumulator with one entity at a time.
1246    /// - `reduce` combines two accumulators into one.
1247    ///
1248    /// The returned value is the reduction of every per-thread
1249    /// accumulator. Use this for aggregate-shape workloads (GROUP BY)
1250    /// where per-thread partial state can be merged cheaply.
1251    ///
1252    /// NOTE: when there are 0 or 1 sealed segments, the parallel path
1253    /// is skipped and the work runs sequentially to avoid rayon
1254    /// overhead on tiny tables.
1255    pub fn fold_entities_parallel<T, FInit, FFold, FReduce>(
1256        &self,
1257        init: FInit,
1258        fold: FFold,
1259        reduce: FReduce,
1260    ) -> T
1261    where
1262        T: Send,
1263        FInit: Fn() -> T + Send + Sync,
1264        FFold: Fn(T, &UnifiedEntity) -> T + Send + Sync,
1265        FReduce: Fn(T, T) -> T + Send + Sync,
1266    {
1267        use rayon::prelude::*;
1268
1269        // Growing segment — always sequential (single writer lock,
1270        // usually small working set).
1271        let mut acc = init();
1272        if let Some(growing_arc) = self.growing.read().as_ref() {
1273            let growing = if let Some(g) = growing_arc.try_read() {
1274                g
1275            } else {
1276                growing_arc.read()
1277            };
1278            growing.for_each_fast(|entity| {
1279                acc = fold(std::mem::replace(&mut acc, init()), entity);
1280                true
1281            });
1282        }
1283
1284        // Sealed segments — snapshot the Arc list under the read lock,
1285        // then drop the lock so rayon workers can fan out without
1286        // blocking writers.
1287        let segments: Vec<_> = {
1288            let sealed = self.sealed.read();
1289            sealed.iter().cloned().collect()
1290        };
1291
1292        if segments.len() <= 1 {
1293            for seg_arc in &segments {
1294                let seg = seg_arc.read();
1295                seg.for_each_fast(|entity| {
1296                    acc = fold(std::mem::replace(&mut acc, init()), entity);
1297                    true
1298                });
1299            }
1300            return acc;
1301        }
1302
1303        let sealed_acc = segments
1304            .into_par_iter()
1305            .map(|seg_arc| {
1306                let mut local = init();
1307                let seg = seg_arc.read();
1308                seg.for_each_fast(|entity| {
1309                    local = fold(std::mem::replace(&mut local, init()), entity);
1310                    true
1311                });
1312                local
1313            })
1314            .reduce(&init, &reduce);
1315
1316        reduce(acc, sealed_acc)
1317    }
1318
1319    /// Zone-map-aware iteration across all segments.
1320    ///
1321    /// Like `for_each_entity`, but checks `zone_preds` against each segment's
1322    /// column zone maps before iterating. Segments where any predicate can
1323    /// definitively prove no rows match are skipped entirely.
1324    ///
1325    /// `zone_preds`: slice of `(column_name, ZoneColPred)` extracted from the WHERE clause.
1326    /// Empty slice → same behaviour as `for_each_entity` (no pruning).
1327    pub fn for_each_entity_zoned<F>(&self, zone_preds: &[(&str, ZoneColPred<'_>)], mut callback: F)
1328    where
1329        F: FnMut(&UnifiedEntity) -> bool,
1330    {
1331        let _ = self.for_each_entity_zoned_with_stats(zone_preds, &mut callback);
1332    }
1333
1334    pub fn for_each_entity_zoned_with_stats<F>(
1335        &self,
1336        zone_preds: &[(&str, ZoneColPred<'_>)],
1337        mut callback: F,
1338    ) -> SegmentScanStats
1339    where
1340        F: FnMut(&UnifiedEntity) -> bool,
1341    {
1342        let mut scan_stats = SegmentScanStats::default();
1343        // Growing segment — never skip (it's receiving writes, zones are partial).
1344        // Try a non-blocking read first: if a writer is currently inserting
1345        // (holding the write lock), try_read() returns None and we fall back to
1346        // the blocking read.  In low-contention workloads (reads far outnumber
1347        // writes) the try_read() almost always succeeds and readers never stall.
1348        if let Some(growing_arc) = self.growing.read().as_ref() {
1349            scan_stats.segments_total += 1;
1350            scan_stats.segments_scanned += 1;
1351            let growing = if let Some(g) = growing_arc.try_read() {
1352                g
1353            } else {
1354                growing_arc.read()
1355            };
1356            if !growing.for_each_fast(&mut callback) {
1357                return scan_stats;
1358            }
1359        }
1360
1361        // Sealed segments — check zone maps before iterating
1362        let sealed = self.sealed.read();
1363        for segment_arc in sealed.iter() {
1364            scan_stats.segments_total += 1;
1365            let segment = segment_arc.read();
1366            if !zone_preds.is_empty() && segment.can_skip_zone_preds(zone_preds) {
1367                scan_stats.segments_pruned += 1;
1368                continue; // entire segment pruned
1369            }
1370            scan_stats.segments_scanned += 1;
1371            if !segment.for_each_fast(&mut callback) {
1372                return scan_stats;
1373            }
1374        }
1375        scan_stats
1376    }
1377
1378    /// Zone-map-aware parallel query.
1379    ///
1380    /// Like `query_all` but applies `zone_preds` on the main thread to
1381    /// prune sealed segments before spawning workers — segments that
1382    /// provably contain no matching rows are skipped entirely.
1383    ///
1384    /// Zone check runs single-threaded (it reads per-segment metadata,
1385    /// not row data), so it's cheap. Surviving segments are then scanned
1386    /// in parallel using `std::thread::scope` when there are > 1 of them.
1387    pub fn query_all_zoned<F>(
1388        &self,
1389        zone_preds: &[(&str, ZoneColPred<'_>)],
1390        filter: F,
1391    ) -> Vec<UnifiedEntity>
1392    where
1393        F: Fn(&UnifiedEntity) -> bool + Sync,
1394    {
1395        self.query_all_zoned_with_stats(zone_preds, filter).0
1396    }
1397
1398    pub fn query_all_zoned_with_stats<F>(
1399        &self,
1400        zone_preds: &[(&str, ZoneColPred<'_>)],
1401        filter: F,
1402    ) -> (Vec<UnifiedEntity>, SegmentScanStats)
1403    where
1404        F: Fn(&UnifiedEntity) -> bool + Sync,
1405    {
1406        let mut results = Vec::new();
1407        let mut scan_stats = SegmentScanStats::default();
1408
1409        // Growing segment — always scan, no zone skip (zones are partial).
1410        // Non-blocking try_read() avoids stalling behind in-progress inserts.
1411        if let Some(growing_arc) = self.growing.read().as_ref() {
1412            scan_stats.segments_total += 1;
1413            scan_stats.segments_scanned += 1;
1414            let growing = if let Some(g) = growing_arc.try_read() {
1415                g
1416            } else {
1417                growing_arc.read()
1418            };
1419            results.extend(growing.iter().filter(|e| filter(e)).cloned());
1420        }
1421
1422        // Sealed segments: zone-prune on main thread, then scan in parallel.
1423        let sealed = self.sealed.read();
1424        // Collect only the segments that survive zone-predicate pruning.
1425        let surviving: Vec<_> = sealed
1426            .iter()
1427            .filter(|seg_arc| {
1428                scan_stats.segments_total += 1;
1429                if zone_preds.is_empty() {
1430                    scan_stats.segments_scanned += 1;
1431                    return true;
1432                }
1433                let seg = seg_arc.read();
1434                if seg.can_skip_zone_preds(zone_preds) {
1435                    scan_stats.segments_pruned += 1;
1436                    false
1437                } else {
1438                    scan_stats.segments_scanned += 1;
1439                    true
1440                }
1441            })
1442            .collect();
1443
1444        let use_parallel = surviving.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1445
1446        if use_parallel {
1447            let filter_ref = &filter;
1448            let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1449                surviving
1450                    .iter()
1451                    .map(|segment| {
1452                        s.spawn(move || {
1453                            segment
1454                                .read()
1455                                .iter()
1456                                .filter(|e| filter_ref(e))
1457                                .cloned()
1458                                .collect::<Vec<_>>()
1459                        })
1460                    })
1461                    .collect::<Vec<_>>()
1462                    .into_iter()
1463                    .map(|handle| handle.join().unwrap_or_default())
1464                    .collect()
1465            });
1466            for batch in segment_results {
1467                results.extend(batch);
1468            }
1469        } else {
1470            for segment_arc in surviving {
1471                let seg = segment_arc.read();
1472                results.extend(seg.iter().filter(|e| filter(e)).cloned());
1473            }
1474        }
1475
1476        (results, scan_stats)
1477    }
1478
1479    /// Query across all segments. Uses parallel scanning for sealed segments
1480    /// when more than one sealed segment exists.
1481    pub fn query_all<F>(&self, filter: F) -> Vec<UnifiedEntity>
1482    where
1483        F: Fn(&UnifiedEntity) -> bool + Sync,
1484    {
1485        let mut results = Vec::new();
1486
1487        // Query growing segment — try non-blocking read first (avoids stalling
1488        // behind an in-progress insert; falls back to blocking if writer is active).
1489        if let Some(growing_arc) = self.growing.read().as_ref() {
1490            let growing = if let Some(g) = growing_arc.try_read() {
1491                g
1492            } else {
1493                growing_arc.read()
1494            };
1495            results.extend(growing.iter().filter(|e| filter(e)).cloned());
1496        }
1497
1498        // Query sealed segments — parallel when multiple exist AND multi-core
1499        let sealed = self.sealed.read();
1500        let use_parallel = sealed.len() > 1 && crate::runtime::SystemInfo::should_parallelize();
1501        if use_parallel {
1502            let filter_ref = &filter;
1503            let segment_results: Vec<Vec<UnifiedEntity>> = std::thread::scope(|s| {
1504                sealed
1505                    .iter()
1506                    .map(|segment| {
1507                        s.spawn(move || {
1508                            segment
1509                                .read()
1510                                .iter()
1511                                .filter(|e| filter_ref(e))
1512                                .cloned()
1513                                .collect::<Vec<_>>()
1514                        })
1515                    })
1516                    .collect::<Vec<_>>()
1517                    .into_iter()
1518                    .map(|handle| handle.join().unwrap_or_default())
1519                    .collect()
1520            });
1521            for batch in segment_results {
1522                results.extend(batch);
1523            }
1524        } else {
1525            for segment in sealed.iter() {
1526                let seg = segment.read();
1527                results.extend(seg.iter().filter(|e| filter(e)).cloned());
1528            }
1529        }
1530
1531        results
1532    }
1533
1534    /// Probe exact segment membership for a key without materializing matching entities.
1535    ///
1536    /// Returns `false` only when every consulted segment proves absence.
1537    /// Missing segment state is conservative and returns `true`.
1538    pub fn bloom_may_contain_key(&self, key: &[u8]) -> bool {
1539        if let Some(growing_arc) = self.growing.read().as_ref() {
1540            let growing = growing_arc.read();
1541            if growing.may_contain_exact_key(key) {
1542                return true;
1543            }
1544        } else {
1545            return true;
1546        }
1547
1548        let sealed = self.sealed.read();
1549        for segment_arc in sealed.iter() {
1550            let segment = segment_arc.read();
1551            if segment.may_contain_exact_key(key) {
1552                return true;
1553            }
1554        }
1555
1556        false
1557    }
1558
1559    /// Filter by metadata across all segments
1560    pub fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
1561        let mut results = Vec::new();
1562
1563        // Growing segment
1564        if let Some(growing_arc) = self.growing.read().as_ref() {
1565            let growing = growing_arc.read();
1566            results.extend(growing.filter_metadata(filters));
1567        }
1568
1569        // Sealed segments
1570        let sealed = self.sealed.read();
1571        for segment in sealed.iter() {
1572            results.extend(segment.filter_metadata(filters));
1573        }
1574
1575        results
1576    }
1577
1578    /// Get entities by kind
1579    pub fn get_by_kind(&self, kind: &str) -> Vec<UnifiedEntity> {
1580        let mut results = Vec::new();
1581
1582        // Growing segment
1583        if let Some(growing_arc) = self.growing.read().as_ref() {
1584            let growing = growing_arc.read();
1585            for entity in growing.iter_kind(kind) {
1586                results.push(entity.clone());
1587            }
1588        }
1589
1590        // Sealed segments. Iterate through the read guard, not the
1591        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` — that one cannot
1592        // hand out references across the lock, so it returns an empty iterator
1593        // and every sealed entity was silently missing from the result.
1594        let sealed = self.sealed.read();
1595        for segment_arc in sealed.iter() {
1596            let segment = segment_arc.read();
1597            for entity in segment.iter_kind(kind) {
1598                results.push(entity.clone());
1599            }
1600        }
1601
1602        results
1603    }
1604
1605    /// Count entities
1606    pub fn count(&self) -> usize {
1607        self.total_entities_atomic.load(Ordering::Relaxed) as usize
1608    }
1609
1610    /// Get all segment IDs
1611    pub fn segment_ids(&self) -> Vec<SegmentId> {
1612        let mut ids = Vec::new();
1613
1614        if let Some(growing_arc) = self.growing.read().as_ref() {
1615            ids.push(growing_arc.read().id());
1616        }
1617
1618        let sealed = self.sealed.read();
1619        for segment in sealed.iter() {
1620            ids.push(segment.id());
1621        }
1622
1623        ids.extend(self.archived.read().iter().copied());
1624
1625        ids
1626    }
1627
1628    /// Emit a lifecycle event.
1629    ///
1630    /// Perf: this used to push onto a `RwLock<Vec<LifecycleEvent>>`
1631    /// on every insert / delete / seal. Nobody consumes that vec
1632    /// today (no subscription API, `drain_events` has no callers),
1633    /// so the write lock + push was pure tax — and the vec grew
1634    /// unbounded in long-running processes.
1635    ///
1636    /// Current behaviour: no-op. If we ever want the hooks back,
1637    /// replace this with a bounded channel or an actual subscriber
1638    /// registry; the callers (`insert`, `delete`, `maybe_seal_growing`)
1639    /// already pass well-typed events.
1640    #[inline]
1641    #[allow(clippy::unused_self)]
1642    fn emit(&self, _event: LifecycleEvent) {}
1643
1644    /// Drain events. Kept for API compatibility; always returns
1645    /// empty because `emit` no longer buffers.
1646    pub fn drain_events(&self) -> Vec<LifecycleEvent> {
1647        std::mem::take(&mut *self.events.write())
1648    }
1649
1650    /// Run maintenance (would be called periodically in production)
1651    pub fn run_maintenance(&self) -> Result<(), SegmentError> {
1652        // Auto-seal idle segments
1653        self.maybe_seal_growing()?;
1654
1655        if self.config.enable_consolidation {
1656            self.consolidation_tick();
1657        }
1658
1659        Ok(())
1660    }
1661
1662    // ========================================================================
1663    // Consolidation — ADR 0073 §5
1664    // ========================================================================
1665
1666    /// One paced step of sealed-segment consolidation.
1667    ///
1668    /// Starts a run when a threshold has been crossed, copies at most
1669    /// `consolidation_entities_per_tick` live entities into the merged segment,
1670    /// and swaps once every source has been drained. Returns without finishing
1671    /// otherwise — the half-built merged segment waits for the next tick.
1672    fn consolidation_tick(&self) {
1673        // Drop the read guard before `start_consolidation` takes the write
1674        // guard: `parking_lot` locks are not reentrant.
1675        let running = self.consolidation.read().is_some();
1676        if !running && !self.start_consolidation() {
1677            return;
1678        }
1679
1680        let finished = {
1681            let mut guard = self.consolidation.write();
1682            let Some(run) = guard.as_mut() else {
1683                return;
1684            };
1685            self.copy_bounded(run);
1686            run.cursor >= run.sources.len()
1687        };
1688
1689        if finished {
1690            let run = self.consolidation.write().take();
1691            if let Some(run) = run {
1692                self.finish_consolidation(run);
1693            }
1694        }
1695    }
1696
1697    /// Evaluate the trigger and, when it fires, open a consolidation run.
1698    ///
1699    /// Returns `true` when a run was started.
1700    fn start_consolidation(&self) -> bool {
1701        let sources = self.select_candidates();
1702        self.start_consolidation_from_sources(sources)
1703    }
1704
1705    fn start_consolidation_from_sources(&self, sources: Vec<SegmentId>) -> bool {
1706        if sources.len() < 2 && !self.candidates_carry_tombstones(&sources) {
1707            return false;
1708        }
1709
1710        let merged_id = self.next_segment_id.fetch_add(1, Ordering::SeqCst);
1711        let merged = GrowingSegment::new(merged_id, &self.collection);
1712
1713        *self.consolidation.write() = Some(Consolidation {
1714            sources: sources
1715                .into_iter()
1716                .map(|id| SourceProgress {
1717                    id,
1718                    copied: Vec::new(),
1719                    tombstones_at_open: None,
1720                })
1721                .collect(),
1722            merged_id,
1723            merged,
1724            cursor: 0,
1725            pending_ids: Vec::new(),
1726            pending_cursor: 0,
1727            entities_copied: 0,
1728        });
1729
1730        self.stats.write().consolidation.runs_started += 1;
1731        true
1732    }
1733
1734    /// Run one paced consolidation tick because admission pressure needs
1735    /// reclaimable bytes, even if the background thresholds have not fired.
1736    ///
1737    /// Returns true when the call started or advanced a consolidation run.
1738    pub fn pressure_consolidation_tick(&self) -> bool {
1739        if !self.config.enable_consolidation {
1740            return false;
1741        }
1742
1743        let running = self.consolidation.read().is_some();
1744        if !running {
1745            let sources = self.dirty_sealed_segments();
1746            if !self.start_consolidation_from_sources(sources) {
1747                return false;
1748            }
1749        }
1750
1751        self.consolidation_tick();
1752        true
1753    }
1754
1755    /// Which sealed segments this collection should merge, if any.
1756    ///
1757    /// Three thresholds, any of which arms consolidation:
1758    ///
1759    /// * **sealed-segment count** above `max_sealed_segments` — every sealed
1760    ///   segment merges, which is what that knob always claimed to do.
1761    /// * **tombstone ratio** — dead entities over total entities across the
1762    ///   sealed set, past [`CONSOLIDATION_TOMBSTONE_RATIO`].
1763    /// * **fragmentation** — reclaimable bytes over held bytes, past
1764    ///   [`CONSOLIDATION_FRAGMENTATION_RATIO`].
1765    ///
1766    /// The count trigger takes every sealed segment; the two budget triggers
1767    /// take only the segments that actually carry tombstones, so a clean
1768    /// segment is never rewritten for nothing.
1769    fn select_candidates(&self) -> Vec<SegmentId> {
1770        let sealed = self.sealed.read();
1771        if sealed.is_empty() {
1772            return Vec::new();
1773        }
1774
1775        if sealed.len() > self.config.max_sealed_segments {
1776            return sealed.iter().map(|segment| segment.read().id()).collect();
1777        }
1778
1779        let mut live = 0usize;
1780        let mut tombstones = 0usize;
1781        let mut held = 0u64;
1782        let mut reclaimable = 0u64;
1783        let mut dirty = Vec::new();
1784
1785        for segment_arc in sealed.iter() {
1786            let segment = segment_arc.read();
1787            live += segment.live_entity_count();
1788            tombstones += segment.tombstone_count();
1789            held += segment.resident_bytes();
1790            reclaimable += segment.reclaimable_bytes();
1791            if segment.tombstone_count() > 0 {
1792                dirty.push(segment.id());
1793            }
1794        }
1795
1796        let total = live + tombstones;
1797        let tombstone_ratio = if total == 0 {
1798            0.0
1799        } else {
1800            tombstones as f64 / total as f64
1801        };
1802        let fragmentation = if held == 0 {
1803            0.0
1804        } else {
1805            reclaimable as f64 / held as f64
1806        };
1807
1808        if tombstone_ratio >= CONSOLIDATION_TOMBSTONE_RATIO
1809            || fragmentation >= CONSOLIDATION_FRAGMENTATION_RATIO
1810        {
1811            return dirty;
1812        }
1813
1814        Vec::new()
1815    }
1816
1817    fn dirty_sealed_segments(&self) -> Vec<SegmentId> {
1818        self.sealed
1819            .read()
1820            .iter()
1821            .filter_map(|segment_arc| {
1822                let segment = segment_arc.read();
1823                (segment.reclaimable_bytes() > 0).then(|| segment.id())
1824            })
1825            .collect()
1826    }
1827
1828    /// A single candidate is still worth merging when it carries tombstones —
1829    /// the merge drops them. A single clean candidate is not.
1830    fn candidates_carry_tombstones(&self, sources: &[SegmentId]) -> bool {
1831        if sources.is_empty() {
1832            return false;
1833        }
1834        let sealed = self.sealed.read();
1835        sealed.iter().any(|segment_arc| {
1836            let segment = segment_arc.read();
1837            sources.contains(&segment.id()) && segment.tombstone_count() > 0
1838        })
1839    }
1840
1841    fn sealed_segment(&self, id: SegmentId) -> Option<Arc<RwLock<GrowingSegment>>> {
1842        let sealed = self.sealed.read();
1843        sealed
1844            .iter()
1845            .find(|segment| segment.read().id() == id)
1846            .map(Arc::clone)
1847    }
1848
1849    /// Copy at most one tick's worth of live entities into the merged segment.
1850    ///
1851    /// The per-tick bound is the pacing contract: a tick's cost is O(bound),
1852    /// independent of how many entities the run still has to move.
1853    fn copy_bounded(&self, run: &mut Consolidation) {
1854        let mut budget = self.config.consolidation_entities_per_tick;
1855
1856        while budget > 0 && run.cursor < run.sources.len() {
1857            let source_id = run.sources[run.cursor].id;
1858            let Some(source_arc) = self.sealed_segment(source_id) else {
1859                // The source vanished. Only consolidation removes sealed
1860                // segments and it is single-threaded per collection, so this
1861                // cannot happen today — skip it rather than resurrect its rows.
1862                run.cursor += 1;
1863                run.pending_ids.clear();
1864                run.pending_cursor = 0;
1865                continue;
1866            };
1867
1868            if run.sources[run.cursor].tombstones_at_open.is_none() {
1869                let source = source_arc.read();
1870                run.pending_ids = source.live_entity_ids();
1871                run.pending_cursor = 0;
1872                run.sources[run.cursor].tombstones_at_open = Some(source.tombstone_count());
1873            }
1874
1875            {
1876                let source = source_arc.read();
1877                while budget > 0 && run.pending_cursor < run.pending_ids.len() {
1878                    let id = run.pending_ids[run.pending_cursor];
1879                    run.pending_cursor += 1;
1880                    budget -= 1;
1881
1882                    // Tombstoned between the snapshot and now — it must not be
1883                    // resurrected into the merged segment.
1884                    let Some(entity) = source.get(id) else {
1885                        continue;
1886                    };
1887                    let metadata = source.get_metadata(id);
1888                    run.merged.adopt_entity(entity.clone(), metadata);
1889                    run.sources[run.cursor].copied.push(id);
1890                    run.entities_copied += 1;
1891                }
1892            }
1893
1894            if run.pending_cursor >= run.pending_ids.len() {
1895                run.cursor += 1;
1896                run.pending_ids.clear();
1897                run.pending_cursor = 0;
1898            }
1899        }
1900    }
1901
1902    /// Atomically replace the source segments with the merged one.
1903    ///
1904    /// The `sealed` write lock is the boundary: a reader holds the read lock
1905    /// for the whole of its scan, so it observes either the old set or the new
1906    /// set, never a gap and never both copies of a row.
1907    fn finish_consolidation(&self, mut run: Consolidation) {
1908        let mut sealed = self.sealed.write();
1909
1910        let mut positions = Vec::with_capacity(run.sources.len());
1911        for source in &run.sources {
1912            if let Some(index) = sealed
1913                .iter()
1914                .position(|segment| segment.read().id() == source.id)
1915            {
1916                positions.push(index);
1917            }
1918        }
1919        if positions.is_empty() {
1920            return;
1921        }
1922        positions.sort_unstable();
1923
1924        // Rows deleted from a source after we copied them must not come back.
1925        // A source whose tombstone count is unchanged saw no deletes while the
1926        // merge ran, so its copied rows need no per-row check.
1927        let mut stale = Vec::new();
1928        for source in &run.sources {
1929            let Some(source_arc) = sealed
1930                .iter()
1931                .find(|segment| segment.read().id() == source.id)
1932            else {
1933                continue;
1934            };
1935            let segment = source_arc.read();
1936            if source.tombstones_at_open == Some(segment.tombstone_count()) {
1937                continue;
1938            }
1939            stale.extend(
1940                source
1941                    .copied
1942                    .iter()
1943                    .copied()
1944                    .filter(|id| !segment.contains(*id)),
1945            );
1946        }
1947        for id in stale {
1948            run.merged.evict_entity(id);
1949        }
1950
1951        let reclaimed_bytes: u64 = positions
1952            .iter()
1953            .map(|&index| sealed[index].read().resident_bytes())
1954            .sum();
1955        let reclaimed_tombstones: u64 = positions
1956            .iter()
1957            .map(|&index| sealed[index].read().tombstone_count() as u64)
1958            .sum();
1959
1960        if run.merged.seal().is_err() {
1961            return;
1962        }
1963        let merged_bytes = run.merged.resident_bytes();
1964        let merged_arc = Arc::new(RwLock::new(run.merged));
1965
1966        for &index in positions.iter().rev() {
1967            sealed.remove(index);
1968        }
1969        let insert_at = positions[0].min(sealed.len());
1970        sealed.insert(insert_at, Arc::clone(&merged_arc));
1971        drop(sealed);
1972
1973        // Remap the entity→segment hints that pointed at the retired sources.
1974        let source_ids: Vec<SegmentId> = run.sources.iter().map(|source| source.id).collect();
1975        {
1976            let mut entity_segment = self.entity_segment.write();
1977            for segment_id in entity_segment.values_mut() {
1978                if source_ids.contains(segment_id) {
1979                    *segment_id = run.merged_id;
1980                }
1981            }
1982        }
1983
1984        {
1985            let mut stats = self.stats.write();
1986            stats.sealed_count = stats
1987                .sealed_count
1988                .saturating_sub(positions.len())
1989                .saturating_add(1);
1990            let consolidation = &mut stats.consolidation;
1991            consolidation.runs_completed += 1;
1992            consolidation.segments_merged += positions.len() as u64;
1993            consolidation.tombstones_reclaimed += reclaimed_tombstones;
1994            consolidation.bytes_reclaimed += reclaimed_bytes.saturating_sub(merged_bytes);
1995        }
1996
1997        self.emit(LifecycleEvent::SegmentConsolidated {
1998            source: source_ids,
1999            target: run.merged_id,
2000        });
2001    }
2002
2003    /// Entities copied by the consolidation currently in flight, or `None` when
2004    /// none is running. Exposed for the pacing tests.
2005    #[cfg(test)]
2006    fn consolidation_progress(&self) -> Option<u64> {
2007        self.consolidation
2008            .read()
2009            .as_ref()
2010            .map(|run| run.entities_copied)
2011    }
2012}
2013
2014// Implement the Arc<RwLock<GrowingSegment>> as UnifiedSegment
2015// This is needed because we store growing segments in the sealed list after sealing
2016impl UnifiedSegment for Arc<RwLock<GrowingSegment>> {
2017    fn id(&self) -> SegmentId {
2018        self.read().id()
2019    }
2020
2021    fn state(&self) -> SegmentState {
2022        self.read().state()
2023    }
2024
2025    fn collection(&self) -> &str {
2026        // This is a limitation - we'd need to store collection in the Arc wrapper
2027        "unknown"
2028    }
2029
2030    fn stats(&self) -> SegmentStats {
2031        self.read().stats()
2032    }
2033
2034    fn entity_count(&self) -> usize {
2035        self.read().entity_count()
2036    }
2037
2038    fn contains(&self, id: EntityId) -> bool {
2039        self.read().contains(id)
2040    }
2041
2042    fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2043        // This is tricky with RwLock - we can't return a reference
2044        // In production, we'd use a different approach
2045        None
2046    }
2047
2048    fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2049        None
2050    }
2051
2052    fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2053        self.write().insert(entity)
2054    }
2055
2056    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2057        self.write().update(entity)
2058    }
2059
2060    fn update_hot(
2061        &mut self,
2062        entity: UnifiedEntity,
2063        modified_columns: &[String],
2064    ) -> Result<(), SegmentError> {
2065        self.write().update_hot(entity, modified_columns)
2066    }
2067
2068    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2069        self.write().delete(id)
2070    }
2071
2072    fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2073        self.read().get_metadata(id)
2074    }
2075
2076    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2077        self.write().set_metadata(id, metadata)
2078    }
2079
2080    fn seal(&mut self) -> Result<(), SegmentError> {
2081        self.write().seal()
2082    }
2083
2084    fn should_seal(&self, config: &SegmentConfig) -> bool {
2085        self.read().should_seal(config)
2086    }
2087
2088    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2089        // Cannot return iterator with RwLock
2090        Box::new(std::iter::empty())
2091    }
2092
2093    fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2094        Box::new(std::iter::empty())
2095    }
2096
2097    fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2098        self.read().filter_metadata(filters)
2099    }
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104    use super::*;
2105    use crate::storage::schema::Value;
2106    use crate::storage::unified::entity::{EntityData, EntityKind, RowData};
2107
2108    #[test]
2109    fn test_manager_basic() {
2110        let manager = SegmentManager::new("test_collection");
2111
2112        let entity = UnifiedEntity::table_row(
2113            manager.next_entity_id(),
2114            "users",
2115            1,
2116            vec![Value::text("Alice".to_string())],
2117        );
2118
2119        let id = manager.insert(entity).unwrap();
2120        assert!(manager.get(id).is_some());
2121        assert_eq!(manager.count(), 1);
2122    }
2123
2124    #[test]
2125    fn bloom_may_contain_key_probes_inserted_rid_bytes() {
2126        let manager = SegmentManager::new("test_collection");
2127
2128        let entity = UnifiedEntity::table_row(
2129            manager.next_entity_id(),
2130            "users",
2131            1,
2132            vec![Value::text("Alice".to_string())],
2133        );
2134
2135        let id = manager.insert(entity).unwrap();
2136
2137        assert!(manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
2138        assert!(manager.bloom_may_contain_key(id.raw().to_string().as_bytes()));
2139        assert!(!manager.bloom_may_contain_key(&u64::MAX.to_le_bytes()));
2140
2141        assert!(manager.delete(id).unwrap());
2142        assert!(!manager.bloom_may_contain_key(&id.raw().to_le_bytes()));
2143    }
2144
2145    #[test]
2146    fn zoned_scan_reports_pruned_segments_and_preserves_results() {
2147        let config = ManagerConfig {
2148            segment_config: SegmentConfig {
2149                max_entities: 2,
2150                ..Default::default()
2151            },
2152            ..Default::default()
2153        };
2154        let manager = SegmentManager::with_config("events", config);
2155
2156        for (idx, value) in [10, 20, 100, 110, 200].into_iter().enumerate() {
2157            let row = RowData::with_names(vec![Value::Integer(value)], vec!["ts".to_string()]);
2158            let entity = UnifiedEntity::new(
2159                manager.next_entity_id(),
2160                EntityKind::TableRow {
2161                    table: "events".into(),
2162                    row_id: (idx + 1) as u64,
2163                },
2164                EntityData::Row(row),
2165            );
2166            manager.insert(entity).unwrap();
2167        }
2168
2169        let full = manager.query_all(|entity| match &entity.data {
2170            EntityData::Row(row) => matches!(row.get_field("ts"), Some(Value::Integer(100))),
2171            _ => false,
2172        });
2173        let probe = Value::Integer(100);
2174        let (pruned, stats) =
2175            manager.query_all_zoned_with_stats(&[("ts", ZoneColPred::Eq(&probe))], |entity| {
2176                match &entity.data {
2177                    EntityData::Row(row) => {
2178                        matches!(row.get_field("ts"), Some(Value::Integer(100)))
2179                    }
2180                    _ => false,
2181                }
2182            });
2183
2184        assert_eq!(pruned.len(), full.len());
2185        assert_eq!(pruned[0].id, full[0].id);
2186        assert_eq!(stats.segments_total, 3);
2187        assert_eq!(stats.segments_scanned, 2);
2188        assert_eq!(stats.segments_pruned, 1);
2189    }
2190
2191    #[test]
2192    fn test_manager_auto_seal() {
2193        let config = ManagerConfig {
2194            segment_config: SegmentConfig {
2195                max_entities: 2,
2196                ..Default::default()
2197            },
2198            ..Default::default()
2199        };
2200
2201        let manager = SegmentManager::with_config("test", config);
2202
2203        // Insert first entity
2204        manager
2205            .insert(UnifiedEntity::vector(
2206                manager.next_entity_id(),
2207                "v",
2208                vec![0.1],
2209            ))
2210            .unwrap();
2211
2212        // Insert second entity (triggers seal check)
2213        manager
2214            .insert(UnifiedEntity::vector(
2215                manager.next_entity_id(),
2216                "v",
2217                vec![0.2],
2218            ))
2219            .unwrap();
2220
2221        // Insert third entity (should trigger auto-seal of first segment)
2222        manager
2223            .insert(UnifiedEntity::vector(
2224                manager.next_entity_id(),
2225                "v",
2226                vec![0.3],
2227            ))
2228            .unwrap();
2229
2230        let stats = manager.stats();
2231        assert_eq!(stats.total_entities, 3);
2232    }
2233
2234    #[test]
2235    fn test_manager_delete() {
2236        let manager = SegmentManager::new("test");
2237
2238        let id = manager
2239            .insert(UnifiedEntity::vector(
2240                manager.next_entity_id(),
2241                "v",
2242                vec![0.1],
2243            ))
2244            .unwrap();
2245
2246        assert!(manager.get(id).is_some());
2247        assert!(manager.delete(id).unwrap());
2248        assert!(manager.get(id).is_none());
2249    }
2250
2251    #[test]
2252    fn test_manager_metadata() {
2253        let manager = SegmentManager::new("test");
2254
2255        let id = manager
2256            .insert(UnifiedEntity::table_row(
2257                manager.next_entity_id(),
2258                "hosts",
2259                1,
2260                vec![Value::text("192.168.1.1".to_string())],
2261            ))
2262            .unwrap();
2263
2264        let mut meta = Metadata::new();
2265        meta.set(
2266            "os",
2267            super::super::metadata::MetadataValue::String("linux".to_string()),
2268        );
2269
2270        manager.set_metadata(id, meta).unwrap();
2271
2272        let retrieved = manager.get_metadata(id).unwrap();
2273        assert!(retrieved.has("os"));
2274    }
2275
2276    #[test]
2277    fn test_manager_query_by_kind() {
2278        let manager = SegmentManager::new("test");
2279
2280        manager
2281            .insert(UnifiedEntity::table_row(
2282                manager.next_entity_id(),
2283                "hosts",
2284                1,
2285                vec![],
2286            ))
2287            .unwrap();
2288
2289        manager
2290            .insert(UnifiedEntity::vector(
2291                manager.next_entity_id(),
2292                "embeddings",
2293                vec![0.1],
2294            ))
2295            .unwrap();
2296
2297        manager
2298            .insert(UnifiedEntity::table_row(
2299                manager.next_entity_id(),
2300                "hosts",
2301                2,
2302                vec![],
2303            ))
2304            .unwrap();
2305
2306        let rows = manager.get_by_kind("table");
2307        assert_eq!(rows.len(), 2);
2308
2309        let vectors = manager.get_by_kind("vector");
2310        assert_eq!(vectors.len(), 1);
2311    }
2312
2313    // ========================================================================
2314    // Consolidation — ADR 0073 §5
2315    // ========================================================================
2316
2317    /// A manager whose sealed set never trips the count trigger, so tests can
2318    /// exercise the tombstone/fragmentation triggers in isolation.
2319    fn consolidating_manager(entities_per_tick: usize) -> SegmentManager {
2320        SegmentManager::with_config(
2321            "test",
2322            ManagerConfig {
2323                max_sealed_segments: 1_000,
2324                consolidation_entities_per_tick: entities_per_tick,
2325                ..Default::default()
2326            },
2327        )
2328    }
2329
2330    /// Bulk-load `count` rows into a fresh segment and seal it. Bulk-inserted
2331    /// entities land in flat storage, where a tombstone keeps its payload
2332    /// resident — which is the memory consolidation reclaims.
2333    fn seal_bulk_segment(manager: &SegmentManager, count: usize) -> Vec<EntityId> {
2334        let entities: Vec<UnifiedEntity> = (0..count)
2335            .map(|i| {
2336                UnifiedEntity::table_row(
2337                    manager.next_entity_id(),
2338                    "users",
2339                    i as u64,
2340                    vec![Value::Integer(i as i64), Value::text(format!("row-{i}"))],
2341                )
2342            })
2343            .collect();
2344        let ids = manager.bulk_insert(entities).expect("bulk insert");
2345        manager.force_seal().expect("seal");
2346        ids
2347    }
2348
2349    fn live_ids(manager: &SegmentManager) -> Vec<u64> {
2350        let mut ids: Vec<u64> = manager
2351            .query_all(|_| true)
2352            .into_iter()
2353            .map(|entity| entity.id.raw())
2354            .collect();
2355        ids.sort_unstable();
2356        ids
2357    }
2358
2359    fn drain_maintenance(manager: &SegmentManager) -> usize {
2360        let mut ticks = 0;
2361        while manager.consolidation.read().is_some() || ticks == 0 {
2362            manager.run_maintenance().expect("maintenance");
2363            ticks += 1;
2364            assert!(ticks < 1_000, "consolidation failed to converge");
2365        }
2366        ticks
2367    }
2368
2369    #[test]
2370    fn delete_heavy_workload_reclaims_tombstoned_memory_and_counts_it() {
2371        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
2372        let ids = seal_bulk_segment(&manager, 100);
2373
2374        let bytes_before_delete = manager.resident_bytes();
2375
2376        // 40% tombstones — past CONSOLIDATION_TOMBSTONE_RATIO.
2377        for id in ids.iter().take(40) {
2378            assert!(manager.delete(*id).expect("delete"));
2379        }
2380
2381        // Tombstones cost memory: the payload stays resident in flat storage
2382        // and the tombstone set grows.
2383        let bytes_with_tombstones = manager.resident_bytes();
2384        assert!(bytes_with_tombstones > bytes_before_delete);
2385
2386        let survivors = live_ids(&manager);
2387        assert_eq!(survivors.len(), 60);
2388
2389        drain_maintenance(&manager);
2390
2391        let stats = manager.stats();
2392        assert_eq!(stats.consolidation.runs_started, 1);
2393        assert_eq!(stats.consolidation.runs_completed, 1);
2394        assert_eq!(stats.consolidation.segments_merged, 1);
2395        assert_eq!(stats.consolidation.tombstones_reclaimed, 40);
2396        assert!(stats.consolidation.bytes_reclaimed > 0);
2397
2398        // The reclaimed memory is at least the tombstoned entities' share.
2399        let bytes_after = manager.resident_bytes();
2400        assert!(
2401            bytes_after <= bytes_before_delete,
2402            "consolidation must return the tombstones' memory: {bytes_after} > {bytes_before_delete}"
2403        );
2404        assert_eq!(
2405            bytes_with_tombstones - bytes_after,
2406            stats.consolidation.bytes_reclaimed
2407        );
2408        assert_eq!(stats.total_memory_bytes, bytes_after as usize);
2409
2410        assert_eq!(live_ids(&manager), survivors);
2411        assert_eq!(manager.sealed.read().len(), 1);
2412        assert_eq!(manager.sealed.read()[0].read().tombstone_count(), 0);
2413    }
2414
2415    #[test]
2416    fn query_results_are_identical_before_during_and_after_consolidation() {
2417        let manager = consolidating_manager(3);
2418        let ids = seal_bulk_segment(&manager, 20);
2419        seal_bulk_segment(&manager, 20);
2420        for id in ids.iter().take(10) {
2421            assert!(manager.delete(*id).expect("delete"));
2422        }
2423
2424        let before = live_ids(&manager);
2425        let survivor = *ids.last().expect("at least one row");
2426        let tombstoned = ids[0];
2427
2428        // Every mid-tick observation matches the pre-consolidation snapshot:
2429        // point reads, full scans, and the kind index (rebuilt by the merge).
2430        let mut mid_tick_observations = 0;
2431        while manager.consolidation.read().is_some() || mid_tick_observations == 0 {
2432            manager.run_maintenance().expect("maintenance");
2433            assert_eq!(live_ids(&manager), before);
2434            assert!(manager.get(survivor).is_some());
2435            assert!(manager.get(tombstoned).is_none());
2436            assert_eq!(manager.get_by_kind("table").len(), before.len());
2437            mid_tick_observations += 1;
2438            assert!(mid_tick_observations < 1_000);
2439        }
2440
2441        assert!(
2442            mid_tick_observations > 2,
2443            "expected the run to span several ticks, saw {mid_tick_observations}"
2444        );
2445        assert_eq!(live_ids(&manager), before);
2446        assert!(manager.get(survivor).is_some());
2447        assert!(manager.get(tombstoned).is_none());
2448        assert_eq!(manager.get_by_kind("table").len(), before.len());
2449    }
2450
2451    #[test]
2452    fn get_by_kind_reaches_sealed_segments() {
2453        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
2454        let ids = seal_bulk_segment(&manager, 5);
2455        assert!(manager.delete(ids[0]).expect("delete"));
2456
2457        // The sealed list is walked through the segment read guard; the
2458        // `Arc<RwLock<GrowingSegment>>` impl of `iter_kind` cannot hand out
2459        // references across the lock and yields nothing.
2460        assert_eq!(manager.get_by_kind("table").len(), 4);
2461        assert_eq!(manager.get_by_kind("vector").len(), 0);
2462    }
2463
2464    #[test]
2465    fn consolidation_is_paced_by_the_per_tick_entity_bound() {
2466        const PER_TICK: usize = 4;
2467        let manager = consolidating_manager(PER_TICK);
2468        let ids = seal_bulk_segment(&manager, 60);
2469        for id in ids.iter().take(30) {
2470            assert!(manager.delete(*id).expect("delete"));
2471        }
2472
2473        // First tick opens the run and copies exactly one bound's worth.
2474        manager.run_maintenance().expect("maintenance");
2475        assert_eq!(manager.consolidation_progress(), Some(PER_TICK as u64));
2476
2477        // Each subsequent tick advances by no more than the bound, and the run
2478        // is still in flight — it never completes in one unbounded pass.
2479        let mut previous = PER_TICK as u64;
2480        let mut ticks = 1;
2481        while let Some(copied) = manager.consolidation_progress() {
2482            assert!(
2483                copied - previous <= PER_TICK as u64,
2484                "tick copied {} entities, bound is {PER_TICK}",
2485                copied - previous
2486            );
2487            previous = copied;
2488            manager.run_maintenance().expect("maintenance");
2489            ticks += 1;
2490            assert!(ticks < 100);
2491        }
2492
2493        // 30 survivors at 4 per tick: the run cannot have finished before the
2494        // eighth tick.
2495        assert!(ticks >= 8, "consolidation completed in only {ticks} ticks");
2496        assert_eq!(manager.stats().consolidation.runs_completed, 1);
2497        assert_eq!(live_ids(&manager).len(), 30);
2498    }
2499
2500    #[test]
2501    fn sealed_segment_count_above_the_limit_triggers_a_merge() {
2502        let manager = SegmentManager::with_config(
2503            "test",
2504            ManagerConfig {
2505                max_sealed_segments: 3,
2506                ..Default::default()
2507            },
2508        );
2509        for _ in 0..4 {
2510            seal_bulk_segment(&manager, 5);
2511        }
2512        assert_eq!(manager.sealed.read().len(), 4);
2513
2514        let before = live_ids(&manager);
2515        drain_maintenance(&manager);
2516
2517        // The knob finally does what it claimed: the sealed set collapses even
2518        // though not a single row was deleted.
2519        assert_eq!(manager.sealed.read().len(), 1);
2520        assert_eq!(manager.stats().consolidation.segments_merged, 4);
2521        assert_eq!(manager.stats().consolidation.tombstones_reclaimed, 0);
2522        assert_eq!(live_ids(&manager), before);
2523    }
2524
2525    #[test]
2526    fn a_clean_sealed_set_below_the_limit_is_never_consolidated() {
2527        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
2528        seal_bulk_segment(&manager, 10);
2529        seal_bulk_segment(&manager, 10);
2530
2531        for _ in 0..5 {
2532            manager.run_maintenance().expect("maintenance");
2533        }
2534
2535        assert_eq!(manager.stats().consolidation.runs_started, 0);
2536        assert_eq!(manager.sealed.read().len(), 2);
2537    }
2538
2539    #[test]
2540    fn concurrent_readers_never_observe_a_missing_or_duplicated_entity() {
2541        use std::sync::atomic::AtomicBool;
2542
2543        let manager = Arc::new(consolidating_manager(2));
2544        let ids = seal_bulk_segment(&manager, 40);
2545        seal_bulk_segment(&manager, 40);
2546        for id in ids.iter().take(20) {
2547            assert!(manager.delete(*id).expect("delete"));
2548        }
2549
2550        let expected = live_ids(&manager);
2551        let stop = Arc::new(AtomicBool::new(false));
2552
2553        let readers: Vec<_> = (0..4)
2554            .map(|_| {
2555                let manager = Arc::clone(&manager);
2556                let stop = Arc::clone(&stop);
2557                let expected = expected.clone();
2558                std::thread::spawn(move || {
2559                    let mut scans = 0u64;
2560                    // Do-while: every reader scans at least once even when the
2561                    // drain finishes before this thread is first scheduled.
2562                    loop {
2563                        let mut seen: Vec<u64> = manager
2564                            .query_all(|_| true)
2565                            .into_iter()
2566                            .map(|entity| entity.id.raw())
2567                            .collect();
2568                        seen.sort_unstable();
2569                        // Sorting is not enough: a duplicated row would keep
2570                        // the set equal but change the length.
2571                        assert_eq!(seen, expected, "reader saw a torn sealed set");
2572                        scans += 1;
2573                        if stop.load(Ordering::Relaxed) {
2574                            break;
2575                        }
2576                    }
2577                    scans
2578                })
2579            })
2580            .collect();
2581
2582        drain_maintenance(&manager);
2583        stop.store(true, Ordering::Relaxed);
2584
2585        for reader in readers {
2586            let scans = reader.join().expect("reader thread");
2587            assert!(scans > 0, "reader never got to scan");
2588        }
2589
2590        assert_eq!(manager.stats().consolidation.runs_completed, 1);
2591        assert_eq!(live_ids(&manager), expected);
2592    }
2593
2594    #[test]
2595    fn a_row_deleted_mid_consolidation_is_not_resurrected_by_the_swap() {
2596        let manager = consolidating_manager(4);
2597        let ids = seal_bulk_segment(&manager, 20);
2598        for id in ids.iter().take(6) {
2599            assert!(manager.delete(*id).expect("delete"));
2600        }
2601
2602        // Copy the first bound's worth, then delete a row we just copied.
2603        manager.run_maintenance().expect("maintenance");
2604        assert_eq!(manager.consolidation_progress(), Some(4));
2605        let doomed = ids[6];
2606        assert!(manager.delete(doomed).expect("delete"));
2607
2608        let expected = live_ids(&manager);
2609        drain_maintenance(&manager);
2610
2611        assert!(manager.get(doomed).is_none(), "deleted row came back");
2612        assert_eq!(live_ids(&manager), expected);
2613    }
2614
2615    #[test]
2616    fn discarding_an_in_flight_consolidation_leaves_the_data_untouched() {
2617        let manager = consolidating_manager(3);
2618        let ids = seal_bulk_segment(&manager, 30);
2619        for id in ids.iter().take(12) {
2620            assert!(manager.delete(*id).expect("delete"));
2621        }
2622
2623        let before = live_ids(&manager);
2624        let sealed_before: Vec<SegmentId> = manager
2625            .sealed
2626            .read()
2627            .iter()
2628            .map(|segment| segment.read().id())
2629            .collect();
2630
2631        manager.run_maintenance().expect("maintenance");
2632        manager.run_maintenance().expect("maintenance");
2633        assert!(manager.consolidation.read().is_some());
2634
2635        // A crash between ticks drops the half-built merged segment. Sealed
2636        // segments are the durable truth, so nothing was lost — and the run is
2637        // simply started over.
2638        *manager.consolidation.write() = None;
2639
2640        assert_eq!(live_ids(&manager), before);
2641        let sealed_after: Vec<SegmentId> = manager
2642            .sealed
2643            .read()
2644            .iter()
2645            .map(|segment| segment.read().id())
2646            .collect();
2647        assert_eq!(sealed_after, sealed_before);
2648        assert_eq!(manager.stats().consolidation.runs_completed, 0);
2649
2650        drain_maintenance(&manager);
2651        assert_eq!(live_ids(&manager), before);
2652        assert_eq!(manager.stats().consolidation.runs_completed, 1);
2653    }
2654
2655    #[test]
2656    fn consolidation_preserves_entity_payload_metadata_and_sequence() {
2657        let manager = consolidating_manager(CONSOLIDATION_ENTITIES_PER_TICK);
2658
2659        // Metadata must be attached before the seal: `set_metadata` on a sealed
2660        // row rewrites it into the growing segment, which would take it out of
2661        // the segment under test.
2662        let entities: Vec<UnifiedEntity> = (0..10)
2663            .map(|i| {
2664                UnifiedEntity::table_row(
2665                    manager.next_entity_id(),
2666                    "users",
2667                    i as u64,
2668                    vec![Value::Integer(i as i64)],
2669                )
2670            })
2671            .collect();
2672        let ids = manager.bulk_insert(entities).expect("bulk insert");
2673
2674        let mut meta = Metadata::new();
2675        meta.set(
2676            "os",
2677            super::super::metadata::MetadataValue::String("linux".to_string()),
2678        );
2679        manager.set_metadata(ids[9], meta).expect("set metadata");
2680        manager.force_seal().expect("seal");
2681
2682        assert!(manager.delete(ids[0]).expect("delete"));
2683        assert!(manager.delete(ids[1]).expect("delete"));
2684        assert!(manager.delete(ids[2]).expect("delete"));
2685
2686        let before: Vec<UnifiedEntity> = {
2687            let mut entities = manager.query_all(|_| true);
2688            entities.sort_by_key(|entity| entity.id.raw());
2689            entities
2690        };
2691
2692        drain_maintenance(&manager);
2693
2694        let after: Vec<UnifiedEntity> = {
2695            let mut entities = manager.query_all(|_| true);
2696            entities.sort_by_key(|entity| entity.id.raw());
2697            entities
2698        };
2699
2700        let columns = |entity: &UnifiedEntity| match &entity.data {
2701            super::super::entity::EntityData::Row(row) => row.columns.clone(),
2702            _ => Vec::new(),
2703        };
2704
2705        assert_eq!(before.len(), after.len());
2706        for (old, new) in before.iter().zip(after.iter()) {
2707            assert_eq!(old.id, new.id);
2708            assert_eq!(columns(old), columns(new));
2709            assert_eq!(
2710                old.sequence_id, new.sequence_id,
2711                "merge must not re-issue sequence ids"
2712            );
2713        }
2714        assert!(manager
2715            .get_metadata(ids[9])
2716            .expect("metadata survives the merge")
2717            .has("os"));
2718    }
2719
2720    #[test]
2721    #[ignore = "lifecycle events intentionally no-op since the emit-channel refactor; drain_events returns empty — see SegmentManager::emit"]
2722    fn test_lifecycle_events() {
2723        let manager = SegmentManager::new("test");
2724
2725        manager
2726            .insert(UnifiedEntity::vector(
2727                manager.next_entity_id(),
2728                "v",
2729                vec![0.1],
2730            ))
2731            .unwrap();
2732
2733        let events = manager.drain_events();
2734
2735        // Should have: SegmentCreated, EntityInserted
2736        assert!(events
2737            .iter()
2738            .any(|e| matches!(e, LifecycleEvent::SegmentCreated(_))));
2739        assert!(events
2740            .iter()
2741            .any(|e| matches!(e, LifecycleEvent::EntityInserted(_, _))));
2742    }
2743}