Skip to main content

reddb_engine/storage/
segment.rs

1//! Unified Segment System
2//!
3//! Implements the Growing → Sealed segment lifecycle pattern. Segments are the
4//! fundamental unit of storage that handle entities of all types.
5//!
6//! # Write Path
7//!
8//! 1. WAL write (durability)
9//! 2. Growing-segment entity storage in RAM (HashMap mode or flat-vector mode
10//!    with epoch-published lock-free reads)
11//! 3. `seal()` freezes the growing segment into a Sealed segment and builds
12//!    pruning summaries over the immutable data
13//!
14//! # Read Path
15//!
16//! 1. Growing segment entity storage (most recent writes)
17//! 2. Sealed segments (older, immutable, zone-map guarded)
18//!
19//! # Lifecycle
20//!
21//! ```text
22//! Growing (in-memory, accepts writes)
23//!    ↓ seal() when full or manually triggered
24//! Sealed (immutable, zone maps built)
25//!    ↓ flush() for persistence
26//! Flushed (on disk, can be mmap'd)
27//!    ↓ archive() for cold storage
28//! Archived (compressed, infrequently accessed)
29//! ```
30
31use std::collections::{BTreeMap, HashMap, HashSet};
32use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
33
34use super::entity::{CrossRef, EntityData, EntityId, EntityKind, RefType, UnifiedEntity};
35use super::metadata::{Metadata, MetadataFilter, MetadataStorage};
36use super::segment_graph_index::SegmentGraphIndex;
37use super::segment_key_lookup::SegmentKeyLookup;
38use super::segment_unique_key::SegmentUniqueKey;
39use reddb_types::value_compare::partial_compare_values;
40use reddb_types::{value_to_canonical_key, CanonicalKey, Value};
41
42/// Unique identifier for a segment
43pub type SegmentId = u64;
44
45/// Segment state in its lifecycle
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum SegmentState {
48    /// Accepts writes, partial/no index
49    Growing,
50    /// Transitioning, building indices
51    Sealing,
52    /// Immutable, fully indexed
53    Sealed,
54    /// Persisted to disk
55    Flushed,
56    /// Compressed, cold storage
57    Archived,
58}
59
60impl SegmentState {
61    /// Check if segment accepts writes
62    pub fn is_writable(&self) -> bool {
63        matches!(self, Self::Growing)
64    }
65
66    /// Check if segment is queryable
67    pub fn is_queryable(&self) -> bool {
68        !matches!(self, Self::Sealing)
69    }
70
71    /// Check if segment is immutable
72    pub fn is_immutable(&self) -> bool {
73        matches!(self, Self::Sealed | Self::Flushed | Self::Archived)
74    }
75}
76
77/// Configuration for segments
78#[derive(Debug, Clone)]
79pub struct SegmentConfig {
80    /// Maximum entities before auto-sealing
81    pub max_entities: usize,
82    /// Maximum memory bytes before auto-sealing
83    pub max_bytes: usize,
84    /// Maximum age in seconds before auto-sealing
85    pub max_age_secs: u64,
86    /// Enable vector indexing when sealed
87    pub build_vector_index: bool,
88    /// Enable graph indexing when sealed
89    pub build_graph_index: bool,
90    /// Compression level for archived segments (0-9)
91    pub compression_level: u8,
92}
93
94impl Default for SegmentConfig {
95    fn default() -> Self {
96        Self {
97            max_entities: 100_000,
98            max_bytes: 256 * 1024 * 1024, // 256 MB
99            max_age_secs: 3600,           // 1 hour
100            build_vector_index: true,
101            build_graph_index: true,
102            compression_level: 6,
103        }
104    }
105}
106
107/// Segment statistics
108#[derive(Debug, Clone, Default)]
109pub struct SegmentStats {
110    /// Number of entities
111    pub entity_count: usize,
112    /// Number of deleted entities
113    pub deleted_count: usize,
114    /// Approximate memory usage in bytes
115    pub memory_bytes: usize,
116    /// Number of vectors
117    pub vector_count: usize,
118    /// Number of graph nodes
119    pub node_count: usize,
120    /// Number of graph edges
121    pub edge_count: usize,
122    /// Number of table rows
123    pub row_count: usize,
124    /// Number of cross-references
125    pub cross_ref_count: usize,
126}
127
128/// Segment error types
129#[derive(Debug, Clone)]
130pub enum SegmentError {
131    /// Segment is not writable
132    NotWritable,
133    /// Entity not found
134    NotFound(EntityId),
135    /// Entity already exists
136    AlreadyExists(EntityId),
137    /// Segment is full
138    Full,
139    /// Invalid operation for current state
140    InvalidState(SegmentState),
141    /// Internal error
142    Internal(String),
143}
144
145impl std::fmt::Display for SegmentError {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            Self::NotWritable => write!(f, "segment is not writable"),
149            Self::NotFound(id) => write!(f, "entity not found: {}", id),
150            Self::AlreadyExists(id) => write!(f, "entity already exists: {}", id),
151            Self::Full => write!(f, "segment is full"),
152            Self::InvalidState(state) => write!(f, "invalid operation for state: {:?}", state),
153            Self::Internal(msg) => write!(f, "internal error: {}", msg),
154        }
155    }
156}
157
158impl std::error::Error for SegmentError {}
159
160use crate::clock::current_unix_secs;
161
162fn entity_id_from_probe_key(key: &[u8]) -> Option<EntityId> {
163    if let Ok(text) = std::str::from_utf8(key) {
164        if let Ok(raw) = text.parse::<u64>() {
165            return Some(EntityId::new(raw));
166        }
167    }
168
169    let bytes: [u8; 8] = key.try_into().ok()?;
170    Some(EntityId::new(u64::from_le_bytes(bytes)))
171}
172
173const SEALED_MULTI_ZONE_MAX_INTERVALS: usize = 4;
174
175/// Approximate cost of one entry in the `deleted` tombstone map: the entity
176/// id and deletion ordinal plus hashbrown's control byte and load-factor slack.
177///
178/// Tombstones outlive the entity they bury — a sealed segment holds its
179/// `deleted` map for as long as the segment lives. That map is the memory
180/// consolidation reclaims even in HashMap mode, where the entity body itself
181/// was already freed by `force_delete`.
182const TOMBSTONE_ENTRY_BYTES: u64 = 24;
183
184#[derive(Debug, Clone)]
185struct UpdateIndexSnapshot {
186    pk_column_name: Option<String>,
187    pk_value: Option<Value>,
188    pk_index_key: Option<(String, String)>,
189    cross_refs: Vec<CrossRef>,
190}
191
192impl UpdateIndexSnapshot {
193    fn from_entity(entity: &UnifiedEntity) -> Self {
194        let (pk_column_name, pk_value) = match &entity.data {
195            EntityData::Row(row) => (
196                row.schema
197                    .as_deref()
198                    .and_then(|schema| schema.first().cloned()),
199                row.columns.first().cloned(),
200            ),
201            _ => (None, None),
202        };
203        let pk_index_key = pk_value
204            .as_ref()
205            .map(|value| (entity.kind.collection().to_string(), format!("{:?}", value)));
206        Self {
207            pk_column_name,
208            pk_value,
209            pk_index_key,
210            cross_refs: entity.cross_refs().to_vec(),
211        }
212    }
213}
214
215/// A unified segment that stores all entity types
216pub trait UnifiedSegment: Send + Sync {
217    /// Get segment ID
218    fn id(&self) -> SegmentId;
219
220    /// Get current state
221    fn state(&self) -> SegmentState;
222
223    /// Get collection/namespace name
224    fn collection(&self) -> &str;
225
226    /// Get statistics
227    fn stats(&self) -> SegmentStats;
228
229    /// O(1) live entity count (entities minus tombstones)
230    fn entity_count(&self) -> usize;
231
232    /// Check if entity exists
233    fn contains(&self, id: EntityId) -> bool;
234
235    /// Get an entity by ID
236    fn get(&self, id: EntityId) -> Option<&UnifiedEntity>;
237
238    /// Get mutable reference to entity
239    fn get_mut(&mut self, id: EntityId) -> Option<&mut UnifiedEntity>;
240
241    /// Insert a new entity
242    fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError>;
243
244    /// Update an existing entity
245    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError>;
246
247    /// HOT-update: like update but receives the set of field names that actually
248    /// changed. Allows skipping index work when indexed columns are unaffected.
249    /// Default: falls back to full update.
250    fn update_hot(
251        &mut self,
252        entity: UnifiedEntity,
253        modified_columns: &[String],
254    ) -> Result<(), SegmentError> {
255        let _ = modified_columns;
256        self.update(entity)
257    }
258
259    /// Delete an entity
260    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError>;
261
262    /// Get metadata for an entity
263    fn get_metadata(&self, id: EntityId) -> Option<Metadata>;
264
265    /// Set metadata for an entity
266    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError>;
267
268    /// Seal the segment (make immutable)
269    fn seal(&mut self) -> Result<(), SegmentError>;
270
271    /// Check if should auto-seal based on config
272    fn should_seal(&self, config: &SegmentConfig) -> bool;
273
274    /// Iterate over all entities
275    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
276
277    /// Iterate over entities of a specific kind
278    fn iter_kind(&self, kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
279
280    /// Search entities by metadata filter
281    fn filter_metadata(
282        &self,
283        filters: &[(String, super::metadata::MetadataFilter)],
284    ) -> Vec<EntityId>;
285}
286
287// ─────────────────────────────────────────────────────────────────────────────
288// Zone map: per-column min/max for segment pruning
289// ─────────────────────────────────────────────────────────────────────────────
290
291/// Tracks the min and max observed `Value` for one column in a segment.
292/// Used to skip segments that cannot satisfy a range or equality predicate.
293#[derive(Debug, Clone)]
294pub struct ColZone {
295    pub min: Value,
296    pub max: Value,
297    min_key: Option<CanonicalKey>,
298    max_key: Option<CanonicalKey>,
299}
300
301impl ColZone {
302    fn payload_bytes(&self) -> usize {
303        super::memory_size::value_heap_bytes(&self.min)
304            .saturating_add(super::memory_size::value_heap_bytes(&self.max))
305            .saturating_add(
306                self.min_key
307                    .as_ref()
308                    .map_or(0, super::memory_size::canonical_heap_bytes),
309            )
310            .saturating_add(
311                self.max_key
312                    .as_ref()
313                    .map_or(0, super::memory_size::canonical_heap_bytes),
314            )
315    }
316
317    fn new(v: Value) -> Self {
318        Self {
319            min_key: value_to_canonical_key(&v),
320            max_key: value_to_canonical_key(&v),
321            min: v.clone(),
322            max: v,
323        }
324    }
325
326    fn with_bounds(min: Value, max: Value) -> Self {
327        Self {
328            min_key: value_to_canonical_key(&min),
329            max_key: value_to_canonical_key(&max),
330            min,
331            max,
332        }
333    }
334
335    fn update(&mut self, v: &Value) {
336        if compare_zone_values(v, None, &self.min, self.min_key.as_ref())
337            .map(|o| o == std::cmp::Ordering::Less)
338            .unwrap_or(false)
339        {
340            self.min = v.clone();
341            self.min_key = value_to_canonical_key(v);
342        }
343        if compare_zone_values(v, None, &self.max, self.max_key.as_ref())
344            .map(|o| o == std::cmp::Ordering::Greater)
345            .unwrap_or(false)
346        {
347            self.max = v.clone();
348            self.max_key = value_to_canonical_key(v);
349        }
350    }
351}
352
353#[derive(Debug, Clone, Default)]
354pub struct MultiColZone {
355    pub intervals: Vec<ColZone>,
356}
357
358impl MultiColZone {
359    fn can_skip(&self, pred: &ZoneColPred<'_>) -> bool {
360        !self.intervals.is_empty() && self.intervals.iter().all(|zone| pred.can_skip(zone))
361    }
362}
363
364fn compare_zone_values(
365    left: &Value,
366    left_key: Option<&CanonicalKey>,
367    right: &Value,
368    right_key: Option<&CanonicalKey>,
369) -> Option<std::cmp::Ordering> {
370    partial_compare_values(left, right).or_else(|| {
371        let left_key = left_key.cloned().or_else(|| value_to_canonical_key(left))?;
372        let right_key = right_key
373            .cloned()
374            .or_else(|| value_to_canonical_key(right))?;
375        (left_key.family() == right_key.family()).then(|| left_key.cmp(&right_key))
376    })
377}
378
379/// Tag-only variant of `ZoneColPred` — used where the Value is stored separately.
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub enum ZoneColPredKind {
382    Eq,
383    Gt,
384    Gte,
385    Lt,
386    Lte,
387}
388
389/// A predicate on a single column that can be checked against a `ColZone`.
390#[derive(Debug, Clone)]
391pub enum ZoneColPred<'a> {
392    Eq(&'a Value),
393    Gt(&'a Value),
394    Gte(&'a Value),
395    Lt(&'a Value),
396    Lte(&'a Value),
397}
398
399impl<'a> ZoneColPred<'a> {
400    /// Returns `true` when the entire segment can be skipped (no row can match).
401    pub fn can_skip(&self, zone: &ColZone) -> bool {
402        match self {
403            // Equality: skip if val < min OR val > max
404            ZoneColPred::Eq(val) => {
405                compare_zone_values(val, None, &zone.min, zone.min_key.as_ref())
406                    .map(|o| o == std::cmp::Ordering::Less)
407                    .unwrap_or(false)
408                    || compare_zone_values(val, None, &zone.max, zone.max_key.as_ref())
409                        .map(|o| o == std::cmp::Ordering::Greater)
410                        .unwrap_or(false)
411            }
412            // col > val: skip if max <= val (all rows have col ≤ val, none > val)
413            ZoneColPred::Gt(val) => {
414                compare_zone_values(&zone.max, zone.max_key.as_ref(), val, None)
415                    .map(|o| o != std::cmp::Ordering::Greater)
416                    .unwrap_or(false)
417            }
418            // col >= val: skip if max < val
419            ZoneColPred::Gte(val) => {
420                compare_zone_values(&zone.max, zone.max_key.as_ref(), val, None)
421                    .map(|o| o == std::cmp::Ordering::Less)
422                    .unwrap_or(false)
423            }
424            // col < val: skip if min >= val
425            ZoneColPred::Lt(val) => {
426                compare_zone_values(&zone.min, zone.min_key.as_ref(), val, None)
427                    .map(|o| o != std::cmp::Ordering::Less)
428                    .unwrap_or(false)
429            }
430            // col <= val: skip if min > val
431            ZoneColPred::Lte(val) => {
432                compare_zone_values(&zone.min, zone.min_key.as_ref(), val, None)
433                    .map(|o| o == std::cmp::Ordering::Greater)
434                    .unwrap_or(false)
435            }
436        }
437    }
438}
439
440/// The two physical kinds used by graph materialization passes.
441#[derive(Clone, Copy)]
442pub enum GraphEntityKind {
443    Node,
444    Edge,
445}
446
447impl GraphEntityKind {
448    fn mask(self) -> u8 {
449        match self {
450            Self::Node => 1,
451            Self::Edge => 2,
452        }
453    }
454
455    pub fn matches(self, kind: &EntityKind) -> bool {
456        matches!(
457            (self, kind),
458            (Self::Node, EntityKind::GraphNode(_)) | (Self::Edge, EntityKind::GraphEdge(_))
459        )
460    }
461}
462
463/// Maximum physical positions examined under one segment read guard.
464pub const SCAN_BATCH_SIZE: usize = 256;
465
466/// Maximum distinct dirty physical IDs tracked per segment and checkpoint projection.
467pub const CHECKPOINT_MUTATIONS_MAX: usize = 8192;
468
469/// Stable append-only positions, bounded when a scan opens the segment.
470pub struct SegmentScanCursor {
471    flat_end: usize,
472    hash_end: usize,
473    deleted_end: usize,
474    position: usize,
475}
476
477impl SegmentScanCursor {
478    pub fn remaining(&self) -> usize {
479        self.flat_end + self.hash_end - self.position
480    }
481
482    pub fn finished(&self) -> bool {
483        self.remaining() == 0
484    }
485}
486
487/// One indexed key at a time, with an exclusive resume ID and a source-wide
488/// upper bound captured before any batch. MVCC is checked during live hydration.
489pub struct GraphIndexCursor {
490    key_index: usize,
491    after: Option<EntityId>,
492    upper: EntityId,
493}
494
495impl GraphIndexCursor {
496    pub fn finished(&self, keys: &[EntityId]) -> bool {
497        self.key_index == keys.len()
498    }
499}
500
501/// Growing segment implementation (in-memory, writable)
502pub struct GrowingSegment {
503    /// Segment ID
504    id: SegmentId,
505    /// Collection/namespace name
506    collection: String,
507    /// Current state
508    state: SegmentState,
509    /// Creation timestamp
510    created_at: u64,
511    /// Last write timestamp
512    last_write_at: u64,
513
514    /// Entity storage (HashMap for random access)
515    entities: HashMap<EntityId, UnifiedEntity>,
516    /// Append-only positions for HashMap-resident IDs. Deleted entries stay
517    /// until the segment is reclaimed; flat storage already has stable positions.
518    hash_scan_ids: Vec<EntityId>,
519    /// Flat entity storage for bulk inserts (no HashMap overhead, O(1) by offset)
520    /// Used when entity IDs are sequential from base_entity_id
521    flat_entities: Vec<UnifiedEntity>,
522    /// Base entity ID for flat_entities (flat_entities[0].id == base_entity_id)
523    base_entity_id: u64,
524    /// Whether flat storage is active (bulk insert mode)
525    use_flat: bool,
526    /// Deleted entity IDs (tombstones)
527    // Ordinals never change or disappear during the segment lifetime. Cursors
528    // skip old tombstones but keep IDs relocated after their capture boundary.
529    deleted: HashMap<EntityId, usize>,
530    /// Metadata storage (type-aware)
531    metadata: MetadataStorage,
532
533    /// Primary key index: (collection, pk_value) → EntityId
534    pk_index: BTreeMap<(String, String), EntityId>,
535    /// Lazy schema-aware unique-key lookups, independent of the legacy
536    /// first-column index. Protected by the owning segment's entity lock.
537    unique_key_lookup: parking_lot::RwLock<Vec<SegmentUniqueKey>>,
538    unique_key_lookup_bytes: AtomicU64,
539    key_lookup: Option<SegmentKeyLookup>,
540    /// Type index: kind → EntityIds
541    kind_index: HashMap<String, HashSet<EntityId>>,
542    /// Conservative physical presence, independent of kind_index and MVCC.
543    /// Bits only accumulate; unrestricted mutable access sets both bits.
544    graph_kind_mask: u8,
545    /// Retains every physical graph version until physical reclamation.
546    graph_read_index: parking_lot::RwLock<Option<SegmentGraphIndex>>,
547    graph_read_index_bytes: AtomicU64,
548    /// Cross-reference index: source → Vec<(target, ref_type)>
549    cross_ref_forward: HashMap<EntityId, Vec<(EntityId, RefType)>>,
550    /// Reverse cross-reference index: target → Vec<(source, ref_type)>
551    cross_ref_reverse: HashMap<EntityId, Vec<(EntityId, RefType)>>,
552
553    /// Per-column zone maps: col_name → (min, max) for segment pruning
554    col_zones: HashMap<String, ColZone>,
555    zone_payload_bytes: usize,
556    /// Sealed-only minmax-multi summaries built from canonical ordering.
557    sealed_col_zones: HashMap<String, MultiColZone>,
558
559    /// Sequence counter for ordering
560    sequence: AtomicU64,
561    /// Volatile checkpoint invalidation; never part of the storage format.
562    checkpoint_revision: u64,
563    checkpoint_mutations: std::collections::VecDeque<(u64, EntityId)>,
564    // Latest revisions retain complete coverage across repeated edits of the
565    // same bounded set of physical IDs, including versions removed by GC.
566    checkpoint_latest_mutations: HashMap<EntityId, u64>,
567    checkpoint_mutation_floor: u64,
568    /// Approximate payload bytes of the entities physically resident in this
569    /// segment. Resident means "still occupying memory", which includes
570    /// tombstoned entities in flat mode (their slot must stay so positional
571    /// lookup by `id - base_entity_id` keeps working).
572    memory_bytes: AtomicU64,
573    /// Payload bytes of the tombstoned entities that are still resident.
574    /// `memory_bytes - dead_entity_bytes` is the live payload.
575    dead_entity_bytes: u64,
576    /// Count of tombstoned entities that are still resident.
577    dead_resident_count: usize,
578
579    /// Epoch counter for lock-free reads of `flat_entities`.
580    ///
581    /// Updated with `Release` ordering after every flat-mode insert so that
582    /// readers can safely access `flat_entities[0..published_flat_len]` by
583    /// loading with `Acquire` ordering, without holding the segment RwLock.
584    /// Only meaningful when `use_flat == true`; always 0 in HashMap mode.
585    published_flat_len: AtomicUsize,
586}
587
588impl GrowingSegment {
589    pub fn may_contain_graph_kind(&self, kind: GraphEntityKind) -> bool {
590        self.graph_kind_mask & kind.mask() != 0
591    }
592
593    fn observe_graph_kind(&mut self, kind: &EntityKind) {
594        self.graph_kind_mask |= match kind {
595            EntityKind::GraphNode(_) => GraphEntityKind::Node.mask(),
596            EntityKind::GraphEdge(_) => GraphEntityKind::Edge.mask(),
597            _ => 0,
598        };
599    }
600
601    pub fn scan_cursor(&self) -> SegmentScanCursor {
602        SegmentScanCursor {
603            flat_end: self.flat_entities.len(),
604            hash_end: self.hash_scan_ids.len(),
605            deleted_end: self.deleted.len(),
606            position: 0,
607        }
608    }
609
610    /// Append-only slots survive insertions, deletions and sealing. The caller
611    /// owns the segment read guard and must not re-enter storage in `visit`.
612    /// IDs must be hydrated through the manager: retired sources can be stale.
613    pub fn scan_batch(
614        &self,
615        cursor: &mut SegmentScanCursor,
616        before_work: &mut impl FnMut() -> bool,
617        visit: &mut impl FnMut(EntityId),
618    ) -> bool {
619        for _ in 0..SCAN_BATCH_SIZE {
620            if cursor.finished() {
621                break;
622            }
623            if !before_work() {
624                return false;
625            }
626            let id = if cursor.position < cursor.flat_end {
627                self.flat_entities[cursor.position].id
628            } else {
629                self.hash_scan_ids[cursor.position - cursor.flat_end]
630            };
631            cursor.position += 1;
632            let eligible = match self.deleted.get(&id) {
633                Some(&ordinal) => ordinal >= cursor.deleted_end,
634                // Unpublished consolidation eviction leaves an unused position
635                // without a tombstone. It must not become a candidate later.
636                None => self.get(id).is_some(),
637            };
638            if eligible {
639                visit(id);
640            }
641        }
642        true
643    }
644
645    /// Direct iteration without Box<dyn> trait dispatch. Returns false to stop early.
646    /// Uses concrete iterator types to avoid heap allocation per call.
647    #[inline]
648    pub fn for_each_fast<F>(&self, mut f: F) -> bool
649    where
650        F: FnMut(&UnifiedEntity) -> bool,
651    {
652        if self.use_flat {
653            // Sequential Vec — best cache locality
654            if self.deleted.is_empty() {
655                for entity in &self.flat_entities {
656                    if !f(entity) {
657                        return false;
658                    }
659                }
660                // Also walk the HashMap — entities added via per-row
661                // `insert()` after flat mode was initialized land there.
662                for entity in self.entities.values() {
663                    if !f(entity) {
664                        return false;
665                    }
666                }
667            } else {
668                for entity in &self.flat_entities {
669                    if self.deleted.contains_key(&entity.id) {
670                        continue;
671                    }
672                    if !f(entity) {
673                        return false;
674                    }
675                }
676                for entity in self.entities.values() {
677                    if self.deleted.contains_key(&entity.id) {
678                        continue;
679                    }
680                    if !f(entity) {
681                        return false;
682                    }
683                }
684            }
685        } else {
686            // HashMap values — random order, no boxing
687            if self.deleted.is_empty() {
688                for entity in self.entities.values() {
689                    if !f(entity) {
690                        return false;
691                    }
692                }
693            } else {
694                for entity in self.entities.values() {
695                    if self.deleted.contains_key(&entity.id) {
696                        continue;
697                    }
698                    if !f(entity) {
699                        return false;
700                    }
701                }
702            }
703        }
704        true
705    }
706
707    /// Create a new growing segment
708    pub fn new(id: SegmentId, collection: impl Into<String>) -> Self {
709        let now = current_unix_secs();
710
711        Self {
712            id,
713            collection: collection.into(),
714            state: SegmentState::Growing,
715            created_at: now,
716            last_write_at: now,
717            entities: HashMap::new(),
718            hash_scan_ids: Vec::new(),
719            flat_entities: Vec::new(),
720            base_entity_id: 0,
721            use_flat: false,
722            deleted: HashMap::new(),
723            metadata: MetadataStorage::new(),
724            pk_index: BTreeMap::new(),
725            unique_key_lookup: parking_lot::RwLock::new(Vec::new()),
726            unique_key_lookup_bytes: AtomicU64::new(0),
727            key_lookup: Some(SegmentKeyLookup::default()),
728            kind_index: HashMap::new(),
729            graph_read_index: parking_lot::RwLock::new(Some(SegmentGraphIndex::default())),
730            graph_read_index_bytes: AtomicU64::new(0),
731            graph_kind_mask: 0,
732            cross_ref_forward: HashMap::new(),
733            cross_ref_reverse: HashMap::new(),
734            col_zones: HashMap::new(),
735            zone_payload_bytes: 0,
736            sealed_col_zones: HashMap::new(),
737            sequence: AtomicU64::new(0),
738            checkpoint_revision: 0,
739            checkpoint_mutations: std::collections::VecDeque::new(),
740            checkpoint_latest_mutations: HashMap::new(),
741            checkpoint_mutation_floor: 0,
742            memory_bytes: AtomicU64::new(0),
743            dead_entity_bytes: 0,
744            dead_resident_count: 0,
745            published_flat_len: AtomicUsize::new(0),
746        }
747    }
748
749    /// Get next sequence number
750    fn next_sequence(&self) -> u64 {
751        self.sequence.fetch_add(1, Ordering::SeqCst)
752    }
753
754    pub fn checkpoint_revision(&self) -> u64 {
755        self.checkpoint_revision
756    }
757
758    fn mark_checkpoint_entity_dirty(&mut self, id: EntityId) {
759        self.checkpoint_revision = self
760            .checkpoint_revision
761            .checked_add(1)
762            .expect("segment mutation counter must not overflow");
763        if !self.checkpoint_latest_mutations.contains_key(&id)
764            && self.checkpoint_latest_mutations.len() == CHECKPOINT_MUTATIONS_MAX
765        {
766            // Evict the least recently changed ID. A baseline before its last
767            // change no longer has complete coverage and must fail closed.
768            while let Some((revision, previous)) = self.checkpoint_mutations.pop_front() {
769                if self.checkpoint_latest_mutations.get(&previous) == Some(&revision) {
770                    self.checkpoint_latest_mutations.remove(&previous);
771                    self.checkpoint_mutation_floor = self.checkpoint_mutation_floor.max(revision);
772                    break;
773                }
774            }
775        }
776        self.checkpoint_latest_mutations
777            .insert(id, self.checkpoint_revision);
778        if self.checkpoint_mutations.len() == 2 * CHECKPOINT_MUTATIONS_MAX {
779            // Stale duplicate entries do not spend the distinct-ID budget.
780            // Compact at most once per bound's worth of further mutations.
781            self.checkpoint_mutations.retain(|(revision, previous)| {
782                self.checkpoint_latest_mutations.get(previous) == Some(revision)
783            });
784        }
785        self.checkpoint_mutations
786            .push_back((self.checkpoint_revision, id));
787    }
788
789    pub fn checkpoint_mutations_since(&self, revision: u64) -> Option<Vec<EntityId>> {
790        if revision == self.checkpoint_revision {
791            return Some(Vec::new());
792        }
793        if revision < self.checkpoint_mutation_floor || revision > self.checkpoint_revision {
794            return None;
795        }
796        let mut ids = Vec::new();
797        for &(sequence, id) in &self.checkpoint_mutations {
798            if sequence > revision && self.checkpoint_latest_mutations.get(&id) == Some(&sequence) {
799                ids.push(id);
800            }
801        }
802        Some(ids)
803    }
804
805    pub fn checkpoint_tracking_bytes(&self) -> usize {
806        self.checkpoint_mutations.capacity() * std::mem::size_of::<(u64, EntityId)>()
807            // Include conservative hash-table allocation/accounting overhead.
808            + self.checkpoint_latest_mutations.capacity()
809                * (std::mem::size_of::<(EntityId, u64)>() + 16)
810    }
811
812    fn has_live_entity(&self, id: EntityId) -> bool {
813        if self.deleted.contains_key(&id) {
814            return false;
815        }
816        if self.use_flat {
817            let raw = id.raw();
818            if raw >= self.base_entity_id {
819                let idx = (raw - self.base_entity_id) as usize;
820                if self
821                    .flat_entities
822                    .get(idx)
823                    .is_some_and(|entity| entity.id == id)
824                {
825                    return true;
826                }
827            }
828            // Same fallback as `get()` — entities added via `insert()`
829            // after flat mode was initialized live only in the HashMap.
830            self.entities.contains_key(&id)
831        } else {
832            self.entities.contains_key(&id)
833        }
834    }
835
836    fn update_existing_entity_in_place(
837        &mut self,
838        entity: &UnifiedEntity,
839    ) -> Result<UpdateIndexSnapshot, SegmentError> {
840        self.mark_checkpoint_entity_dirty(entity.id);
841        self.invalidate_unique_key_lookup();
842        if self
843            .get(entity.id)
844            .is_some_and(|old| SegmentKeyLookup::key(old) != SegmentKeyLookup::key(entity))
845        {
846            if let Some(mut index) = self.key_lookup.take() {
847                index.remove(
848                    self.get(entity.id)
849                        .expect("invariant: existing replacement row"),
850                );
851                index.insert(entity);
852                self.key_lookup = Some(index);
853            }
854        }
855        if (self.graph_kind_mask != 0
856            || matches!(
857                entity.kind,
858                EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
859            ))
860            && self
861                .get(entity.id)
862                .is_some_and(|old| !SegmentGraphIndex::same_keys(old, entity))
863        {
864            self.invalidate_graph_read_index();
865        }
866        // Record before replacement, including forced sealed updates. Failed
867        // updates may leave a false positive, never a false negative.
868        self.observe_graph_kind(&entity.kind);
869        if self.use_flat {
870            let raw = entity.id.raw();
871            if raw >= self.base_entity_id {
872                let idx = (raw - self.base_entity_id) as usize;
873                if let Some(slot) = self
874                    .flat_entities
875                    .get_mut(idx)
876                    .filter(|slot| slot.id == entity.id)
877                {
878                    let snapshot = UpdateIndexSnapshot::from_entity(slot);
879                    let old_bytes = Self::estimate_entity_size(slot);
880                    slot.clone_from(entity);
881                    self.replace_entity_memory(old_bytes, Self::estimate_entity_size(entity));
882                    return Ok(snapshot);
883                }
884            }
885            let Some(slot) = self.entities.get_mut(&entity.id) else {
886                return Err(SegmentError::NotFound(entity.id));
887            };
888            let snapshot = UpdateIndexSnapshot::from_entity(slot);
889            let old_bytes = Self::estimate_entity_size(slot);
890            slot.clone_from(entity);
891            self.replace_entity_memory(old_bytes, Self::estimate_entity_size(entity));
892            Ok(snapshot)
893        } else {
894            let Some(slot) = self.entities.get_mut(&entity.id) else {
895                return Err(SegmentError::NotFound(entity.id));
896            };
897            let snapshot = UpdateIndexSnapshot::from_entity(slot);
898            let old_bytes = Self::estimate_entity_size(slot);
899            slot.clone_from(entity);
900            self.replace_entity_memory(old_bytes, Self::estimate_entity_size(entity));
901            Ok(snapshot)
902        }
903    }
904
905    fn apply_hot_update_with_metadata(
906        &mut self,
907        entity: &UnifiedEntity,
908        modified_columns: &[String],
909        metadata: Option<&Metadata>,
910    ) -> Result<(), SegmentError> {
911        let old = self.update_existing_entity_in_place(entity)?;
912        self.reindex_for_update(&old, entity, Some(modified_columns));
913        self.update_col_zones_from_entity(entity);
914        if let Some(metadata) = metadata {
915            self.metadata.set_all(entity.id, metadata);
916        }
917        Ok(())
918    }
919
920    fn apply_update_with_metadata(
921        &mut self,
922        entity: &UnifiedEntity,
923        metadata: Option<&Metadata>,
924    ) -> Result<(), SegmentError> {
925        let old = self.update_existing_entity_in_place(entity)?;
926        self.reindex_for_update(&old, entity, None);
927        self.update_col_zones_from_entity(entity);
928        if let Some(metadata) = metadata {
929            self.metadata.set_all(entity.id, metadata);
930        }
931        Ok(())
932    }
933
934    pub fn update_hot_batch_with_metadata<'a, I>(&mut self, items: I) -> Result<(), SegmentError>
935    where
936        I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
937    {
938        if !self.state.is_writable() {
939            return Err(SegmentError::NotWritable);
940        }
941
942        let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
943            items.into_iter().collect();
944        if items.is_empty() {
945            return Ok(());
946        }
947
948        for (entity, _, _) in &items {
949            if !self.has_live_entity(entity.id) {
950                return Err(SegmentError::NotFound(entity.id));
951            }
952        }
953
954        for (entity, modified_columns, metadata) in items {
955            self.apply_hot_update_with_metadata(entity, modified_columns, metadata)?;
956        }
957
958        self.last_write_at = current_unix_secs();
959        Ok(())
960    }
961
962    /// Preserve the first removal order, including repeated flat deletes.
963    fn record_tombstone(&mut self, id: EntityId) -> bool {
964        self.mark_checkpoint_entity_dirty(id);
965        let ordinal = self.deleted.len();
966        match self.deleted.entry(id) {
967            std::collections::hash_map::Entry::Vacant(entry) => {
968                entry.insert(ordinal);
969                true
970            }
971            std::collections::hash_map::Entry::Occupied(_) => false,
972        }
973    }
974
975    /// Tombstone a flat-mode entity in place. Its slot stays so that positional
976    /// lookup by `id - base_entity_id` keeps working, so its payload keeps
977    /// costing memory until consolidation merges the segment away.
978    ///
979    /// Returns `false` when the entity was already tombstoned.
980    fn tombstone_flat_entity(&mut self, idx: usize, id: EntityId) -> bool {
981        self.invalidate_unique_key_lookup();
982        if !self.record_tombstone(id) {
983            return false;
984        }
985        if let Some(index) = self.key_lookup.as_mut() {
986            index.remove(&self.flat_entities[idx]);
987        }
988        if let Some(index) = self.graph_read_index.get_mut() {
989            index.remove(&self.flat_entities[idx]);
990            self.graph_read_index_bytes
991                .store(index.memory_bytes(), Ordering::Relaxed);
992        }
993        let size = Self::estimate_entity_size(&self.flat_entities[idx]) as u64;
994        self.dead_entity_bytes += size;
995        self.dead_resident_count += 1;
996        self.metadata.remove_all(id);
997        true
998    }
999
1000    /// Remove a HashMap-resident entity. The payload is freed immediately; only
1001    /// the tombstone survives, and consolidation reclaims that.
1002    ///
1003    /// Returns `false` when the entity is not present.
1004    fn remove_hashmap_entity(&mut self, id: EntityId) -> bool {
1005        self.invalidate_unique_key_lookup();
1006        let Some(entity) = self.entities.remove(&id) else {
1007            return false;
1008        };
1009        self.release_memory(Self::estimate_entity_size(&entity));
1010        self.unindex_entity(&entity);
1011        self.metadata.remove_all(id);
1012        self.record_tombstone(id);
1013        true
1014    }
1015
1016    pub fn delete_batch(&mut self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
1017        if !self.state.is_writable() {
1018            return Err(SegmentError::NotWritable);
1019        }
1020        if ids.is_empty() {
1021            return Ok(Vec::new());
1022        }
1023
1024        let mut deleted_ids = Vec::with_capacity(ids.len());
1025
1026        if self.use_flat {
1027            for &id in ids {
1028                let raw = id.raw();
1029                if raw < self.base_entity_id {
1030                    if self.remove_hashmap_entity(id) {
1031                        deleted_ids.push(id);
1032                    }
1033                    continue;
1034                }
1035                let idx = (raw - self.base_entity_id) as usize;
1036                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
1037                    if self.tombstone_flat_entity(idx, id) {
1038                        deleted_ids.push(id);
1039                    }
1040                } else if self.remove_hashmap_entity(id) {
1041                    deleted_ids.push(id);
1042                }
1043            }
1044        } else {
1045            for &id in ids {
1046                if self.remove_hashmap_entity(id) {
1047                    deleted_ids.push(id);
1048                }
1049            }
1050        }
1051
1052        if !deleted_ids.is_empty() {
1053            self.last_write_at = current_unix_secs();
1054        }
1055
1056        Ok(deleted_ids)
1057    }
1058
1059    /// Update memory estimate
1060    fn add_memory(&self, bytes: usize) {
1061        self.memory_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
1062    }
1063
1064    fn replace_entity_memory(&self, previous: usize, current: usize) {
1065        if current >= previous {
1066            self.add_memory(current - previous);
1067        } else {
1068            self.release_memory(previous - current);
1069        }
1070    }
1071
1072    /// Boot-time representation compaction. The manager proves exclusive
1073    /// ownership before calling this; published segments are never rewritten.
1074    pub(crate) fn compact_recovered_rows(&mut self, schema: &std::sync::Arc<Vec<String>>) -> usize {
1075        let mut removed = 0usize;
1076        let mut added = 0usize;
1077        let mut compacted = 0;
1078        for entity in self
1079            .entities
1080            .values_mut()
1081            .chain(self.flat_entities.iter_mut())
1082        {
1083            let before = super::memory_size::entity_bytes(entity);
1084            if let EntityData::Row(row) = &mut entity.data {
1085                if row.compact_with_schema(schema) {
1086                    removed = removed.saturating_add(before);
1087                    added = added.saturating_add(super::memory_size::entity_bytes(entity));
1088                    compacted += 1;
1089                }
1090            }
1091        }
1092        self.release_memory(removed);
1093        self.add_memory(added);
1094        compacted
1095    }
1096
1097    /// This segment's approximate resident bytes. One relaxed load — the
1098    /// arena's contribution to the shared accounting pool (ADR 0073 §2).
1099    /// Unlike `stats()` this never walks the entity map, so a stats reader
1100    /// does not pay `O(entities)` to observe memory.
1101    pub fn memory_bytes(&self) -> u64 {
1102        self.memory_bytes.load(Ordering::Relaxed)
1103            + self.zone_payload_bytes as u64
1104            + self.unique_key_lookup_bytes.load(Ordering::Relaxed)
1105            + self
1106                .key_lookup
1107                .as_ref()
1108                .map_or(0, SegmentKeyLookup::memory_bytes)
1109            + self.graph_read_index_bytes.load(Ordering::Relaxed)
1110            + (self.hash_scan_ids.capacity() * std::mem::size_of::<EntityId>()) as u64
1111    }
1112
1113    /// Release a resident entity's payload from the memory estimate. Only for
1114    /// paths that physically drop the entity (HashMap removal, eviction).
1115    fn release_memory(&self, bytes: usize) {
1116        let mut current = self.memory_bytes.load(Ordering::Relaxed);
1117        loop {
1118            let next = current.saturating_sub(bytes as u64);
1119            match self.memory_bytes.compare_exchange_weak(
1120                current,
1121                next,
1122                Ordering::Relaxed,
1123                Ordering::Relaxed,
1124            ) {
1125                Ok(_) => break,
1126                Err(observed) => current = observed,
1127            }
1128        }
1129    }
1130
1131    /// Entities physically present, live or tombstoned-but-resident.
1132    pub fn resident_entity_count(&self) -> usize {
1133        self.flat_entities.len() + self.entities.len()
1134    }
1135
1136    /// Entities visible to readers.
1137    pub fn live_entity_count(&self) -> usize {
1138        self.resident_entity_count()
1139            .saturating_sub(self.dead_resident_count)
1140    }
1141
1142    pub(crate) fn is_tombstoned(&self, id: EntityId) -> bool {
1143        self.deleted.contains_key(&id)
1144    }
1145
1146    /// Tombstones this segment carries — including the ones whose entity body
1147    /// was already freed. The set itself is what leaks without consolidation.
1148    pub fn tombstone_count(&self) -> usize {
1149        self.deleted.len()
1150    }
1151
1152    /// Approximate bytes this segment holds: resident entity payloads plus the
1153    /// tombstone set.
1154    pub fn resident_bytes(&self) -> u64 {
1155        self.memory_bytes() + self.deleted.len() as u64 * TOMBSTONE_ENTRY_BYTES
1156    }
1157
1158    /// Bytes a consolidation of this segment would return to the budget: the
1159    /// payloads of resident tombstones plus the tombstone set that buries them.
1160    pub fn reclaimable_bytes(&self) -> u64 {
1161        self.dead_entity_bytes + self.deleted.len() as u64 * TOMBSTONE_ENTRY_BYTES
1162    }
1163
1164    /// Snapshot the ids of every live entity, in iteration order.
1165    ///
1166    /// Consolidation copies a segment across several maintenance ticks, so it
1167    /// needs a stable cursor into the segment that survives concurrent
1168    /// tombstoning. Ids are stable; iteration position is not.
1169    pub fn live_entity_ids(&self) -> Vec<EntityId> {
1170        let mut ids = Vec::with_capacity(self.live_entity_count());
1171        self.for_each_fast(|entity| {
1172            ids.push(entity.id);
1173            true
1174        });
1175        ids
1176    }
1177
1178    /// Move an existing entity into a writable segment without assigning a
1179    /// new sequence identity. A pending update's preimage must still match
1180    /// its durable checkpoint after the MVCC delete stamp is normalized.
1181    pub fn insert_preserving_sequence(
1182        &mut self,
1183        entity: UnifiedEntity,
1184    ) -> Result<EntityId, SegmentError> {
1185        if !self.state.is_writable() {
1186            return Err(SegmentError::NotWritable);
1187        }
1188        if self.entities.contains_key(&entity.id) {
1189            return Err(SegmentError::AlreadyExists(entity.id));
1190        }
1191        let id = entity.id;
1192        self.adopt_entity(entity, None);
1193        self.last_write_at = current_unix_secs();
1194        Ok(id)
1195    }
1196
1197    /// Take an entity from another segment, preserving its identity.
1198    ///
1199    /// Unlike `insert`, this keeps `sequence_id` — the merged segment is the
1200    /// same rows, not new ones, and callers (`physical_collection_roots`, MVCC
1201    /// ordering) read that field. All derived structures are rebuilt: kind
1202    /// index, primary-key index, cross-reference forward/reverse indexes, and
1203    /// per-column zone maps.
1204    pub fn adopt_entity(&mut self, entity: UnifiedEntity, metadata: Option<Metadata>) {
1205        let id = entity.id;
1206
1207        // Keep the sequence counter ahead of every adopted entity, so a later
1208        // insert into this segment cannot re-issue a live sequence number.
1209        let next = entity.sequence_id.saturating_add(1);
1210        if self.sequence.load(Ordering::Relaxed) < next {
1211            self.sequence.store(next, Ordering::Relaxed);
1212        }
1213
1214        self.add_memory(Self::estimate_entity_size(&entity));
1215        self.index_entity(&entity);
1216        self.update_col_zones_from_entity(&entity);
1217        self.entities.insert(id, entity);
1218
1219        if let Some(metadata) = metadata {
1220            if !metadata.is_empty() {
1221                self.metadata.set_all(id, &metadata);
1222            }
1223        }
1224    }
1225
1226    /// Physically remove an entity without leaving a tombstone.
1227    ///
1228    /// Only legal on a segment nobody can see yet — the half-built merged
1229    /// segment, when the swap discovers a copied row was deleted from its
1230    /// source mid-consolidation. A tombstone here would defeat the whole point.
1231    pub fn evict_entity(&mut self, id: EntityId) -> bool {
1232        self.mark_checkpoint_entity_dirty(id);
1233        self.invalidate_unique_key_lookup();
1234        let Some(entity) = self.entities.remove(&id) else {
1235            return false;
1236        };
1237        self.release_memory(Self::estimate_entity_size(&entity));
1238        self.unindex_entity(&entity);
1239        for cross_ref in entity.cross_refs() {
1240            if let Some(reverse) = self.cross_ref_reverse.get_mut(&cross_ref.target) {
1241                reverse.retain(|(source, _)| *source != id);
1242            }
1243        }
1244        self.metadata.remove_all(id);
1245        true
1246    }
1247
1248    /// Estimate memory for an entity
1249    fn estimate_entity_size(entity: &UnifiedEntity) -> usize {
1250        super::memory_size::entity_bytes(entity)
1251    }
1252
1253    /// Update per-column zone maps from a newly inserted entity's fields.
1254    ///
1255    /// Handles both insert paths:
1256    /// - **Named** (`row.named`): individual inserts where fields are a `HashMap<String, Value>`
1257    /// - **Positional** (`row.columns` + `row.schema`): bulk-inserted entities stored as `Vec<Value>`
1258    ///   keyed by the shared schema. Previously this path was silently skipped, meaning zone maps
1259    ///   were always empty for bulk-loaded tables and segment pruning never fired.
1260    fn update_column_zone(&mut self, column: &str, value: &Value) {
1261        if matches!(value, Value::Null) {
1262            return;
1263        }
1264        match self.col_zones.entry(column.to_string()) {
1265            std::collections::hash_map::Entry::Occupied(mut entry) => {
1266                let before = entry.get().payload_bytes();
1267                entry.get_mut().update(value);
1268                self.zone_payload_bytes = self
1269                    .zone_payload_bytes
1270                    .saturating_sub(before)
1271                    .saturating_add(entry.get().payload_bytes());
1272            }
1273            std::collections::hash_map::Entry::Vacant(entry) => {
1274                let zone = ColZone::new(value.clone());
1275                self.zone_payload_bytes =
1276                    self.zone_payload_bytes.saturating_add(zone.payload_bytes());
1277                entry.insert(zone);
1278            }
1279        }
1280    }
1281
1282    fn update_col_zones_from_entity(&mut self, entity: &UnifiedEntity) {
1283        if let EntityData::Row(row) = &entity.data {
1284            if let Some(named) = &row.named {
1285                for (column, value) in named {
1286                    self.update_column_zone(column, value);
1287                }
1288            } else if let Some(schema) = &row.schema {
1289                for (column, value) in schema.iter().zip(&row.columns) {
1290                    self.update_column_zone(column, value);
1291                }
1292            }
1293        }
1294    }
1295
1296    fn rebuild_sealed_col_zones(&mut self) {
1297        let mut values_by_col: HashMap<String, Vec<(CanonicalKey, Value)>> = HashMap::new();
1298        let mut family_by_col: HashMap<String, reddb_types::CanonicalKeyFamily> = HashMap::new();
1299        let mut mixed_family_cols = HashSet::new();
1300        let mut unsupported_cols = HashSet::new();
1301
1302        let mut observe_row = |row: &super::entity::RowData| {
1303            for (col, value) in row.iter_fields() {
1304                if matches!(value, Value::Null) {
1305                    continue;
1306                }
1307                let Some(key) = value_to_canonical_key(value) else {
1308                    unsupported_cols.insert(col.to_string());
1309                    continue;
1310                };
1311                match family_by_col.get(col).copied() {
1312                    Some(existing) if existing != key.family() => {
1313                        mixed_family_cols.insert(col.to_string());
1314                    }
1315                    None => {
1316                        family_by_col.insert(col.to_string(), key.family());
1317                    }
1318                    _ => {}
1319                }
1320                values_by_col
1321                    .entry(col.to_string())
1322                    .or_default()
1323                    .push((key, value.clone()));
1324            }
1325        };
1326
1327        if self.use_flat {
1328            for entity in &self.flat_entities {
1329                if self.deleted.contains_key(&entity.id) {
1330                    continue;
1331                }
1332                if let EntityData::Row(row) = &entity.data {
1333                    observe_row(row);
1334                }
1335            }
1336        } else {
1337            for entity in self.entities.values() {
1338                if self.deleted.contains_key(&entity.id) {
1339                    continue;
1340                }
1341                if let EntityData::Row(row) = &entity.data {
1342                    observe_row(row);
1343                }
1344            }
1345        }
1346
1347        let mut sealed_col_zones = HashMap::new();
1348        for (col, mut entries) in values_by_col {
1349            if mixed_family_cols.contains(&col)
1350                || unsupported_cols.contains(&col)
1351                || entries.is_empty()
1352            {
1353                continue;
1354            }
1355            entries.sort_unstable_by(|left, right| left.0.cmp(&right.0));
1356            entries.dedup_by(|left, right| left.0 == right.0);
1357
1358            let intervals = build_minmax_multi_intervals(&entries, SEALED_MULTI_ZONE_MAX_INTERVALS);
1359            if intervals.len() > 1 {
1360                sealed_col_zones.insert(col, MultiColZone { intervals });
1361            }
1362        }
1363
1364        let previous = self
1365            .sealed_col_zones
1366            .values()
1367            .flat_map(|zones| &zones.intervals)
1368            .map(ColZone::payload_bytes)
1369            .fold(0, usize::saturating_add);
1370        let current = sealed_col_zones
1371            .values()
1372            .flat_map(|zones| &zones.intervals)
1373            .map(ColZone::payload_bytes)
1374            .fold(0, usize::saturating_add);
1375        self.zone_payload_bytes = self
1376            .zone_payload_bytes
1377            .saturating_sub(previous)
1378            .saturating_add(current);
1379        self.sealed_col_zones = sealed_col_zones;
1380    }
1381
1382    /// Returns `true` when this segment can be entirely skipped for the given predicates.
1383    /// A segment is skipped only if ALL predicates say so (conservative: any non-skippable
1384    /// predicate forces the scan to proceed).
1385    pub fn can_skip_zone_preds(&self, preds: &[(&str, ZoneColPred<'_>)]) -> bool {
1386        if preds.is_empty() {
1387            return false;
1388        }
1389        for (col, pred) in preds {
1390            if let Some(zone) = self.sealed_col_zones.get(*col) {
1391                if zone.can_skip(pred) {
1392                    return true;
1393                }
1394                continue;
1395            }
1396            if let Some(zone) = self.col_zones.get(*col) {
1397                if pred.can_skip(zone) {
1398                    return true; // ONE predicate suffices to skip
1399                }
1400            }
1401        }
1402        false
1403    }
1404
1405    /// Index an entity
1406    fn index_entity(&mut self, entity: &UnifiedEntity) {
1407        self.mark_checkpoint_entity_dirty(entity.id);
1408        if let Some(index) = self.key_lookup.as_mut() {
1409            index.insert(entity);
1410        }
1411        self.hash_scan_ids.push(entity.id);
1412        self.observe_graph_kind(&entity.kind);
1413        if let Some(index) = self.graph_read_index.get_mut() {
1414            index.insert(entity);
1415            self.graph_read_index_bytes
1416                .store(index.memory_bytes(), Ordering::Relaxed);
1417        }
1418        let indexes = self.unique_key_lookup.get_mut();
1419        for index in indexes.iter_mut() {
1420            index.insert(entity);
1421        }
1422        self.unique_key_lookup_bytes.store(
1423            indexes
1424                .iter()
1425                .map(|index| index.memory_bytes() as u64)
1426                .sum(),
1427            Ordering::Relaxed,
1428        );
1429        // Kind index
1430        let kind_key = entity.kind.storage_type().to_string();
1431        self.kind_index
1432            .entry(kind_key)
1433            .or_default()
1434            .insert(entity.id);
1435
1436        // Primary key index (if applicable)
1437        if let EntityData::Row(row) = &entity.data {
1438            if let Some(first_col) = row.columns.first() {
1439                let pk_str = format!("{:?}", first_col);
1440                self.pk_index
1441                    .insert((entity.kind.collection().to_string(), pk_str), entity.id);
1442            }
1443        }
1444
1445        // Cross-reference indices
1446        for cross_ref in entity.cross_refs() {
1447            self.cross_ref_forward
1448                .entry(cross_ref.source)
1449                .or_default()
1450                .push((cross_ref.target, cross_ref.ref_type));
1451
1452            self.cross_ref_reverse
1453                .entry(cross_ref.target)
1454                .or_default()
1455                .push((cross_ref.source, cross_ref.ref_type));
1456        }
1457    }
1458
1459    fn invalidate_graph_read_index(&mut self) {
1460        *self.graph_read_index.get_mut() = None;
1461        self.graph_read_index_bytes.store(0, Ordering::Relaxed);
1462    }
1463
1464    /// The owning segment read lock keeps candidates stable. Callers must not
1465    /// re-enter storage in `visit`; collect IDs and evaluate RLS after unlocking.
1466    #[cfg(test)]
1467    pub fn visit_graph_index_candidates(
1468        &self,
1469        kind: GraphEntityKind,
1470        keys: &[EntityId],
1471        before_work: &mut impl FnMut() -> bool,
1472        visit: &mut impl FnMut(&UnifiedEntity) -> bool,
1473    ) -> bool {
1474        let Some(cached) = self.graph_read_index_for_query(before_work) else {
1475            return false;
1476        };
1477        let index = cached.as_ref().expect("complete graph index");
1478        for key in keys {
1479            if !before_work() {
1480                return false;
1481            }
1482            for id in index.candidates(kind, *key) {
1483                if !before_work() {
1484                    return false;
1485                }
1486                if let Some(entity) = self.get(id) {
1487                    if !visit(entity) {
1488                        return false;
1489                    }
1490                }
1491            }
1492        }
1493        true
1494    }
1495
1496    fn graph_read_index_for_query(
1497        &self,
1498        before_work: &mut impl FnMut() -> bool,
1499    ) -> Option<parking_lot::RwLockReadGuard<'_, Option<SegmentGraphIndex>>> {
1500        let mut cached = self.graph_read_index.read();
1501        if cached.is_none() {
1502            drop(cached);
1503            let mut writer = self.graph_read_index.write();
1504            if writer.is_none() {
1505                let mut index = SegmentGraphIndex::default();
1506                // Only unrestricted structural mutation invalidates the index.
1507                // A stopped rebuild is discarded, never published partially.
1508                if !self.for_each_fast(|entity| {
1509                    if !before_work() {
1510                        return false;
1511                    }
1512                    index.insert(entity);
1513                    true
1514                }) {
1515                    return None;
1516                }
1517                self.graph_read_index_bytes
1518                    .store(index.memory_bytes(), Ordering::Relaxed);
1519                *writer = Some(index);
1520            }
1521            cached = parking_lot::RwLockWriteGuard::downgrade(writer);
1522        }
1523        Some(cached)
1524    }
1525
1526    pub fn graph_index_cursor(
1527        &self,
1528        kind: GraphEntityKind,
1529        keys: &[EntityId],
1530        before_work: &mut impl FnMut() -> bool,
1531    ) -> Option<GraphIndexCursor> {
1532        let cached = self.graph_read_index_for_query(before_work)?;
1533        let index = cached.as_ref().expect("complete graph index");
1534        let mut upper = EntityId::new(0);
1535        for key in keys {
1536            if !before_work() {
1537                return None;
1538            }
1539            if let Some(last) = index.candidates(kind, *key).next_back() {
1540                upper = upper.max(last);
1541            }
1542        }
1543        Some(GraphIndexCursor {
1544            key_index: 0,
1545            after: None,
1546            upper,
1547        })
1548    }
1549
1550    /// Collect at most SCAN_BATCH_SIZE physical index entries without payload
1551    /// copies. Resume by key seek, never by rescanning an already consumed prefix.
1552    pub fn graph_index_batch(
1553        &self,
1554        kind: GraphEntityKind,
1555        keys: &[EntityId],
1556        cursor: &mut GraphIndexCursor,
1557        before_work: &mut impl FnMut() -> bool,
1558        visit: &mut impl FnMut(EntityId),
1559    ) -> bool {
1560        self.graph_index_batch_with_limit(kind, keys, cursor, before_work, visit, SCAN_BATCH_SIZE)
1561    }
1562
1563    pub fn graph_index_batch_with_limit(
1564        &self,
1565        kind: GraphEntityKind,
1566        keys: &[EntityId],
1567        cursor: &mut GraphIndexCursor,
1568        before_work: &mut impl FnMut() -> bool,
1569        visit: &mut impl FnMut(EntityId),
1570        limit: usize,
1571    ) -> bool {
1572        assert!(
1573            limit > 0 && limit <= SCAN_BATCH_SIZE,
1574            "bounded graph index batch"
1575        );
1576        let Some(cached) = self.graph_read_index_for_query(before_work) else {
1577            return false;
1578        };
1579        let index = cached.as_ref().expect("complete graph index");
1580        let mut count = 0;
1581        while !cursor.finished(keys) && count < limit {
1582            let key = keys[cursor.key_index];
1583            if !before_work() {
1584                return false;
1585            }
1586            for id in index
1587                .candidates_after(kind, key, cursor.after, cursor.upper)
1588                .take(limit - count)
1589            {
1590                if !before_work() {
1591                    return false;
1592                }
1593                cursor.after = Some(id);
1594                count += 1;
1595                visit(id);
1596            }
1597            if count == limit && cursor.after != Some(cursor.upper) {
1598                break;
1599            }
1600            cursor.key_index += 1;
1601            cursor.after = None;
1602        }
1603        true
1604    }
1605
1606    /// Probe one declared UNIQUE/PRIMARY KEY against physical rows. Cache all
1607    /// current definitions together so alternating constraints never rebuilds
1608    /// the same segment. Schema changes replace the set; mutations invalidate it.
1609    /// The caller owns MVCC conflict policy and null-key admission.
1610    pub fn find_unique_key_conflict(
1611        &self,
1612        columns: &[Vec<String>],
1613        key_index: usize,
1614        signatures: &[String],
1615        exclude_id: Option<EntityId>,
1616        reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1617    ) -> Option<EntityId> {
1618        assert!(
1619            key_index < columns.len(),
1620            "constraint index belongs to current schema"
1621        );
1622        let cached = self.unique_key_lookup.read();
1623        if cached.iter().map(|index| &index.columns).eq(columns.iter()) {
1624            return self.find_unique_key_candidate(
1625                &cached[key_index],
1626                signatures,
1627                exclude_id,
1628                reserves_key,
1629            );
1630        }
1631        drop(cached);
1632        let mut cached = self.unique_key_lookup.write();
1633        if !cached.iter().map(|index| &index.columns).eq(columns.iter()) {
1634            let mut indexes: Vec<_> = columns
1635                .iter()
1636                .map(|columns| SegmentUniqueKey::new(columns))
1637                .collect();
1638            self.for_each_fast(|entity| {
1639                for index in &mut indexes {
1640                    index.insert(entity);
1641                }
1642                true
1643            });
1644            self.unique_key_lookup_bytes.store(
1645                indexes
1646                    .iter()
1647                    .map(|index| index.memory_bytes() as u64)
1648                    .sum(),
1649                Ordering::Relaxed,
1650            );
1651            *cached = indexes;
1652        }
1653        self.find_unique_key_candidate(&cached[key_index], signatures, exclude_id, reserves_key)
1654    }
1655
1656    fn find_unique_key_candidate(
1657        &self,
1658        index: &SegmentUniqueKey,
1659        signatures: &[String],
1660        exclude_id: Option<EntityId>,
1661        reserves_key: &impl Fn(&UnifiedEntity) -> bool,
1662    ) -> Option<EntityId> {
1663        index.candidates(signatures).iter().copied().find(|id| {
1664            if Some(*id) == exclude_id {
1665                return false;
1666            }
1667            let Some(
1668                entity @ UnifiedEntity {
1669                    data: EntityData::Row(row),
1670                    ..
1671                },
1672            ) = self.get(*id)
1673            else {
1674                return false;
1675            };
1676            index
1677                .columns
1678                .iter()
1679                .zip(signatures)
1680                .all(|(column, signature)| {
1681                    row.get_field(column)
1682                        .is_some_and(|value| format!("{value:?}") == *signature)
1683                })
1684                && reserves_key(entity)
1685        })
1686    }
1687
1688    fn invalidate_unique_key_lookup(&mut self) {
1689        *self.unique_key_lookup.get_mut() = Vec::new();
1690        self.unique_key_lookup_bytes.store(0, Ordering::Relaxed);
1691    }
1692
1693    /// Only matching-key candidates reach `visible`; no result vector or row clones.
1694    /// Raw mutable rows are rechecked alongside indexed candidates.
1695    pub fn find_key_entity(
1696        &self,
1697        key: &str,
1698        newest: bool,
1699        visible: &mut impl FnMut(&UnifiedEntity) -> bool,
1700    ) -> Option<&UnifiedEntity> {
1701        // Writers may temporarily take the index while updating its candidates,
1702        // but publish it again before releasing their exclusive segment guard.
1703        let index = self
1704            .key_lookup
1705            .as_ref()
1706            .expect("segment owns its key index");
1707        self.find_key_candidate(index, key, newest, visible)
1708    }
1709
1710    fn find_key_candidate(
1711        &self,
1712        index: &SegmentKeyLookup,
1713        key: &str,
1714        newest: bool,
1715        visible: &mut impl FnMut(&UnifiedEntity) -> bool,
1716    ) -> Option<&UnifiedEntity> {
1717        self.select_key_entity(
1718            index
1719                .candidates(key)
1720                .iter()
1721                .chain(index.mutated_candidates())
1722                .filter_map(|id| self.get(*id)),
1723            key,
1724            newest,
1725            visible,
1726        )
1727    }
1728
1729    fn select_key_entity<'a>(
1730        &self,
1731        entities: impl Iterator<Item = &'a UnifiedEntity>,
1732        key: &str,
1733        newest: bool,
1734        visible: &mut impl FnMut(&UnifiedEntity) -> bool,
1735    ) -> Option<&'a UnifiedEntity> {
1736        let mut best: Option<&UnifiedEntity> = None;
1737        for entity in entities {
1738            // Fingerprint collisions must never become logical-key matches.
1739            if SegmentKeyLookup::key(entity) != Some(key) || !visible(entity) {
1740                continue;
1741            }
1742            if !newest {
1743                return Some(entity);
1744            }
1745            if best.is_none_or(|current| entity.xmin >= current.xmin) {
1746                best = Some(entity);
1747            }
1748        }
1749        best
1750    }
1751
1752    /// Check whether an early-exit probe key exists in this segment.
1753    pub fn may_contain_exact_key(&self, key: &[u8]) -> bool {
1754        if let Some(id) = entity_id_from_probe_key(key) {
1755            return self.has_live_entity(id);
1756        }
1757
1758        let Ok(pk_str) = std::str::from_utf8(key) else {
1759            return false;
1760        };
1761        let pk_key = (self.collection.clone(), pk_str.to_string());
1762        self.pk_index
1763            .get(&pk_key)
1764            .is_some_and(|id| self.has_live_entity(*id))
1765    }
1766
1767    /// Remove entity from indices
1768    fn unindex_entity(&mut self, entity: &UnifiedEntity) {
1769        if let Some(index) = self.key_lookup.as_mut() {
1770            index.remove(entity);
1771        }
1772        if let Some(index) = self.graph_read_index.get_mut() {
1773            index.remove(entity);
1774            self.graph_read_index_bytes
1775                .store(index.memory_bytes(), Ordering::Relaxed);
1776        }
1777        // Kind index
1778        let kind_key = entity.kind.storage_type().to_string();
1779        if let Some(set) = self.kind_index.get_mut(&kind_key) {
1780            set.remove(&entity.id);
1781        }
1782
1783        // Primary key index
1784        if let EntityData::Row(row) = &entity.data {
1785            if let Some(first_col) = row.columns.first() {
1786                let pk_str = format!("{:?}", first_col);
1787                self.pk_index
1788                    .remove(&(entity.kind.collection().to_string(), pk_str));
1789            }
1790        }
1791
1792        // Cross-reference indices
1793        self.cross_ref_forward.remove(&entity.id);
1794        // Note: reverse refs from this entity still need cleanup
1795    }
1796
1797    /// Selective re-index for updates.
1798    ///
1799    /// Skips index work that is not needed:
1800    /// - `kind_index`: entity kind is immutable, so remove+reinsert is always
1801    ///   a no-op; we skip it entirely and keep the existing entry.
1802    /// - `pk_index`: only updated when the primary-key column (first column of
1803    ///   a Row entity) actually changed. When `modified_columns` is provided,
1804    ///   we check membership; otherwise we compare old vs new pk value.
1805    /// - `cross_ref`: only rebuilt when the refs actually differ.
1806    fn reindex_for_update(
1807        &mut self,
1808        old: &UpdateIndexSnapshot,
1809        new: &UnifiedEntity,
1810        modified_columns: Option<&[String]>,
1811    ) {
1812        // kind_index: kind is immutable — the existing entry is already correct.
1813        // No remove + reinsert needed.
1814
1815        // pk_index: only update when pk column is touched
1816        let pk_changed = match &new.data {
1817            EntityData::Row(new_row) => {
1818                if let Some(cols) = modified_columns {
1819                    // Caller told us exactly what changed — check if first schema column modified
1820                    // pk is the first column; check by name against the schema or by position 0
1821                    let pk_col_name = old.pk_column_name.as_deref().or_else(|| {
1822                        new_row
1823                            .schema
1824                            .as_deref()
1825                            .and_then(|schema| schema.first().map(|name| name.as_str()))
1826                    });
1827                    match pk_col_name {
1828                        Some(pk_name) => cols.iter().any(|c| c.eq_ignore_ascii_case(pk_name)),
1829                        // No schema — fall back to value comparison
1830                        None => old.pk_value.as_ref() != new_row.columns.first(),
1831                    }
1832                } else {
1833                    old.pk_value.as_ref() != new_row.columns.first()
1834                }
1835            }
1836            // Non-row types don't use pk_index
1837            _ => false,
1838        };
1839
1840        if pk_changed {
1841            // Remove old pk entry
1842            if let Some((collection, pk_str)) = &old.pk_index_key {
1843                self.pk_index.remove(&(collection.clone(), pk_str.clone()));
1844            }
1845            // Insert new pk entry
1846            if let EntityData::Row(row) = &new.data {
1847                if let Some(first_col) = row.columns.first() {
1848                    let pk_str = format!("{:?}", first_col);
1849                    self.pk_index
1850                        .insert((new.kind.collection().to_string(), pk_str), new.id);
1851                }
1852            }
1853        }
1854
1855        // cross_ref: only rebuild when refs actually changed
1856        let new_refs = new.cross_refs();
1857        if old.cross_refs.as_slice() != new_refs {
1858            // Remove stale forward refs
1859            self.cross_ref_forward.remove(&new.id);
1860            // Prune stale entries from reverse index
1861            for cross_ref in &old.cross_refs {
1862                if let Some(rev) = self.cross_ref_reverse.get_mut(&cross_ref.target) {
1863                    rev.retain(|(src, _)| *src != new.id);
1864                }
1865            }
1866            // Add new refs
1867            for cross_ref in new_refs {
1868                self.cross_ref_forward
1869                    .entry(cross_ref.source)
1870                    .or_default()
1871                    .push((cross_ref.target, cross_ref.ref_type));
1872                self.cross_ref_reverse
1873                    .entry(cross_ref.target)
1874                    .or_default()
1875                    .push((cross_ref.source, cross_ref.ref_type));
1876            }
1877        }
1878    }
1879
1880    /// Get entities referencing the given entity
1881    pub fn get_references_to(&self, id: EntityId) -> Vec<(EntityId, RefType)> {
1882        self.cross_ref_reverse.get(&id).cloned().unwrap_or_default()
1883    }
1884
1885    /// Get entities referenced by the given entity
1886    pub fn get_references_from(&self, id: EntityId) -> Vec<(EntityId, RefType)> {
1887        self.cross_ref_forward.get(&id).cloned().unwrap_or_default()
1888    }
1889
1890    /// Get age in seconds
1891    pub fn age_secs(&self) -> u64 {
1892        let now = current_unix_secs();
1893        now.saturating_sub(self.created_at)
1894    }
1895
1896    /// Get time since last write
1897    pub fn idle_secs(&self) -> u64 {
1898        let now = current_unix_secs();
1899        now.saturating_sub(self.last_write_at)
1900    }
1901
1902    /// Turbo bulk insert — minimal allocations per entity.
1903    ///
1904    /// Optimizations vs normal insert:
1905    /// - Skips cross-refs
1906    /// - Resolves kind_index once per consecutive run of the same storage type
1907    /// - Pre-allocates kind_index HashSet
1908    /// - Skips contains_key check (caller guarantees unique IDs)
1909    /// - Uses Relaxed ordering for sequence counter
1910    pub fn bulk_insert(
1911        &mut self,
1912        entities: Vec<UnifiedEntity>,
1913    ) -> Result<Vec<EntityId>, SegmentError> {
1914        if !self.state.is_writable() {
1915            return Err(SegmentError::NotWritable);
1916        }
1917
1918        for entity in &entities {
1919            self.mark_checkpoint_entity_dirty(entity.id);
1920        }
1921
1922        if let Some(index) = self.key_lookup.as_mut() {
1923            for entity in &entities {
1924                index.insert(entity);
1925            }
1926        }
1927
1928        let indexes = self.unique_key_lookup.get_mut();
1929        for index in indexes.iter_mut() {
1930            for entity in &entities {
1931                index.insert(entity);
1932            }
1933        }
1934        self.unique_key_lookup_bytes.store(
1935            indexes
1936                .iter()
1937                .map(|index| index.memory_bytes() as u64)
1938                .sum(),
1939            Ordering::Relaxed,
1940        );
1941
1942        let n = entities.len();
1943
1944        if entities.is_empty() {
1945            return Ok(Vec::new());
1946        }
1947
1948        // A collection may receive a mixed-model batch. Index each item's actual
1949        // type while retaining batched lookup/reservation for homogeneous input.
1950        for run in
1951            entities.chunk_by(|left, right| left.kind.storage_type() == right.kind.storage_type())
1952        {
1953            let kind = run[0].kind.storage_type();
1954            self.observe_graph_kind(&run[0].kind);
1955            if matches!(
1956                run[0].kind,
1957                EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
1958            ) {
1959                if let Some(index) = self.graph_read_index.get_mut() {
1960                    for entity in run {
1961                        index.insert(entity);
1962                    }
1963                    self.graph_read_index_bytes
1964                        .store(index.memory_bytes(), Ordering::Relaxed);
1965                }
1966            }
1967            let kind_set = match self.kind_index.get_mut(kind) {
1968                Some(ids) => ids,
1969                None => self.kind_index.entry(kind.to_string()).or_default(),
1970            };
1971            kind_set.reserve(run.len());
1972            kind_set.extend(run.iter().map(|entity| entity.id));
1973        }
1974
1975        let now = current_unix_secs();
1976
1977        let base_seq = self.sequence.fetch_add(n as u64, Ordering::Relaxed);
1978
1979        let mut ids = Vec::with_capacity(n);
1980
1981        // Use flat storage (Vec) instead of HashMap — saves ~80 bytes/entity overhead
1982        if self.flat_entities.is_empty() && self.entities.is_empty() {
1983            // First bulk insert: initialize flat storage
1984            self.base_entity_id = entities.first().map(|e| e.id.raw()).unwrap_or(0);
1985            self.use_flat = true;
1986        }
1987
1988        // Collect zone-update values per column position, not per
1989        // (row, col). The old code did `col.to_string()` + HashMap
1990        // probe per cell — 15 cols × 25k rows = 375K String
1991        // allocations per bulk_insert call for the typed_insert bench.
1992        //
1993        // New shape: accumulate `Vec<Value>` per column index; after
1994        // the scan, walk each non-empty column exactly once, resolve
1995        // its name via the shared schema, and apply all observations
1996        // under a single HashMap entry. 375K allocs → ~ncols allocs.
1997        let mut columnar_zone_updates: Vec<Vec<Value>> = Vec::new();
1998        let mut columnar_schema: Option<std::sync::Arc<Vec<String>>> = None;
1999        // Named-row fallback (non-prevalidated path) still uses the
2000        // per-cell vec — these rows don't share a schema, so there's
2001        // no columnar shortcut.
2002        let mut named_zone_updates: Vec<(String, Value)> = Vec::new();
2003
2004        // Payload bytes for the whole batch, applied with one atomic add.
2005        // `bulk_insert` used to skip memory tracking entirely, which left the
2006        // budget blind to the rows that dominate a bulk-loaded store — and
2007        // blind to what consolidation reclaims from them.
2008        let mut batch_bytes: usize = 0;
2009
2010        if self.use_flat {
2011            self.flat_entities.reserve(n);
2012            for (i, mut entity) in entities.into_iter().enumerate() {
2013                entity.sequence_id = base_seq + i as u64;
2014                let id = entity.id;
2015                ids.push(id);
2016                batch_bytes += Self::estimate_entity_size(&entity);
2017                if let EntityData::Row(row) = &entity.data {
2018                    if row.schema.is_some() && !row.columns.is_empty() {
2019                        if columnar_zone_updates.is_empty() {
2020                            columnar_zone_updates = vec![Vec::with_capacity(n); row.columns.len()];
2021                            columnar_schema = row.schema.clone();
2022                        }
2023                        for (ci, val) in row.columns.iter().enumerate() {
2024                            if !matches!(val, Value::Null) {
2025                                if let Some(bucket) = columnar_zone_updates.get_mut(ci) {
2026                                    bucket.push(val.clone());
2027                                }
2028                            }
2029                        }
2030                    } else {
2031                        for (col, val) in row.iter_fields() {
2032                            if !matches!(val, Value::Null) {
2033                                named_zone_updates.push((col.to_string(), val.clone()));
2034                            }
2035                        }
2036                    }
2037                }
2038                // Position-based `flat_entities` access in `get(id)`
2039                // assumes IDs are contiguous starting at
2040                // `base_entity_id`. When IDs ARE contiguous (the bulk
2041                // ingest hot path) this saves a HashMap probe per
2042                // lookup. But the global ID counter can jump between
2043                // bulk calls — CREATE INDEX, catalog DDL, even other
2044                // collections in the same store reserve IDs from the
2045                // same allocator. If we blindly `push` a gap entity
2046                // its actual position in the Vec no longer matches
2047                // `id - base`, and `flat_entities[id - base].id == id`
2048                // fails — the entity becomes invisible to `get(id)`.
2049                //
2050                // Route gap entities into the HashMap fallback
2051                // (`self.entities`). `get(id)` already falls through
2052                // there when the flat probe misses.
2053                let expected = self.base_entity_id + self.flat_entities.len() as u64;
2054                if id.raw() == expected {
2055                    self.flat_entities.push(entity);
2056                } else {
2057                    self.hash_scan_ids.push(id);
2058                    self.entities.insert(id, entity);
2059                }
2060            }
2061        } else {
2062            // Fallback to HashMap for non-sequential inserts
2063            self.entities.reserve(n);
2064            self.hash_scan_ids.reserve(n);
2065            let mut pairs = Vec::with_capacity(n);
2066            for (i, mut entity) in entities.into_iter().enumerate() {
2067                entity.sequence_id = base_seq + i as u64;
2068                let id = entity.id;
2069                ids.push(id);
2070                batch_bytes += Self::estimate_entity_size(&entity);
2071                if let EntityData::Row(row) = &entity.data {
2072                    for (col, val) in row.iter_fields() {
2073                        if !matches!(val, Value::Null) {
2074                            named_zone_updates.push((col.to_string(), val.clone()));
2075                        }
2076                    }
2077                }
2078                self.hash_scan_ids.push(id);
2079                pairs.push((id, entity));
2080            }
2081            self.entities.extend(pairs);
2082        }
2083
2084        // Apply the accumulated zone updates.
2085        // Columnar path: one `col_zones.entry` call per column (not
2086        // per cell). Named-fallback path: unchanged.
2087        if !columnar_zone_updates.is_empty() {
2088            let schema = columnar_schema.as_ref();
2089            for (ci, values) in columnar_zone_updates.into_iter().enumerate() {
2090                if values.is_empty() {
2091                    continue;
2092                }
2093                let Some(col_name) = schema.and_then(|s| s.get(ci)) else {
2094                    continue;
2095                };
2096                // Enter the HashMap once per column. `raw_entry_mut`
2097                // avoids the String::clone when the column already
2098                // exists in the map — but that's nightly-only, so we
2099                // pay one `clone()` per call-per-column (ncols total)
2100                // instead of the old ncols×nrows.
2101                let mut iter = values.into_iter();
2102                if let Some(first) = iter.next() {
2103                    let previous = self
2104                        .col_zones
2105                        .get(col_name)
2106                        .map_or(0, ColZone::payload_bytes);
2107                    let zone = self
2108                        .col_zones
2109                        .entry(col_name.clone())
2110                        .and_modify(|z| z.update(&first))
2111                        .or_insert_with(|| ColZone::new(first));
2112                    for v in iter {
2113                        zone.update(&v);
2114                    }
2115                    self.zone_payload_bytes = self
2116                        .zone_payload_bytes
2117                        .saturating_sub(previous)
2118                        .saturating_add(zone.payload_bytes());
2119                }
2120            }
2121        }
2122        for (col, val) in named_zone_updates {
2123            self.update_column_zone(&col, &val);
2124        }
2125
2126        self.add_memory(batch_bytes);
2127        self.last_write_at = now;
2128
2129        // Publish the new flat length so lock-free readers can see the new entities.
2130        if self.use_flat {
2131            self.published_flat_len
2132                .store(self.flat_entities.len(), Ordering::Release);
2133        }
2134
2135        Ok(ids)
2136    }
2137
2138    /// Delete from this segment regardless of its seal state.
2139    /// Used to mutate sealed segments when DELETE touches bulk-inserted entities.
2140    pub fn force_delete(&mut self, id: EntityId) -> bool {
2141        if self.use_flat {
2142            let raw = id.raw();
2143            if raw >= self.base_entity_id {
2144                let idx = (raw - self.base_entity_id) as usize;
2145                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
2146                    self.tombstone_flat_entity(idx, id);
2147                    return true;
2148                }
2149            }
2150            return self.remove_hashmap_entity(id);
2151        }
2152
2153        self.remove_hashmap_entity(id)
2154    }
2155
2156    /// Update an entity in this segment regardless of its seal state.
2157    /// Used to mutate sealed segments when UPDATE touches bulk-inserted entities.
2158    pub fn force_update_with_metadata(
2159        &mut self,
2160        entity: &UnifiedEntity,
2161        modified_columns: &[String],
2162        metadata: Option<&Metadata>,
2163    ) -> Result<(), SegmentError> {
2164        self.apply_hot_update_with_metadata(entity, modified_columns, metadata)
2165    }
2166}
2167
2168impl UnifiedSegment for GrowingSegment {
2169    fn id(&self) -> SegmentId {
2170        self.id
2171    }
2172
2173    fn state(&self) -> SegmentState {
2174        self.state
2175    }
2176
2177    fn collection(&self) -> &str {
2178        &self.collection
2179    }
2180
2181    fn stats(&self) -> SegmentStats {
2182        let mut stats = SegmentStats {
2183            entity_count: self.live_entity_count(),
2184            deleted_count: self.deleted.len(),
2185            memory_bytes: self.resident_bytes() as usize,
2186            ..Default::default()
2187        };
2188
2189        for entity in self.entities.values() {
2190            match &entity.kind {
2191                EntityKind::TableRow { .. } => stats.row_count += 1,
2192                EntityKind::GraphNode(_) => stats.node_count += 1,
2193                EntityKind::GraphEdge(_) => stats.edge_count += 1,
2194                EntityKind::Vector { .. } => stats.vector_count += 1,
2195                EntityKind::TimeSeriesPoint(_) => stats.row_count += 1,
2196                EntityKind::QueueMessage { .. } => stats.row_count += 1,
2197            }
2198            stats.cross_ref_count += entity.cross_refs().len();
2199        }
2200
2201        stats
2202    }
2203
2204    fn entity_count(&self) -> usize {
2205        self.live_entity_count()
2206    }
2207
2208    fn contains(&self, id: EntityId) -> bool {
2209        self.has_live_entity(id)
2210    }
2211
2212    fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
2213        if self.deleted.contains_key(&id) {
2214            return None;
2215        }
2216        if self.use_flat {
2217            let raw = id.raw();
2218            if raw >= self.base_entity_id {
2219                let idx = (raw - self.base_entity_id) as usize;
2220                if let Some(entity) = self.flat_entities.get(idx).filter(|e| e.id == id) {
2221                    return Some(entity);
2222                }
2223            }
2224            // Fall through: once flat mode is initialized by a bulk_insert,
2225            // subsequent per-row `insert()` calls still write into the
2226            // HashMap (see the impl below) so reads must check both. Without
2227            // this fallback, a post-bulk single-row insert silently
2228            // disappears from `get()` / `query_all()`.
2229            self.entities.get(&id)
2230        } else {
2231            self.entities.get(&id)
2232        }
2233    }
2234
2235    fn get_mut(&mut self, id: EntityId) -> Option<&mut UnifiedEntity> {
2236        self.mark_checkpoint_entity_dirty(id);
2237        self.invalidate_unique_key_lookup();
2238        self.invalidate_graph_read_index();
2239        if self.deleted.contains_key(&id) || !self.state.is_writable() {
2240            return None;
2241        }
2242        if let Some(mut index) = self.key_lookup.take() {
2243            if let Some(entity) = self.get(id) {
2244                index.mark_mutated(entity);
2245            }
2246            self.key_lookup = Some(index);
2247        }
2248        // The caller can replace the kind through this reference. Conservatively
2249        // disable both graph pruning passes until a new segment is rebuilt.
2250        self.graph_kind_mask = GraphEntityKind::Node.mask() | GraphEntityKind::Edge.mask();
2251        if self.use_flat {
2252            let raw = id.raw();
2253            if raw >= self.base_entity_id {
2254                let idx = (raw - self.base_entity_id) as usize;
2255                if self
2256                    .flat_entities
2257                    .get(idx)
2258                    .map(|e| e.id == id)
2259                    .unwrap_or(false)
2260                {
2261                    return self.flat_entities.get_mut(idx);
2262                }
2263            }
2264            // Fall through to HashMap for entities inserted via
2265            // per-row `insert()` after flat mode was activated.
2266            self.entities.get_mut(&id)
2267        } else {
2268            self.entities.get_mut(&id)
2269        }
2270    }
2271
2272    fn insert(&mut self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2273        if !self.state.is_writable() {
2274            return Err(SegmentError::NotWritable);
2275        }
2276
2277        if self.entities.contains_key(&entity.id) {
2278            return Err(SegmentError::AlreadyExists(entity.id));
2279        }
2280
2281        // Assign sequence ID
2282        entity.sequence_id = self.next_sequence();
2283
2284        // Estimate and track memory
2285        let size = Self::estimate_entity_size(&entity);
2286        self.add_memory(size);
2287
2288        // Index the entity
2289        self.index_entity(&entity);
2290
2291        // Update column zone maps for range-based segment pruning
2292        self.update_col_zones_from_entity(&entity);
2293
2294        // Store
2295        let id = entity.id;
2296        self.entities.insert(id, entity);
2297
2298        // Update write timestamp
2299        self.last_write_at = current_unix_secs();
2300
2301        Ok(id)
2302    }
2303
2304    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2305        if !self.state.is_writable() {
2306            return Err(SegmentError::NotWritable);
2307        }
2308
2309        self.apply_update_with_metadata(&entity, None)?;
2310        self.last_write_at = current_unix_secs();
2311
2312        Ok(())
2313    }
2314
2315    fn update_hot(
2316        &mut self,
2317        entity: UnifiedEntity,
2318        modified_columns: &[String],
2319    ) -> Result<(), SegmentError> {
2320        if !self.state.is_writable() {
2321            return Err(SegmentError::NotWritable);
2322        }
2323
2324        self.apply_hot_update_with_metadata(&entity, modified_columns, None)?;
2325        self.last_write_at = current_unix_secs();
2326        Ok(())
2327    }
2328
2329    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2330        if !self.state.is_writable() {
2331            return Err(SegmentError::NotWritable);
2332        }
2333
2334        // For flat storage, use tombstone (don't remove from Vec to keep indices valid)
2335        if self.use_flat {
2336            let raw = id.raw();
2337            if raw >= self.base_entity_id {
2338                let idx = (raw - self.base_entity_id) as usize;
2339                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
2340                    self.tombstone_flat_entity(idx, id);
2341                    return Ok(true);
2342                }
2343            }
2344            return Ok(self.remove_hashmap_entity(id));
2345        }
2346
2347        Ok(self.remove_hashmap_entity(id))
2348    }
2349
2350    fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2351        if !self.has_live_entity(id) {
2352            return None;
2353        }
2354        Some(self.metadata.get_all(id))
2355    }
2356
2357    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2358        self.mark_checkpoint_entity_dirty(id);
2359        if !self.state.is_writable() {
2360            return Err(SegmentError::NotWritable);
2361        }
2362
2363        if !self.has_live_entity(id) {
2364            return Err(SegmentError::NotFound(id));
2365        }
2366
2367        self.metadata.set_all(id, &metadata);
2368        Ok(())
2369    }
2370
2371    fn seal(&mut self) -> Result<(), SegmentError> {
2372        if self.state != SegmentState::Growing {
2373            return Err(SegmentError::InvalidState(self.state));
2374        }
2375
2376        self.state = SegmentState::Sealing;
2377
2378        // Build indices on the sealed data:
2379        // - HNSW/IVF for vectors (future)
2380        // - B-tree for sorted access (future)
2381        // - Inverted index for text search (future)
2382        self.rebuild_sealed_col_zones();
2383
2384        self.state = SegmentState::Sealed;
2385        Ok(())
2386    }
2387
2388    fn should_seal(&self, config: &SegmentConfig) -> bool {
2389        // Check entity count
2390        if self.entities.len() >= config.max_entities {
2391            return true;
2392        }
2393
2394        // Check memory usage
2395        if self.memory_bytes.load(Ordering::Relaxed) as usize >= config.max_bytes {
2396            return true;
2397        }
2398
2399        // Check age
2400        if self.age_secs() >= config.max_age_secs {
2401            return true;
2402        }
2403
2404        false
2405    }
2406
2407    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2408        let base: Box<dyn Iterator<Item = &UnifiedEntity>> = if self.use_flat {
2409            // Chain flat entities (from bulk_insert) with any entities
2410            // that landed in the HashMap via per-row `insert()` after
2411            // flat mode was activated.
2412            Box::new(self.flat_entities.iter().chain(self.entities.values()))
2413        } else {
2414            Box::new(self.entities.values())
2415        };
2416        if self.deleted.is_empty() {
2417            base
2418        } else {
2419            Box::new(base.filter(|e| !self.deleted.contains_key(&e.id)))
2420        }
2421    }
2422
2423    fn iter_kind(&self, kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2424        // The iterator borrows the segment, so the index cannot change underneath
2425        // it. Borrow the ID set as well instead of copying O(kind count) entries.
2426        let ids = self.kind_index.get(kind_filter);
2427        // In flat mode entities live in `flat_entities`, not `entities` —
2428        // chain both so iter_kind doesn't drop bulk-inserted entities.
2429        let flat: Box<dyn Iterator<Item = &UnifiedEntity>> = if self.use_flat {
2430            Box::new(self.flat_entities.iter())
2431        } else {
2432            Box::new(std::iter::empty())
2433        };
2434        Box::new(flat.chain(self.entities.values()).filter(move |e| {
2435            if self.deleted.contains_key(&e.id) {
2436                return false;
2437            }
2438            if let Some(ids) = ids {
2439                ids.contains(&e.id)
2440            } else {
2441                false
2442            }
2443        }))
2444    }
2445
2446    fn filter_metadata(
2447        &self,
2448        filters: &[(String, super::metadata::MetadataFilter)],
2449    ) -> Vec<EntityId> {
2450        // For growing segments, we iterate and filter. In flat mode the
2451        // IDs live in `flat_entities`, not `entities`, so chain both.
2452        let flat_ids: Box<dyn Iterator<Item = EntityId> + '_> = if self.use_flat {
2453            Box::new(self.flat_entities.iter().map(|e| e.id))
2454        } else {
2455            Box::new(std::iter::empty())
2456        };
2457        flat_ids
2458            .chain(self.entities.keys().copied())
2459            .filter(|id| {
2460                if self.deleted.contains_key(id) {
2461                    return false;
2462                }
2463                let metadata = self.metadata.get_all(*id);
2464                metadata.matches_all(filters)
2465            })
2466            .collect()
2467    }
2468}
2469
2470fn build_minmax_multi_intervals(
2471    entries: &[(CanonicalKey, Value)],
2472    max_intervals: usize,
2473) -> Vec<ColZone> {
2474    if entries.is_empty() {
2475        return Vec::new();
2476    }
2477    if entries.len() == 1 || max_intervals <= 1 {
2478        return vec![ColZone::with_bounds(
2479            entries[0].1.clone(),
2480            entries[entries.len() - 1].1.clone(),
2481        )];
2482    }
2483
2484    let mut split_points = if entries.len() <= max_intervals {
2485        (1..entries.len()).collect::<Vec<_>>()
2486    } else {
2487        let target_splits = max_intervals - 1;
2488        let mut selected = select_gap_split_points(entries, target_splits);
2489        if selected.len() < target_splits {
2490            for bucket in 1..max_intervals {
2491                let idx = bucket * entries.len() / max_intervals;
2492                if idx == 0 || idx >= entries.len() || selected.contains(&idx) {
2493                    continue;
2494                }
2495                selected.push(idx);
2496                if selected.len() >= target_splits {
2497                    break;
2498                }
2499            }
2500        }
2501        selected.sort_unstable();
2502        selected.dedup();
2503        selected
2504    };
2505
2506    split_points.push(entries.len());
2507
2508    let mut out = Vec::with_capacity(split_points.len());
2509    let mut start = 0usize;
2510    for end in split_points {
2511        if end <= start {
2512            continue;
2513        }
2514        out.push(ColZone::with_bounds(
2515            entries[start].1.clone(),
2516            entries[end - 1].1.clone(),
2517        ));
2518        start = end;
2519    }
2520
2521    if out.is_empty() {
2522        out.push(ColZone::with_bounds(
2523            entries[0].1.clone(),
2524            entries[entries.len() - 1].1.clone(),
2525        ));
2526    }
2527
2528    out
2529}
2530
2531fn select_gap_split_points(entries: &[(CanonicalKey, Value)], max_splits: usize) -> Vec<usize> {
2532    let mut gaps = Vec::new();
2533    for idx in 1..entries.len() {
2534        if let Some(score) = canonical_gap_score(&entries[idx - 1].0, &entries[idx].0) {
2535            if score > 0.0 {
2536                gaps.push((score, idx));
2537            }
2538        }
2539    }
2540    gaps.sort_by(|left, right| {
2541        right
2542            .0
2543            .partial_cmp(&left.0)
2544            .unwrap_or(std::cmp::Ordering::Equal)
2545            .then_with(|| left.1.cmp(&right.1))
2546    });
2547    gaps.into_iter()
2548        .take(max_splits)
2549        .map(|(_, idx)| idx)
2550        .collect()
2551}
2552
2553fn canonical_gap_score(left: &CanonicalKey, right: &CanonicalKey) -> Option<f64> {
2554    if left.family() != right.family() {
2555        return None;
2556    }
2557    match (left, right) {
2558        (CanonicalKey::Signed(_, l), CanonicalKey::Signed(_, r)) => {
2559            Some(r.saturating_sub(*l) as f64)
2560        }
2561        (CanonicalKey::Unsigned(_, l), CanonicalKey::Unsigned(_, r)) => {
2562            Some(r.saturating_sub(*l) as f64)
2563        }
2564        (CanonicalKey::Float(l), CanonicalKey::Float(r)) => {
2565            Some((f64::from_bits(*r) - f64::from_bits(*l)).abs())
2566        }
2567        _ => None,
2568    }
2569}
2570
2571// Implement the std::sync::Arc<parking_lot::RwLock<GrowingSegment>> as UnifiedSegment
2572// This is needed because we store growing segments in the sealed list after sealing
2573impl UnifiedSegment for std::sync::Arc<parking_lot::RwLock<GrowingSegment>> {
2574    fn id(&self) -> SegmentId {
2575        self.read().id()
2576    }
2577
2578    fn state(&self) -> SegmentState {
2579        self.read().state()
2580    }
2581
2582    fn collection(&self) -> &str {
2583        // This is a limitation - we'd need to store collection in the Arc wrapper
2584        "unknown"
2585    }
2586
2587    fn stats(&self) -> SegmentStats {
2588        self.read().stats()
2589    }
2590
2591    fn entity_count(&self) -> usize {
2592        self.read().entity_count()
2593    }
2594
2595    fn contains(&self, id: EntityId) -> bool {
2596        self.read().contains(id)
2597    }
2598
2599    fn get(&self, _id: EntityId) -> Option<&UnifiedEntity> {
2600        // This is tricky with RwLock - we can't return a reference
2601        // In production, we'd use a different approach
2602        None
2603    }
2604
2605    fn get_mut(&mut self, _id: EntityId) -> Option<&mut UnifiedEntity> {
2606        None
2607    }
2608
2609    fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
2610        self.write().insert(entity)
2611    }
2612
2613    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
2614        self.write().update(entity)
2615    }
2616
2617    fn update_hot(
2618        &mut self,
2619        entity: UnifiedEntity,
2620        modified_columns: &[String],
2621    ) -> Result<(), SegmentError> {
2622        self.write().update_hot(entity, modified_columns)
2623    }
2624
2625    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
2626        self.write().delete(id)
2627    }
2628
2629    fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
2630        self.read().get_metadata(id)
2631    }
2632
2633    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
2634        self.write().set_metadata(id, metadata)
2635    }
2636
2637    fn seal(&mut self) -> Result<(), SegmentError> {
2638        self.write().seal()
2639    }
2640
2641    fn should_seal(&self, config: &SegmentConfig) -> bool {
2642        self.read().should_seal(config)
2643    }
2644
2645    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2646        // Cannot return iterator with RwLock
2647        Box::new(std::iter::empty())
2648    }
2649
2650    fn iter_kind(&self, _kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
2651        Box::new(std::iter::empty())
2652    }
2653
2654    fn filter_metadata(&self, filters: &[(String, MetadataFilter)]) -> Vec<EntityId> {
2655        self.read().filter_metadata(filters)
2656    }
2657}
2658
2659#[cfg(test)]
2660mod tests {
2661    use super::*;
2662    use crate::storage::entity::RowData;
2663    use crate::storage::metadata::MetadataValue;
2664    use reddb_types::Value;
2665
2666    fn keyed_row(id: u64, key: &str, xmin: u64, xmax: u64) -> UnifiedEntity {
2667        let mut entity = UnifiedEntity::table_row(EntityId::new(id), "keys", id, vec![]);
2668        entity.data = EntityData::Row(RowData::with_names(
2669            vec![Value::text(key), Value::Integer(id as i64)],
2670            vec!["key".into(), "value".into()],
2671        ));
2672        entity.set_xmin(xmin);
2673        entity.set_xmax(xmax);
2674        entity
2675    }
2676
2677    fn keyed_probe(segment: &GrowingSegment, key: &str) -> Option<EntityId> {
2678        segment
2679            .find_key_entity(key, true, &mut |_| true)
2680            .map(|entity| entity.id)
2681    }
2682
2683    #[test]
2684    fn bulk_checkpoint_journal_preserves_prior_changes_and_ignores_rejected_batches() {
2685        let mut segment = GrowingSegment::new(1, "keys");
2686        segment.insert(keyed_row(1, "one", 1, 0)).expect("row");
2687        segment
2688            .bulk_insert(vec![keyed_row(2, "two", 1, 0), keyed_row(3, "three", 1, 0)])
2689            .expect("batch");
2690        assert_eq!(
2691            segment.checkpoint_mutations_since(0),
2692            Some(vec![EntityId::new(1), EntityId::new(2), EntityId::new(3)])
2693        );
2694        let revision = segment.checkpoint_revision();
2695        segment.bulk_insert(Vec::new()).expect("empty batch");
2696        assert_eq!(segment.checkpoint_revision(), revision);
2697        segment.seal().expect("seal segment");
2698        assert!(matches!(
2699            segment.bulk_insert(vec![keyed_row(4, "four", 1, 0)]),
2700            Err(SegmentError::NotWritable)
2701        ));
2702        assert_eq!(segment.checkpoint_revision(), revision);
2703        assert_eq!(segment.checkpoint_mutations_since(revision), Some(vec![]));
2704    }
2705
2706    #[test]
2707    fn checkpoint_journal_retains_bounded_row_versions_through_repeated_updates_and_gc() {
2708        const ROWS: u64 = 1700;
2709        let mut segment = GrowingSegment::new(1, "keys");
2710        for id in 1..=ROWS {
2711            segment
2712                .insert(keyed_row(id, "export", 1, 0))
2713                .expect("seed export");
2714        }
2715        let baseline = segment.checkpoint_revision();
2716        // An export is inserted, updated after delivery, then acknowledged.
2717        // GC removes its superseded physical versions. These are 5,100
2718        // distinct IDs, even though their mutation count exceeds the bound.
2719        for id in 1..=ROWS {
2720            segment
2721                .update(keyed_row(id, "export", 1, 2))
2722                .expect("supersede");
2723            let delivered = ROWS + id;
2724            segment
2725                .insert(keyed_row(delivered, "export", 2, 0))
2726                .expect("delivered");
2727            segment
2728                .update(keyed_row(delivered, "export", 2, 3))
2729                .expect("acknowledge");
2730            segment
2731                .insert(keyed_row(2 * ROWS + id, "export", 3, 0))
2732                .expect("final version");
2733            assert_eq!(
2734                segment
2735                    .delete_batch(&[EntityId::new(id), EntityId::new(delivered)])
2736                    .expect("GC")
2737                    .len(),
2738                2
2739            );
2740        }
2741        let changed = segment
2742            .checkpoint_mutations_since(baseline)
2743            .expect("all changed IDs still fit the bounded checkpoint journal");
2744        let actual: std::collections::BTreeSet<_> = changed.into_iter().collect();
2745        let expected: std::collections::BTreeSet<_> = (1..=3 * ROWS).map(EntityId::new).collect();
2746        assert_eq!(actual, expected, "deleted versions must remain tracked too");
2747    }
2748
2749    #[test]
2750    fn checkpoint_journal_preserves_cold_changes_during_hot_row_churn() {
2751        let mut segment = GrowingSegment::new(1, "keys");
2752        segment
2753            .insert(keyed_row(1, "cold", 1, 0))
2754            .expect("cold row");
2755        segment.insert(keyed_row(2, "hot", 1, 0)).expect("hot row");
2756        for xid in 2..=(5 * CHECKPOINT_MUTATIONS_MAX) as u64 {
2757            segment
2758                .update(keyed_row(2, "hot", xid, 0))
2759                .expect("hot edit");
2760        }
2761        let changed: std::collections::BTreeSet<_> = segment
2762            .checkpoint_mutations_since(0)
2763            .expect("two physical IDs never exhaust the distinct-ID bound")
2764            .into_iter()
2765            .collect();
2766        assert_eq!(
2767            changed,
2768            [EntityId::new(1), EntityId::new(2)].into_iter().collect()
2769        );
2770        assert!(
2771            segment.checkpoint_tracking_bytes() <= 1024 * 1024,
2772            "retained proof memory must stay bounded under unlimited repeated edits"
2773        );
2774    }
2775
2776    #[test]
2777    fn checkpoint_journal_complete_suffix_matches_uncompressed_mutation_history() {
2778        let mut segment = GrowingSegment::new(1, "keys");
2779        let mut history = Vec::new();
2780        let mut seed = 0x7b3d_5961_a902_4c85_u64;
2781        for n in 0..3 * CHECKPOINT_MUTATIONS_MAX {
2782            seed ^= seed << 13;
2783            seed ^= seed >> 7;
2784            seed ^= seed << 17;
2785            let raw = if n < CHECKPOINT_MUTATIONS_MAX + 17 {
2786                n as u64 + 1
2787            } else {
2788                seed % (CHECKPOINT_MUTATIONS_MAX as u64 + 17) + 1
2789            };
2790            let id = EntityId::new(raw);
2791            segment.mark_checkpoint_entity_dirty(id);
2792            history.push(id);
2793        }
2794        let now = segment.checkpoint_revision();
2795        assert!(
2796            segment.checkpoint_mutations_since(0).is_none(),
2797            "an evicted ID must still invalidate an older baseline"
2798        );
2799        let mut complete = 0;
2800        for baseline in [0, 1, now / 3, now / 2, now - 2048, now - 128, now] {
2801            if let Some(ids) = segment.checkpoint_mutations_since(baseline) {
2802                let actual: std::collections::BTreeSet<_> = ids.into_iter().collect();
2803                let expected: std::collections::BTreeSet<_> =
2804                    history[baseline as usize..].iter().copied().collect();
2805                assert_eq!(actual, expected, "baseline {baseline}");
2806                complete += 1;
2807            }
2808        }
2809        assert!(
2810            complete >= 3,
2811            "recent baselines must retain a complete proof"
2812        );
2813        assert!(segment.checkpoint_mutations_since(now + 1).is_none());
2814        assert!(segment.checkpoint_tracking_bytes() <= 1024 * 1024);
2815    }
2816
2817    #[test]
2818    fn bulk_checkpoint_journal_exhaustion_keeps_a_bounded_complete_suffix() {
2819        let mut segment = GrowingSegment::new(1, "keys");
2820        segment
2821            .bulk_insert(
2822                (1..=CHECKPOINT_MUTATIONS_MAX as u64 + 1)
2823                    .map(|id| keyed_row(id, "key", 1, 0))
2824                    .collect(),
2825            )
2826            .expect("large batch");
2827        assert!(segment.checkpoint_mutations_since(0).is_none());
2828        assert_eq!(
2829            segment
2830                .checkpoint_mutations_since(1)
2831                .expect("complete suffix")
2832                .len(),
2833            CHECKPOINT_MUTATIONS_MAX
2834        );
2835        let revision = segment.checkpoint_revision();
2836        let id = EntityId::new(CHECKPOINT_MUTATIONS_MAX as u64 + 2);
2837        segment
2838            .bulk_insert(vec![keyed_row(id.raw(), "next", 1, 0)])
2839            .expect("later batch");
2840        assert_eq!(segment.checkpoint_mutations_since(revision), Some(vec![id]));
2841        assert_eq!(segment.checkpoint_mutations.len(), CHECKPOINT_MUTATIONS_MAX);
2842    }
2843
2844    #[test]
2845    fn key_lookup_visits_only_requested_key_versions() {
2846        let mut segment = GrowingSegment::new(1, "keys");
2847        segment
2848            .bulk_insert(
2849                (1..=4096)
2850                    .map(|id| keyed_row(id, &format!("other-{id}"), 1, 0))
2851                    .collect(),
2852            )
2853            .expect("unrelated rows");
2854        segment
2855            .insert(keyed_row(5000, "wanted", 2, 8))
2856            .expect("old version");
2857        segment
2858            .insert(keyed_row(5001, "wanted", 8, 0))
2859            .expect("new version");
2860        let mut visits = 0;
2861        let result = segment.find_key_entity("wanted", true, &mut |entity| {
2862            visits += 1;
2863            entity.xmin < 8
2864        });
2865        assert_eq!(result.map(|entity| entity.id), Some(EntityId::new(5000)));
2866        assert_eq!(
2867            visits, 2,
2868            "unrelated rows must never reach visibility evaluation"
2869        );
2870        assert_eq!(keyed_probe(&segment, "wanted"), Some(EntityId::new(5001)));
2871        let mut misses = 0;
2872        assert!(segment
2873            .find_key_entity("absent", true, &mut |_| {
2874                misses += 1;
2875                true
2876            })
2877            .is_none());
2878        assert_eq!(misses, 0);
2879
2880        // A bucket collision narrows candidates, but cannot change key equality.
2881        let mut collision = SegmentKeyLookup::default();
2882        collision.insert(&keyed_row(1, "wanted", 1, 0));
2883        assert!(segment
2884            .find_key_candidate(&collision, "wanted", true, &mut |_| true)
2885            .is_none());
2886    }
2887
2888    #[test]
2889    fn raw_mutation_does_not_disable_unrelated_key_candidates() {
2890        let mut segment = GrowingSegment::new(1, "keys");
2891        segment
2892            .bulk_insert(
2893                (1..=4096)
2894                    .map(|id| keyed_row(id, &format!("key-{id}"), 1, 0))
2895                    .collect(),
2896            )
2897            .expect("rows");
2898        for _ in 0..3 {
2899            *segment.get_mut(EntityId::new(1)).expect("mutable") = keyed_row(1, "renamed", 2, 0);
2900        }
2901        let index = segment.key_lookup.as_ref().expect("index retained");
2902        assert_eq!(index.candidates("key-4096"), &[EntityId::new(4096)]);
2903        assert_eq!(index.mutated_candidates().count(), 1);
2904        assert!(index.candidates("key-1").is_empty());
2905        assert_eq!(keyed_probe(&segment, "renamed"), Some(EntityId::new(1)));
2906        assert_eq!(keyed_probe(&segment, "key-4096"), Some(EntityId::new(4096)));
2907        segment.delete(EntityId::new(1)).expect("delete raw row");
2908        assert_eq!(
2909            segment
2910                .key_lookup
2911                .as_ref()
2912                .unwrap()
2913                .mutated_candidates()
2914                .count(),
2915            0
2916        );
2917        assert_eq!(keyed_probe(&segment, "renamed"), None);
2918    }
2919
2920    #[test]
2921    fn key_lookup_tracks_mutations_sealing_and_adoption() {
2922        let mut segment = GrowingSegment::new(1, "keys");
2923        segment
2924            .bulk_insert(vec![keyed_row(1, "flat", 1, 0)])
2925            .expect("flat row");
2926        segment
2927            .insert(keyed_row(3, "hash", 1, 0))
2928            .expect("hash fallback");
2929        segment
2930            .bulk_insert(vec![keyed_row(5, "gap", 1, 0)])
2931            .expect("bulk gap");
2932        assert!(segment.key_lookup.as_ref().expect("index").memory_bytes() > 0);
2933        assert_eq!(keyed_probe(&segment, "gap"), Some(EntityId::new(5)));
2934        segment
2935            .update(keyed_row(3, "renamed", 2, 0))
2936            .expect("rename key");
2937        assert!(
2938            segment.key_lookup.is_some(),
2939            "key replacements maintain the index"
2940        );
2941        assert_eq!(keyed_probe(&segment, "hash"), None);
2942        assert_eq!(keyed_probe(&segment, "renamed"), Some(EntityId::new(3)));
2943
2944        *segment.get_mut(EntityId::new(3)).expect("mutable row") = keyed_row(3, "raw", 3, 0);
2945        assert!(
2946            segment.key_lookup.is_some(),
2947            "raw mutation preserves unaffected key candidates"
2948        );
2949        assert_eq!(keyed_probe(&segment, "renamed"), None);
2950        assert_eq!(keyed_probe(&segment, "raw"), Some(EntityId::new(3)));
2951        segment.seal().expect("seal");
2952        segment
2953            .force_update_with_metadata(&keyed_row(5, "sealed", 4, 0), &["key".into()], None)
2954            .expect("sealed rename");
2955        assert_eq!(keyed_probe(&segment, "gap"), None);
2956        assert_eq!(keyed_probe(&segment, "sealed"), Some(EntityId::new(5)));
2957        assert!(segment.force_delete(EntityId::new(1)));
2958        assert!(segment.force_delete(EntityId::new(3)));
2959        assert_eq!(keyed_probe(&segment, "flat"), None);
2960        assert_eq!(keyed_probe(&segment, "raw"), None);
2961
2962        let mut adopted = GrowingSegment::new(2, "keys");
2963        segment.for_each_fast(|entity| {
2964            adopted.adopt_entity(entity.clone(), None);
2965            true
2966        });
2967        assert_eq!(keyed_probe(&adopted, "sealed"), Some(EntityId::new(5)));
2968        assert!(adopted.evict_entity(EntityId::new(5)));
2969        assert_eq!(keyed_probe(&adopted, "sealed"), None);
2970    }
2971
2972    #[test]
2973    #[ignore = "diagnostic microbenchmark; no timing assertions"]
2974    fn key_lookup_scaling_diagnostic() {
2975        use std::{hint::black_box, time::Instant};
2976        for count in [100, 10_000, 100_000] {
2977            let mut segment = GrowingSegment::new(1, "keys");
2978            segment
2979                .bulk_insert(
2980                    (1..=count)
2981                        .map(|id| keyed_row(id, &format!("key-{id}"), 1, 0))
2982                        .collect(),
2983                )
2984                .expect("rows");
2985            let key = format!("key-{count}");
2986            let started = Instant::now();
2987            for _ in 0..1000 {
2988                assert_eq!(
2989                    keyed_probe(black_box(&segment), black_box(&key)),
2990                    Some(EntityId::new(count))
2991                );
2992            }
2993            let indexed = started.elapsed();
2994            let started = Instant::now();
2995            for _ in 0..1000 {
2996                let mut found = None;
2997                segment.for_each_fast(|entity| {
2998                    if SegmentKeyLookup::key(entity) == Some(black_box(key.as_str())) {
2999                        found = Some(entity.id);
3000                        return false;
3001                    }
3002                    true
3003                });
3004                assert_eq!(black_box(found), Some(EntityId::new(count)));
3005            }
3006            eprintln!(
3007                "rows={count} reads=1000 indexed={indexed:?} scan={:?}",
3008                started.elapsed()
3009            );
3010        }
3011    }
3012
3013    fn pruning_node(id: u64) -> UnifiedEntity {
3014        UnifiedEntity::graph_node(EntityId::new(id), "mixed", "person", Default::default())
3015    }
3016
3017    fn pruning_edge(id: u64) -> UnifiedEntity {
3018        UnifiedEntity::graph_edge(
3019            EntityId::new(id),
3020            "mixed",
3021            "a",
3022            "b",
3023            1.0,
3024            Default::default(),
3025        )
3026    }
3027
3028    #[test]
3029    fn graph_presence_covers_bulk_gaps_inserts_and_adoption() {
3030        let mut segment = GrowingSegment::new(1, "mixed");
3031        assert!(!segment.may_contain_graph_kind(GraphEntityKind::Node));
3032        assert!(!segment.may_contain_graph_kind(GraphEntityKind::Edge));
3033        segment
3034            .bulk_insert(vec![
3035                UnifiedEntity::vector(EntityId::new(1), "mixed", vec![]),
3036                pruning_node(2),
3037                pruning_edge(4),
3038            ])
3039            .expect("mixed bulk with gap");
3040        assert!(segment.may_contain_graph_kind(GraphEntityKind::Node));
3041        assert!(segment.may_contain_graph_kind(GraphEntityKind::Edge));
3042        segment.seal().expect("seal");
3043        assert!(segment.force_delete(EntityId::new(2)));
3044        assert!(segment.force_delete(EntityId::new(4)));
3045        // Deletion must not erase presence; reconstruction may safely tighten it.
3046        assert!(segment.may_contain_graph_kind(GraphEntityKind::Node));
3047        let mut rebuilt = GrowingSegment::new(2, "mixed");
3048        for item in segment.iter() {
3049            rebuilt.adopt_entity(item.clone(), None);
3050        }
3051        assert!(!rebuilt.may_contain_graph_kind(GraphEntityKind::Node));
3052        assert!(!rebuilt.may_contain_graph_kind(GraphEntityKind::Edge));
3053        rebuilt.adopt_entity(pruning_node(5), None);
3054        assert!(rebuilt.may_contain_graph_kind(GraphEntityKind::Node));
3055        rebuilt.insert(pruning_edge(6)).expect("single edge");
3056        assert!(rebuilt.may_contain_graph_kind(GraphEntityKind::Edge));
3057    }
3058
3059    #[test]
3060    fn graph_presence_covers_replacement_and_unrestricted_mutation() {
3061        for flat in [false, true] {
3062            for operation in 0..4 {
3063                let mut segment = GrowingSegment::new(1, "mixed");
3064                let vector = UnifiedEntity::vector(EntityId::new(1), "mixed", vec![]);
3065                if flat {
3066                    segment.bulk_insert(vec![vector]).expect("flat vector");
3067                } else {
3068                    segment.insert(vector).expect("hashmap vector");
3069                }
3070                match operation {
3071                    0 => segment.update(pruning_node(1)).expect("replace with node"),
3072                    1 => segment
3073                        .update_hot(pruning_node(1), &[])
3074                        .expect("hot replacement"),
3075                    2 => {
3076                        segment.seal().expect("seal vector");
3077                        segment
3078                            .force_update_with_metadata(&pruning_node(1), &[], None)
3079                            .expect("sealed replacement");
3080                    }
3081                    _ => {
3082                        *segment.get_mut(EntityId::new(1)).expect("mutable vector") =
3083                            pruning_node(1)
3084                    }
3085                }
3086                assert!(segment.may_contain_graph_kind(GraphEntityKind::Node));
3087                assert!(matches!(
3088                    segment.get(EntityId::new(1)).expect("node").kind,
3089                    EntityKind::GraphNode(_)
3090                ));
3091                if operation == 3 {
3092                    assert!(segment.may_contain_graph_kind(GraphEntityKind::Edge));
3093                }
3094                segment
3095                    .force_update_with_metadata(&pruning_edge(1), &[], None)
3096                    .expect("replace with edge");
3097                assert!(segment.may_contain_graph_kind(GraphEntityKind::Edge));
3098                assert!(segment.may_contain_graph_kind(GraphEntityKind::Node));
3099            }
3100        }
3101    }
3102
3103    fn primary_key_row(id: u64, key: Value) -> UnifiedEntity {
3104        UnifiedEntity::new(
3105            EntityId::new(id),
3106            EntityKind::TableRow {
3107                table: "keys".into(),
3108                row_id: id,
3109            },
3110            EntityData::Row(RowData::with_names(
3111                vec![Value::text("payload"), key],
3112                vec!["payload".into(), "primary".into()],
3113            )),
3114        )
3115    }
3116
3117    fn primary_key_probe(
3118        segment: &GrowingSegment,
3119        key: Value,
3120        exclude: Option<EntityId>,
3121    ) -> Option<EntityId> {
3122        segment.find_unique_key_conflict(
3123            &[vec!["primary".into()]],
3124            0,
3125            &[format!("{key:?}")],
3126            exclude,
3127            &|_| true,
3128        )
3129    }
3130
3131    #[test]
3132    fn unique_key_lookup_follows_segment_mutations_and_sealing() {
3133        let mut segment = GrowingSegment::new(1, "keys");
3134        segment
3135            .bulk_insert(vec![primary_key_row(1, Value::Integer(10))])
3136            .expect("bulk row");
3137        let payload_bytes = segment.memory_bytes();
3138        assert_eq!(
3139            primary_key_probe(&segment, Value::Integer(10), None),
3140            Some(EntityId::new(1))
3141        );
3142        assert!(segment.memory_bytes() > payload_bytes);
3143        segment
3144            .insert(primary_key_row(3, Value::Integer(30)))
3145            .expect("hashmap fallback row");
3146        segment
3147            .bulk_insert(vec![primary_key_row(4, Value::Integer(40))])
3148            .expect("bulk gap row");
3149        assert_eq!(
3150            primary_key_probe(&segment, Value::Integer(40), None),
3151            Some(EntityId::new(4))
3152        );
3153        assert_eq!(
3154            primary_key_probe(&segment, Value::Integer(30), None),
3155            Some(EntityId::new(3))
3156        );
3157        segment
3158            .update_hot(primary_key_row(1, Value::Integer(11)), &["primary".into()])
3159            .expect("replace flat key");
3160        assert_eq!(segment.unique_key_lookup_bytes.load(Ordering::Relaxed), 0);
3161        assert_eq!(primary_key_probe(&segment, Value::Integer(10), None), None);
3162        assert_eq!(
3163            primary_key_probe(&segment, Value::Integer(11), None),
3164            Some(EntityId::new(1))
3165        );
3166        segment
3167            .delete_batch(&[EntityId::new(1), EntityId::new(3)])
3168            .expect("delete flat and map");
3169        assert_eq!(primary_key_probe(&segment, Value::Integer(11), None), None);
3170        assert_eq!(primary_key_probe(&segment, Value::Integer(30), None), None);
3171        segment.seal().expect("seal");
3172        assert_eq!(
3173            primary_key_probe(&segment, Value::Integer(40), None),
3174            Some(EntityId::new(4))
3175        );
3176        segment
3177            .force_update_with_metadata(
3178                &primary_key_row(4, Value::Integer(41)),
3179                &["primary".into()],
3180                None,
3181            )
3182            .expect("sealed update");
3183        assert_eq!(primary_key_probe(&segment, Value::Integer(40), None), None);
3184        assert_eq!(
3185            primary_key_probe(&segment, Value::Integer(41), None),
3186            Some(EntityId::new(4))
3187        );
3188        assert!(segment.force_delete(EntityId::new(4)));
3189        assert_eq!(primary_key_probe(&segment, Value::Integer(41), None), None);
3190    }
3191
3192    #[test]
3193    fn unique_lookup_retains_all_current_definitions_and_rechecks_candidates() {
3194        let mut segment = GrowingSegment::new(1, "keys");
3195        segment
3196            .bulk_insert(vec![
3197                primary_key_row(1, Value::Integer(10)),
3198                primary_key_row(2, Value::Integer(20)),
3199            ])
3200            .expect("initial rows");
3201        let definitions = vec![
3202            vec!["primary".into()],
3203            vec!["payload".into()],
3204            vec!["primary".into(), "payload".into()],
3205        ];
3206        let payload = format!("{:?}", Value::text("payload"));
3207        assert_eq!(
3208            segment.find_unique_key_conflict(
3209                &definitions,
3210                1,
3211                std::slice::from_ref(&payload),
3212                None,
3213                &|entity| entity.id != EntityId::new(1)
3214            ),
3215            Some(EntityId::new(2))
3216        );
3217        assert_eq!(segment.unique_key_lookup.read().len(), 3);
3218        segment
3219            .insert(primary_key_row(3, Value::Integer(30)))
3220            .expect("append");
3221        assert_eq!(
3222            segment.find_unique_key_conflict(
3223                &definitions,
3224                2,
3225                &[format!("{:?}", Value::Integer(30)), payload.clone()],
3226                None,
3227                &|_| true
3228            ),
3229            Some(EntityId::new(3))
3230        );
3231        let bytes = segment.unique_key_lookup_bytes.load(Ordering::Relaxed);
3232        assert_eq!(
3233            segment.find_unique_key_conflict(
3234                &definitions,
3235                0,
3236                &[format!("{:?}", Value::Integer(20))],
3237                None,
3238                &|_| true
3239            ),
3240            Some(EntityId::new(2))
3241        );
3242        assert_eq!(segment.unique_key_lookup.read().len(), 3);
3243        assert_eq!(
3244            segment.unique_key_lookup_bytes.load(Ordering::Relaxed),
3245            bytes
3246        );
3247        let reduced = vec![vec!["payload".into()]];
3248        assert_eq!(
3249            segment.find_unique_key_conflict(&reduced, 0, &[payload], None, &|_| true),
3250            Some(EntityId::new(1))
3251        );
3252        assert_eq!(segment.unique_key_lookup.read().len(), 1);
3253        assert!(segment.unique_key_lookup_bytes.load(Ordering::Relaxed) < bytes);
3254    }
3255
3256    #[test]
3257    fn unique_key_lookup_preserves_signature_semantics_and_exclusion() {
3258        let mut segment = GrowingSegment::new(1, "keys");
3259        let values = [
3260            Value::Float(f64::NAN),
3261            Value::Float(-0.0),
3262            Value::Float(0.0),
3263            Value::Integer(1),
3264            Value::UnsignedInteger(1),
3265            Value::text("1"),
3266        ];
3267        for (offset, value) in values.iter().enumerate() {
3268            let id = offset as u64 + 1;
3269            segment
3270                .insert(primary_key_row(id, value.clone()))
3271                .expect("distinct physical row");
3272            assert_eq!(
3273                primary_key_probe(&segment, value.clone(), None),
3274                Some(EntityId::new(id))
3275            );
3276            assert_eq!(
3277                primary_key_probe(&segment, value.clone(), Some(EntityId::new(id))),
3278                None
3279            );
3280        }
3281        segment
3282            .insert(primary_key_row(20, Value::Integer(1)))
3283            .expect("duplicate physical key");
3284        assert_eq!(
3285            primary_key_probe(&segment, Value::Integer(1), Some(EntityId::new(4))),
3286            Some(EntityId::new(20))
3287        );
3288        // Candidate fingerprints are not equality proof: simulate a stale or
3289        // colliding entry by changing its row without the normal invalidation.
3290        segment
3291            .entities
3292            .insert(EntityId::new(4), primary_key_row(4, Value::Integer(99)));
3293        assert_eq!(
3294            primary_key_probe(&segment, Value::Integer(1), Some(EntityId::new(20))),
3295            None
3296        );
3297        // Public mutable access must invalidate before handing out the row.
3298        let entity = segment.get_mut(EntityId::new(4)).expect("mutable row");
3299        *entity = primary_key_row(4, Value::Integer(100));
3300        assert_eq!(
3301            primary_key_probe(&segment, Value::Integer(100), None),
3302            Some(EntityId::new(4))
3303        );
3304    }
3305
3306    #[test]
3307    fn test_growing_segment_basic() {
3308        let mut segment = GrowingSegment::new(1, "test");
3309
3310        let entity = UnifiedEntity::table_row(
3311            EntityId::new(1),
3312            "users",
3313            1,
3314            vec![Value::text("Alice".to_string())],
3315        );
3316
3317        let id = segment.insert(entity).unwrap();
3318        assert_eq!(id, EntityId::new(1));
3319        assert!(segment.contains(id));
3320
3321        let stats = segment.stats();
3322        assert_eq!(stats.entity_count, 1);
3323        assert_eq!(stats.row_count, 1);
3324    }
3325
3326    #[test]
3327    fn test_segment_metadata() {
3328        let mut segment = GrowingSegment::new(1, "test");
3329
3330        let entity = UnifiedEntity::table_row(
3331            EntityId::new(1),
3332            "users",
3333            1,
3334            vec![Value::text("Alice".to_string())],
3335        );
3336        segment.insert(entity).unwrap();
3337
3338        let mut meta = Metadata::new();
3339        meta.set("role", MetadataValue::String("admin".to_string()));
3340        meta.set("level", MetadataValue::Int(5));
3341
3342        segment.set_metadata(EntityId::new(1), meta).unwrap();
3343
3344        let retrieved = segment.get_metadata(EntityId::new(1)).unwrap();
3345        assert_eq!(
3346            retrieved.get("role"),
3347            Some(&MetadataValue::String("admin".to_string()))
3348        );
3349    }
3350
3351    #[test]
3352    fn test_segment_seal() {
3353        let mut segment = GrowingSegment::new(1, "test");
3354
3355        let entity = UnifiedEntity::vector(EntityId::new(1), "embeddings", vec![0.1, 0.2, 0.3]);
3356        segment.insert(entity).unwrap();
3357
3358        // Can write before sealing
3359        assert!(segment.state().is_writable());
3360
3361        // Seal the segment
3362        segment.seal().unwrap();
3363        assert_eq!(segment.state(), SegmentState::Sealed);
3364
3365        // Cannot write after sealing
3366        let entity2 = UnifiedEntity::vector(EntityId::new(2), "embeddings", vec![0.4, 0.5, 0.6]);
3367        assert!(segment.insert(entity2).is_err());
3368    }
3369
3370    #[test]
3371    fn test_should_seal() {
3372        let mut segment = GrowingSegment::new(1, "test");
3373
3374        let config = SegmentConfig {
3375            max_entities: 2,
3376            ..Default::default()
3377        };
3378
3379        assert!(!segment.should_seal(&config));
3380
3381        segment
3382            .insert(UnifiedEntity::vector(EntityId::new(1), "v", vec![0.1]))
3383            .unwrap();
3384        assert!(!segment.should_seal(&config));
3385
3386        segment
3387            .insert(UnifiedEntity::vector(EntityId::new(2), "v", vec![0.2]))
3388            .unwrap();
3389        assert!(segment.should_seal(&config));
3390    }
3391
3392    #[test]
3393    fn test_cross_references() {
3394        let mut segment = GrowingSegment::new(1, "test");
3395
3396        let mut entity1 = UnifiedEntity::table_row(
3397            EntityId::new(1),
3398            "hosts",
3399            1,
3400            vec![Value::text("192.168.1.1".to_string())],
3401        );
3402        entity1.add_cross_ref(CrossRef::new(
3403            EntityId::new(1),
3404            EntityId::new(2),
3405            "nodes",
3406            RefType::RowToNode,
3407        ));
3408        segment.insert(entity1).unwrap();
3409
3410        let refs_from = segment.get_references_from(EntityId::new(1));
3411        assert_eq!(refs_from.len(), 1);
3412        assert_eq!(refs_from[0], (EntityId::new(2), RefType::RowToNode));
3413
3414        let refs_to = segment.get_references_to(EntityId::new(2));
3415        assert_eq!(refs_to.len(), 1);
3416        assert_eq!(refs_to[0], (EntityId::new(1), RefType::RowToNode));
3417    }
3418
3419    #[test]
3420    fn test_zone_predicate_uses_canonical_fallback_for_email_values() {
3421        let mut zone = ColZone::new(Value::Email("bravo@example.com".to_string()));
3422        zone.update(&Value::Email("delta@example.com".to_string()));
3423
3424        let probe = Value::Email("alpha@example.com".to_string());
3425        assert!(ZoneColPred::Eq(&probe).can_skip(&zone));
3426
3427        let in_range = Value::Email("charlie@example.com".to_string());
3428        assert!(!ZoneColPred::Eq(&in_range).can_skip(&zone));
3429    }
3430
3431    #[test]
3432    fn test_sealed_multi_zone_prunes_numeric_gap_outlier() {
3433        let mut segment = GrowingSegment::new(1, "test");
3434
3435        for (row_id, age) in [(1_u64, 1_i64), (2, 2), (3, 3), (4, 1000)] {
3436            let entity = UnifiedEntity::new(
3437                EntityId::new(row_id),
3438                EntityKind::TableRow {
3439                    table: "users".into(),
3440                    row_id,
3441                },
3442                EntityData::Row(RowData::with_names(
3443                    vec![Value::Integer(age)],
3444                    vec!["age".to_string()],
3445                )),
3446            );
3447            segment.insert(entity).unwrap();
3448        }
3449
3450        segment.seal().unwrap();
3451
3452        let miss = Value::Integer(500);
3453        assert!(segment.can_skip_zone_preds(&[("age", ZoneColPred::Eq(&miss))]));
3454
3455        let hit = Value::Integer(1000);
3456        assert!(!segment.can_skip_zone_preds(&[("age", ZoneColPred::Eq(&hit))]));
3457    }
3458}