Skip to main content

reddb_server/storage/unified/
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//!    bloom filter and zone maps over the immutable data
13//!
14//! # Read Path
15//!
16//! 1. Growing segment entity storage (most recent writes)
17//! 2. Sealed segments (older, immutable, bloom/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, bloom filter + 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};
33use std::time::{SystemTime, UNIX_EPOCH};
34
35use super::entity::{CrossRef, EntityData, EntityId, EntityKind, RefType, UnifiedEntity};
36use super::metadata::{Metadata, MetadataStorage};
37use crate::storage::primitives::bloom::BloomFilter;
38use crate::storage::query::value_compare::partial_compare_values;
39use crate::storage::schema::{value_to_canonical_key, CanonicalKey, Value};
40
41/// Unique identifier for a segment
42pub type SegmentId = u64;
43
44/// Segment state in its lifecycle
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum SegmentState {
47    /// Accepts writes, partial/no index
48    Growing,
49    /// Transitioning, building indices
50    Sealing,
51    /// Immutable, fully indexed
52    Sealed,
53    /// Persisted to disk
54    Flushed,
55    /// Compressed, cold storage
56    Archived,
57}
58
59impl SegmentState {
60    /// Check if segment accepts writes
61    pub fn is_writable(&self) -> bool {
62        matches!(self, Self::Growing)
63    }
64
65    /// Check if segment is queryable
66    pub fn is_queryable(&self) -> bool {
67        !matches!(self, Self::Sealing)
68    }
69
70    /// Check if segment is immutable
71    pub fn is_immutable(&self) -> bool {
72        matches!(self, Self::Sealed | Self::Flushed | Self::Archived)
73    }
74}
75
76/// Configuration for segments
77#[derive(Debug, Clone)]
78pub struct SegmentConfig {
79    /// Maximum entities before auto-sealing
80    pub max_entities: usize,
81    /// Maximum memory bytes before auto-sealing
82    pub max_bytes: usize,
83    /// Maximum age in seconds before auto-sealing
84    pub max_age_secs: u64,
85    /// Enable vector indexing when sealed
86    pub build_vector_index: bool,
87    /// Enable graph indexing when sealed
88    pub build_graph_index: bool,
89    /// Compression level for archived segments (0-9)
90    pub compression_level: u8,
91}
92
93impl Default for SegmentConfig {
94    fn default() -> Self {
95        Self {
96            max_entities: 100_000,
97            max_bytes: 256 * 1024 * 1024, // 256 MB
98            max_age_secs: 3600,           // 1 hour
99            build_vector_index: true,
100            build_graph_index: true,
101            compression_level: 6,
102        }
103    }
104}
105
106/// Segment statistics
107#[derive(Debug, Clone, Default)]
108pub struct SegmentStats {
109    /// Number of entities
110    pub entity_count: usize,
111    /// Number of deleted entities
112    pub deleted_count: usize,
113    /// Approximate memory usage in bytes
114    pub memory_bytes: usize,
115    /// Number of vectors
116    pub vector_count: usize,
117    /// Number of graph nodes
118    pub node_count: usize,
119    /// Number of graph edges
120    pub edge_count: usize,
121    /// Number of table rows
122    pub row_count: usize,
123    /// Number of cross-references
124    pub cross_ref_count: usize,
125}
126
127/// Segment error types
128#[derive(Debug, Clone)]
129pub enum SegmentError {
130    /// Segment is not writable
131    NotWritable,
132    /// Entity not found
133    NotFound(EntityId),
134    /// Entity already exists
135    AlreadyExists(EntityId),
136    /// Segment is full
137    Full,
138    /// Invalid operation for current state
139    InvalidState(SegmentState),
140    /// Internal error
141    Internal(String),
142}
143
144impl std::fmt::Display for SegmentError {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::NotWritable => write!(f, "segment is not writable"),
148            Self::NotFound(id) => write!(f, "entity not found: {}", id),
149            Self::AlreadyExists(id) => write!(f, "entity already exists: {}", id),
150            Self::Full => write!(f, "segment is full"),
151            Self::InvalidState(state) => write!(f, "invalid operation for state: {:?}", state),
152            Self::Internal(msg) => write!(f, "internal error: {}", msg),
153        }
154    }
155}
156
157impl std::error::Error for SegmentError {}
158
159fn current_unix_secs() -> u64 {
160    SystemTime::now()
161        .duration_since(UNIX_EPOCH)
162        .unwrap_or_default()
163        .as_secs()
164}
165
166const SEALED_MULTI_ZONE_MAX_INTERVALS: usize = 4;
167
168/// Approximate cost of one entry in the `deleted` tombstone set: the 8-byte
169/// entity id plus hashbrown's control byte and load-factor slack.
170///
171/// Tombstones outlive the entity they bury — a sealed segment holds its
172/// `deleted` set for as long as the segment lives. That set is the memory
173/// consolidation reclaims even in HashMap mode, where the entity body itself
174/// was already freed by `force_delete`.
175const TOMBSTONE_ENTRY_BYTES: u64 = 16;
176
177#[derive(Debug, Clone)]
178struct UpdateIndexSnapshot {
179    pk_column_name: Option<String>,
180    pk_value: Option<Value>,
181    pk_index_key: Option<(String, String)>,
182    cross_refs: Vec<CrossRef>,
183}
184
185impl UpdateIndexSnapshot {
186    fn from_entity(entity: &UnifiedEntity) -> Self {
187        let (pk_column_name, pk_value) = match &entity.data {
188            EntityData::Row(row) => (
189                row.schema
190                    .as_deref()
191                    .and_then(|schema| schema.first().cloned()),
192                row.columns.first().cloned(),
193            ),
194            _ => (None, None),
195        };
196        let pk_index_key = pk_value
197            .as_ref()
198            .map(|value| (entity.kind.collection().to_string(), format!("{:?}", value)));
199        Self {
200            pk_column_name,
201            pk_value,
202            pk_index_key,
203            cross_refs: entity.cross_refs().to_vec(),
204        }
205    }
206}
207
208/// A unified segment that stores all entity types
209pub trait UnifiedSegment: Send + Sync {
210    /// Get segment ID
211    fn id(&self) -> SegmentId;
212
213    /// Get current state
214    fn state(&self) -> SegmentState;
215
216    /// Get collection/namespace name
217    fn collection(&self) -> &str;
218
219    /// Get statistics
220    fn stats(&self) -> SegmentStats;
221
222    /// O(1) live entity count (entities minus tombstones)
223    fn entity_count(&self) -> usize;
224
225    /// Check if entity exists
226    fn contains(&self, id: EntityId) -> bool;
227
228    /// Get an entity by ID
229    fn get(&self, id: EntityId) -> Option<&UnifiedEntity>;
230
231    /// Get mutable reference to entity
232    fn get_mut(&mut self, id: EntityId) -> Option<&mut UnifiedEntity>;
233
234    /// Insert a new entity
235    fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError>;
236
237    /// Update an existing entity
238    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError>;
239
240    /// HOT-update: like update but receives the set of field names that actually
241    /// changed. Allows skipping index work when indexed columns are unaffected.
242    /// Default: falls back to full update.
243    fn update_hot(
244        &mut self,
245        entity: UnifiedEntity,
246        modified_columns: &[String],
247    ) -> Result<(), SegmentError> {
248        let _ = modified_columns;
249        self.update(entity)
250    }
251
252    /// Delete an entity
253    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError>;
254
255    /// Get metadata for an entity
256    fn get_metadata(&self, id: EntityId) -> Option<Metadata>;
257
258    /// Set metadata for an entity
259    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError>;
260
261    /// Seal the segment (make immutable)
262    fn seal(&mut self) -> Result<(), SegmentError>;
263
264    /// Check if should auto-seal based on config
265    fn should_seal(&self, config: &SegmentConfig) -> bool;
266
267    /// Iterate over all entities
268    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
269
270    /// Iterate over entities of a specific kind
271    fn iter_kind(&self, kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
272
273    /// Search entities by metadata filter
274    fn filter_metadata(
275        &self,
276        filters: &[(String, super::metadata::MetadataFilter)],
277    ) -> Vec<EntityId>;
278}
279
280// ─────────────────────────────────────────────────────────────────────────────
281// Zone map: per-column min/max for segment pruning
282// ─────────────────────────────────────────────────────────────────────────────
283
284/// Tracks the min and max observed `Value` for one column in a segment.
285/// Used to skip segments that cannot satisfy a range or equality predicate.
286#[derive(Debug, Clone)]
287pub struct ColZone {
288    pub min: Value,
289    pub max: Value,
290    min_key: Option<CanonicalKey>,
291    max_key: Option<CanonicalKey>,
292}
293
294impl ColZone {
295    fn new(v: Value) -> Self {
296        Self {
297            min_key: value_to_canonical_key(&v),
298            max_key: value_to_canonical_key(&v),
299            min: v.clone(),
300            max: v,
301        }
302    }
303
304    fn with_bounds(min: Value, max: Value) -> Self {
305        Self {
306            min_key: value_to_canonical_key(&min),
307            max_key: value_to_canonical_key(&max),
308            min,
309            max,
310        }
311    }
312
313    fn update(&mut self, v: &Value) {
314        if compare_zone_values(v, None, &self.min, self.min_key.as_ref())
315            .map(|o| o == std::cmp::Ordering::Less)
316            .unwrap_or(false)
317        {
318            self.min = v.clone();
319            self.min_key = value_to_canonical_key(v);
320        }
321        if compare_zone_values(v, None, &self.max, self.max_key.as_ref())
322            .map(|o| o == std::cmp::Ordering::Greater)
323            .unwrap_or(false)
324        {
325            self.max = v.clone();
326            self.max_key = value_to_canonical_key(v);
327        }
328    }
329}
330
331#[derive(Debug, Clone, Default)]
332pub struct MultiColZone {
333    pub intervals: Vec<ColZone>,
334}
335
336impl MultiColZone {
337    fn can_skip(&self, pred: &ZoneColPred<'_>) -> bool {
338        !self.intervals.is_empty() && self.intervals.iter().all(|zone| pred.can_skip(zone))
339    }
340}
341
342fn compare_zone_values(
343    left: &Value,
344    left_key: Option<&CanonicalKey>,
345    right: &Value,
346    right_key: Option<&CanonicalKey>,
347) -> Option<std::cmp::Ordering> {
348    partial_compare_values(left, right).or_else(|| {
349        let left_key = left_key.cloned().or_else(|| value_to_canonical_key(left))?;
350        let right_key = right_key
351            .cloned()
352            .or_else(|| value_to_canonical_key(right))?;
353        (left_key.family() == right_key.family()).then(|| left_key.cmp(&right_key))
354    })
355}
356
357/// Tag-only variant of `ZoneColPred` — used where the Value is stored separately.
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum ZoneColPredKind {
360    Eq,
361    Gt,
362    Gte,
363    Lt,
364    Lte,
365}
366
367/// A predicate on a single column that can be checked against a `ColZone`.
368#[derive(Debug, Clone)]
369pub enum ZoneColPred<'a> {
370    Eq(&'a Value),
371    Gt(&'a Value),
372    Gte(&'a Value),
373    Lt(&'a Value),
374    Lte(&'a Value),
375}
376
377impl<'a> ZoneColPred<'a> {
378    /// Returns `true` when the entire segment can be skipped (no row can match).
379    pub fn can_skip(&self, zone: &ColZone) -> bool {
380        match self {
381            // Equality: skip if val < min OR val > max
382            ZoneColPred::Eq(val) => {
383                compare_zone_values(val, None, &zone.min, zone.min_key.as_ref())
384                    .map(|o| o == std::cmp::Ordering::Less)
385                    .unwrap_or(false)
386                    || compare_zone_values(val, None, &zone.max, zone.max_key.as_ref())
387                        .map(|o| o == std::cmp::Ordering::Greater)
388                        .unwrap_or(false)
389            }
390            // col > val: skip if max <= val (all rows have col ≤ val, none > val)
391            ZoneColPred::Gt(val) => {
392                compare_zone_values(&zone.max, zone.max_key.as_ref(), val, None)
393                    .map(|o| o != std::cmp::Ordering::Greater)
394                    .unwrap_or(false)
395            }
396            // col >= val: skip if max < val
397            ZoneColPred::Gte(val) => {
398                compare_zone_values(&zone.max, zone.max_key.as_ref(), val, None)
399                    .map(|o| o == std::cmp::Ordering::Less)
400                    .unwrap_or(false)
401            }
402            // col < val: skip if min >= val
403            ZoneColPred::Lt(val) => {
404                compare_zone_values(&zone.min, zone.min_key.as_ref(), val, None)
405                    .map(|o| o != std::cmp::Ordering::Less)
406                    .unwrap_or(false)
407            }
408            // col <= val: skip if min > val
409            ZoneColPred::Lte(val) => {
410                compare_zone_values(&zone.min, zone.min_key.as_ref(), val, None)
411                    .map(|o| o == std::cmp::Ordering::Greater)
412                    .unwrap_or(false)
413            }
414        }
415    }
416}
417
418/// Growing segment implementation (in-memory, writable)
419pub struct GrowingSegment {
420    /// Segment ID
421    id: SegmentId,
422    /// Collection/namespace name
423    collection: String,
424    /// Current state
425    state: SegmentState,
426    /// Creation timestamp
427    created_at: u64,
428    /// Last write timestamp
429    last_write_at: u64,
430
431    /// Entity storage (HashMap for random access)
432    entities: HashMap<EntityId, UnifiedEntity>,
433    /// Flat entity storage for bulk inserts (no HashMap overhead, O(1) by offset)
434    /// Used when entity IDs are sequential from base_entity_id
435    flat_entities: Vec<UnifiedEntity>,
436    /// Base entity ID for flat_entities (flat_entities[0].id == base_entity_id)
437    base_entity_id: u64,
438    /// Whether flat storage is active (bulk insert mode)
439    use_flat: bool,
440    /// Deleted entity IDs (tombstones)
441    deleted: HashSet<EntityId>,
442    /// Metadata storage (type-aware)
443    metadata: MetadataStorage,
444
445    /// Primary key index: (collection, pk_value) → EntityId
446    pk_index: BTreeMap<(String, String), EntityId>,
447    /// Type index: kind → EntityIds
448    kind_index: HashMap<String, HashSet<EntityId>>,
449    /// Cross-reference index: source → Vec<(target, ref_type)>
450    cross_ref_forward: HashMap<EntityId, Vec<(EntityId, RefType)>>,
451    /// Reverse cross-reference index: target → Vec<(source, ref_type)>
452    cross_ref_reverse: HashMap<EntityId, Vec<(EntityId, RefType)>>,
453
454    /// Bloom filter for fast negative key lookups
455    bloom: BloomFilter,
456
457    /// Per-column zone maps: col_name → (min, max) for segment pruning
458    col_zones: HashMap<String, ColZone>,
459    /// Sealed-only minmax-multi summaries built from canonical ordering.
460    sealed_col_zones: HashMap<String, MultiColZone>,
461
462    /// Sequence counter for ordering
463    sequence: AtomicU64,
464    /// Approximate payload bytes of the entities physically resident in this
465    /// segment. Resident means "still occupying memory", which includes
466    /// tombstoned entities in flat mode (their slot must stay so positional
467    /// lookup by `id - base_entity_id` keeps working).
468    memory_bytes: AtomicU64,
469    /// Payload bytes of the tombstoned entities that are still resident.
470    /// `memory_bytes - dead_entity_bytes` is the live payload.
471    dead_entity_bytes: u64,
472    /// Count of tombstoned entities that are still resident.
473    dead_resident_count: usize,
474
475    /// Epoch counter for lock-free reads of `flat_entities`.
476    ///
477    /// Updated with `Release` ordering after every flat-mode insert so that
478    /// readers can safely access `flat_entities[0..published_flat_len]` by
479    /// loading with `Acquire` ordering, without holding the segment RwLock.
480    /// Only meaningful when `use_flat == true`; always 0 in HashMap mode.
481    pub(crate) published_flat_len: AtomicUsize,
482}
483
484impl GrowingSegment {
485    /// Direct iteration without Box<dyn> trait dispatch. Returns false to stop early.
486    /// Uses concrete iterator types to avoid heap allocation per call.
487    #[inline]
488    pub fn for_each_fast<F>(&self, mut f: F) -> bool
489    where
490        F: FnMut(&UnifiedEntity) -> bool,
491    {
492        if self.use_flat {
493            // Sequential Vec — best cache locality
494            if self.deleted.is_empty() {
495                for entity in &self.flat_entities {
496                    if !f(entity) {
497                        return false;
498                    }
499                }
500                // Also walk the HashMap — entities added via per-row
501                // `insert()` after flat mode was initialized land there.
502                for entity in self.entities.values() {
503                    if !f(entity) {
504                        return false;
505                    }
506                }
507            } else {
508                for entity in &self.flat_entities {
509                    if self.deleted.contains(&entity.id) {
510                        continue;
511                    }
512                    if !f(entity) {
513                        return false;
514                    }
515                }
516                for entity in self.entities.values() {
517                    if self.deleted.contains(&entity.id) {
518                        continue;
519                    }
520                    if !f(entity) {
521                        return false;
522                    }
523                }
524            }
525        } else {
526            // HashMap values — random order, no boxing
527            if self.deleted.is_empty() {
528                for entity in self.entities.values() {
529                    if !f(entity) {
530                        return false;
531                    }
532                }
533            } else {
534                for entity in self.entities.values() {
535                    if self.deleted.contains(&entity.id) {
536                        continue;
537                    }
538                    if !f(entity) {
539                        return false;
540                    }
541                }
542            }
543        }
544        true
545    }
546
547    /// Create a new growing segment
548    pub fn new(id: SegmentId, collection: impl Into<String>) -> Self {
549        Self::with_bloom_capacity(id, collection, 100_000)
550    }
551
552    /// Create a growing segment whose bloom filter is sized for a known
553    /// entity count. Consolidation uses this: the merged segment's live
554    /// count is known up front, so its bloom need not be sized for the
555    /// 100k default when it holds a handful of rows.
556    pub(crate) fn with_bloom_capacity(
557        id: SegmentId,
558        collection: impl Into<String>,
559        expected_entities: usize,
560    ) -> Self {
561        let now = current_unix_secs();
562
563        Self {
564            id,
565            collection: collection.into(),
566            state: SegmentState::Growing,
567            created_at: now,
568            last_write_at: now,
569            entities: HashMap::new(),
570            flat_entities: Vec::new(),
571            base_entity_id: 0,
572            use_flat: false,
573            deleted: HashSet::new(),
574            metadata: MetadataStorage::new(),
575            pk_index: BTreeMap::new(),
576            kind_index: HashMap::new(),
577            cross_ref_forward: HashMap::new(),
578            cross_ref_reverse: HashMap::new(),
579            bloom: BloomFilter::with_capacity(expected_entities.max(1), 0.01),
580            col_zones: HashMap::new(),
581            sealed_col_zones: HashMap::new(),
582            sequence: AtomicU64::new(0),
583            memory_bytes: AtomicU64::new(0),
584            dead_entity_bytes: 0,
585            dead_resident_count: 0,
586            published_flat_len: AtomicUsize::new(0),
587        }
588    }
589
590    /// Get next sequence number
591    fn next_sequence(&self) -> u64 {
592        self.sequence.fetch_add(1, Ordering::SeqCst)
593    }
594
595    fn has_live_entity(&self, id: EntityId) -> bool {
596        if self.deleted.contains(&id) {
597            return false;
598        }
599        if self.use_flat {
600            let raw = id.raw();
601            if raw >= self.base_entity_id {
602                let idx = (raw - self.base_entity_id) as usize;
603                if self
604                    .flat_entities
605                    .get(idx)
606                    .is_some_and(|entity| entity.id == id)
607                {
608                    return true;
609                }
610            }
611            // Same fallback as `get()` — entities added via `insert()`
612            // after flat mode was initialized live only in the HashMap.
613            self.entities.contains_key(&id)
614        } else {
615            self.entities.contains_key(&id)
616        }
617    }
618
619    fn update_existing_entity_in_place(
620        &mut self,
621        entity: &UnifiedEntity,
622    ) -> Result<UpdateIndexSnapshot, SegmentError> {
623        if self.use_flat {
624            let raw = entity.id.raw();
625            if raw < self.base_entity_id {
626                return Err(SegmentError::NotFound(entity.id));
627            }
628            let idx = (raw - self.base_entity_id) as usize;
629            let Some(slot) = self.flat_entities.get_mut(idx) else {
630                return Err(SegmentError::NotFound(entity.id));
631            };
632            if slot.id != entity.id {
633                return Err(SegmentError::NotFound(entity.id));
634            }
635            let snapshot = UpdateIndexSnapshot::from_entity(slot);
636            slot.clone_from(entity);
637            Ok(snapshot)
638        } else {
639            let Some(slot) = self.entities.get_mut(&entity.id) else {
640                return Err(SegmentError::NotFound(entity.id));
641            };
642            let snapshot = UpdateIndexSnapshot::from_entity(slot);
643            slot.clone_from(entity);
644            Ok(snapshot)
645        }
646    }
647
648    fn apply_hot_update_with_metadata(
649        &mut self,
650        entity: &UnifiedEntity,
651        modified_columns: &[String],
652        metadata: Option<&Metadata>,
653    ) -> Result<(), SegmentError> {
654        let old = self.update_existing_entity_in_place(entity)?;
655        self.reindex_for_update(&old, entity, Some(modified_columns));
656        self.update_col_zones_from_entity(entity);
657        if let Some(metadata) = metadata {
658            self.metadata.set_all(entity.id, metadata);
659        }
660        Ok(())
661    }
662
663    fn apply_update_with_metadata(
664        &mut self,
665        entity: &UnifiedEntity,
666        metadata: Option<&Metadata>,
667    ) -> Result<(), SegmentError> {
668        let old = self.update_existing_entity_in_place(entity)?;
669        self.reindex_for_update(&old, entity, None);
670        self.update_col_zones_from_entity(entity);
671        if let Some(metadata) = metadata {
672            self.metadata.set_all(entity.id, metadata);
673        }
674        Ok(())
675    }
676
677    pub fn update_hot_batch_with_metadata<'a, I>(&mut self, items: I) -> Result<(), SegmentError>
678    where
679        I: IntoIterator<Item = (&'a UnifiedEntity, &'a [String], Option<&'a Metadata>)>,
680    {
681        if !self.state.is_writable() {
682            return Err(SegmentError::NotWritable);
683        }
684
685        let items: Vec<(&UnifiedEntity, &[String], Option<&Metadata>)> =
686            items.into_iter().collect();
687        if items.is_empty() {
688            return Ok(());
689        }
690
691        for (entity, _, _) in &items {
692            if !self.has_live_entity(entity.id) {
693                return Err(SegmentError::NotFound(entity.id));
694            }
695        }
696
697        for (entity, modified_columns, metadata) in items {
698            self.apply_hot_update_with_metadata(entity, modified_columns, metadata)?;
699        }
700
701        self.last_write_at = current_unix_secs();
702        Ok(())
703    }
704
705    /// Tombstone a flat-mode entity in place. Its slot stays so that positional
706    /// lookup by `id - base_entity_id` keeps working, so its payload keeps
707    /// costing memory until consolidation merges the segment away.
708    ///
709    /// Returns `false` when the entity was already tombstoned.
710    fn tombstone_flat_entity(&mut self, idx: usize, id: EntityId) -> bool {
711        if !self.deleted.insert(id) {
712            return false;
713        }
714        let size = Self::estimate_entity_size(&self.flat_entities[idx]) as u64;
715        self.dead_entity_bytes += size;
716        self.dead_resident_count += 1;
717        self.metadata.remove_all(id);
718        true
719    }
720
721    /// Remove a HashMap-resident entity. The payload is freed immediately; only
722    /// the tombstone survives, and consolidation reclaims that.
723    ///
724    /// Returns `false` when the entity is not present.
725    fn remove_hashmap_entity(&mut self, id: EntityId) -> bool {
726        let Some(entity) = self.entities.remove(&id) else {
727            return false;
728        };
729        self.release_memory(Self::estimate_entity_size(&entity));
730        self.unindex_entity(&entity);
731        self.metadata.remove_all(id);
732        self.deleted.insert(id);
733        true
734    }
735
736    pub fn delete_batch(&mut self, ids: &[EntityId]) -> Result<Vec<EntityId>, SegmentError> {
737        if !self.state.is_writable() {
738            return Err(SegmentError::NotWritable);
739        }
740        if ids.is_empty() {
741            return Ok(Vec::new());
742        }
743
744        let mut deleted_ids = Vec::with_capacity(ids.len());
745
746        if self.use_flat {
747            for &id in ids {
748                let raw = id.raw();
749                if raw < self.base_entity_id {
750                    if self.remove_hashmap_entity(id) {
751                        deleted_ids.push(id);
752                    }
753                    continue;
754                }
755                let idx = (raw - self.base_entity_id) as usize;
756                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
757                    if self.tombstone_flat_entity(idx, id) {
758                        deleted_ids.push(id);
759                    }
760                } else if self.remove_hashmap_entity(id) {
761                    deleted_ids.push(id);
762                }
763            }
764        } else {
765            for &id in ids {
766                if self.remove_hashmap_entity(id) {
767                    deleted_ids.push(id);
768                }
769            }
770        }
771
772        if !deleted_ids.is_empty() {
773            self.last_write_at = current_unix_secs();
774        }
775
776        Ok(deleted_ids)
777    }
778
779    /// Update memory estimate
780    fn add_memory(&self, bytes: usize) {
781        self.memory_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
782    }
783
784    /// This segment's approximate resident bytes. One relaxed load — the
785    /// arena's contribution to the shared accounting pool (ADR 0073 §2).
786    /// Unlike `stats()` this never walks the entity map, so a stats reader
787    /// does not pay `O(entities)` to observe memory.
788    pub fn memory_bytes(&self) -> u64 {
789        self.memory_bytes.load(Ordering::Relaxed)
790    }
791
792    /// Release a resident entity's payload from the memory estimate. Only for
793    /// paths that physically drop the entity (HashMap removal, eviction).
794    fn release_memory(&self, bytes: usize) {
795        let mut current = self.memory_bytes.load(Ordering::Relaxed);
796        loop {
797            let next = current.saturating_sub(bytes as u64);
798            match self.memory_bytes.compare_exchange_weak(
799                current,
800                next,
801                Ordering::Relaxed,
802                Ordering::Relaxed,
803            ) {
804                Ok(_) => break,
805                Err(observed) => current = observed,
806            }
807        }
808    }
809
810    /// Entities physically present, live or tombstoned-but-resident.
811    pub(crate) fn resident_entity_count(&self) -> usize {
812        self.flat_entities.len() + self.entities.len()
813    }
814
815    /// Entities visible to readers.
816    pub(crate) fn live_entity_count(&self) -> usize {
817        self.resident_entity_count()
818            .saturating_sub(self.dead_resident_count)
819    }
820
821    /// Tombstones this segment carries — including the ones whose entity body
822    /// was already freed. The set itself is what leaks without consolidation.
823    pub(crate) fn tombstone_count(&self) -> usize {
824        self.deleted.len()
825    }
826
827    /// Approximate bytes this segment holds: resident entity payloads plus the
828    /// tombstone set.
829    pub(crate) fn resident_bytes(&self) -> u64 {
830        self.memory_bytes.load(Ordering::Relaxed)
831            + self.deleted.len() as u64 * TOMBSTONE_ENTRY_BYTES
832    }
833
834    /// Bytes a consolidation of this segment would return to the budget: the
835    /// payloads of resident tombstones plus the tombstone set that buries them.
836    pub(crate) fn reclaimable_bytes(&self) -> u64 {
837        self.dead_entity_bytes + self.deleted.len() as u64 * TOMBSTONE_ENTRY_BYTES
838    }
839
840    /// Snapshot the ids of every live entity, in iteration order.
841    ///
842    /// Consolidation copies a segment across several maintenance ticks, so it
843    /// needs a stable cursor into the segment that survives concurrent
844    /// tombstoning. Ids are stable; iteration position is not.
845    pub(crate) fn live_entity_ids(&self) -> Vec<EntityId> {
846        let mut ids = Vec::with_capacity(self.live_entity_count());
847        self.for_each_fast(|entity| {
848            ids.push(entity.id);
849            true
850        });
851        ids
852    }
853
854    /// Take an entity from another segment, preserving its identity.
855    ///
856    /// Unlike `insert`, this keeps `sequence_id` — the merged segment is the
857    /// same rows, not new ones, and callers (`physical_collection_roots`, MVCC
858    /// ordering) read that field. All derived structures are rebuilt: kind
859    /// index, primary-key index, cross-reference forward/reverse indexes, bloom
860    /// filter, and per-column zone maps.
861    pub(crate) fn adopt_entity(&mut self, entity: UnifiedEntity, metadata: Option<Metadata>) {
862        let id = entity.id;
863
864        // Keep the sequence counter ahead of every adopted entity, so a later
865        // insert into this segment cannot re-issue a live sequence number.
866        let next = entity.sequence_id.saturating_add(1);
867        if self.sequence.load(Ordering::Relaxed) < next {
868            self.sequence.store(next, Ordering::Relaxed);
869        }
870
871        self.add_memory(Self::estimate_entity_size(&entity));
872        self.index_entity(&entity);
873        self.update_col_zones_from_entity(&entity);
874        self.entities.insert(id, entity);
875
876        if let Some(metadata) = metadata {
877            if !metadata.is_empty() {
878                self.metadata.set_all(id, &metadata);
879            }
880        }
881    }
882
883    /// Physically remove an entity without leaving a tombstone.
884    ///
885    /// Only legal on a segment nobody can see yet — the half-built merged
886    /// segment, when the swap discovers a copied row was deleted from its
887    /// source mid-consolidation. A tombstone here would defeat the whole point.
888    pub(crate) fn evict_entity(&mut self, id: EntityId) -> bool {
889        let Some(entity) = self.entities.remove(&id) else {
890            return false;
891        };
892        self.release_memory(Self::estimate_entity_size(&entity));
893        self.unindex_entity(&entity);
894        for cross_ref in entity.cross_refs() {
895            if let Some(reverse) = self.cross_ref_reverse.get_mut(&cross_ref.target) {
896                reverse.retain(|(source, _)| *source != id);
897            }
898        }
899        self.metadata.remove_all(id);
900        true
901    }
902
903    /// Estimate memory for an entity
904    fn estimate_entity_size(entity: &UnifiedEntity) -> usize {
905        let mut size = std::mem::size_of::<UnifiedEntity>();
906
907        // Add data size
908        size += match &entity.data {
909            EntityData::Row(row) => row.columns.len() * 64, // Rough estimate
910            EntityData::Node(node) => node.properties.len() * 128,
911            EntityData::Edge(edge) => edge.properties.len() * 128,
912            EntityData::Vector(vec) => {
913                vec.dense.len() * 4 + vec.sparse.as_ref().map_or(0, |s| s.indices.len() * 8)
914            }
915            EntityData::TimeSeries(_) => 64,
916            EntityData::QueueMessage(_) => 128,
917        };
918
919        // Add embeddings
920        for emb in entity.embeddings() {
921            size += emb.vector.len() * 4 + emb.name.len() + emb.model.len();
922        }
923
924        // Add cross-refs
925        size += std::mem::size_of_val(entity.cross_refs());
926
927        size
928    }
929
930    /// Update per-column zone maps from a newly inserted entity's fields.
931    ///
932    /// Handles both insert paths:
933    /// - **Named** (`row.named`): individual inserts where fields are a `HashMap<String, Value>`
934    /// - **Positional** (`row.columns` + `row.schema`): bulk-inserted entities stored as `Vec<Value>`
935    ///   keyed by the shared schema. Previously this path was silently skipped, meaning zone maps
936    ///   were always empty for bulk-loaded tables and segment pruning never fired.
937    fn update_col_zones_from_entity(&mut self, entity: &UnifiedEntity) {
938        if let EntityData::Row(row) = &entity.data {
939            if let Some(named) = &row.named {
940                // Individual insert path — HashMap fields
941                for (col, val) in named {
942                    if matches!(val, Value::Null) {
943                        continue;
944                    }
945                    self.col_zones
946                        .entry(col.clone())
947                        .and_modify(|z| z.update(val))
948                        .or_insert_with(|| ColZone::new(val.clone()));
949                }
950            } else if let Some(schema) = &row.schema {
951                // Bulk-insert (columnar) path — positional Vec<Value> + shared schema.
952                // Previously skipped: zone maps were always empty for bulk-loaded tables.
953                for (col, val) in schema.iter().zip(row.columns.iter()) {
954                    if matches!(val, Value::Null) {
955                        continue;
956                    }
957                    self.col_zones
958                        .entry(col.clone())
959                        .and_modify(|z| z.update(val))
960                        .or_insert_with(|| ColZone::new(val.clone()));
961                }
962            }
963        }
964    }
965
966    fn rebuild_sealed_col_zones(&mut self) {
967        let mut values_by_col: HashMap<String, Vec<(CanonicalKey, Value)>> = HashMap::new();
968        let mut family_by_col: HashMap<String, crate::storage::schema::CanonicalKeyFamily> =
969            HashMap::new();
970        let mut mixed_family_cols = HashSet::new();
971        let mut unsupported_cols = HashSet::new();
972
973        let mut observe_row = |row: &super::entity::RowData| {
974            for (col, value) in row.iter_fields() {
975                if matches!(value, Value::Null) {
976                    continue;
977                }
978                let Some(key) = value_to_canonical_key(value) else {
979                    unsupported_cols.insert(col.to_string());
980                    continue;
981                };
982                match family_by_col.get(col).copied() {
983                    Some(existing) if existing != key.family() => {
984                        mixed_family_cols.insert(col.to_string());
985                    }
986                    None => {
987                        family_by_col.insert(col.to_string(), key.family());
988                    }
989                    _ => {}
990                }
991                values_by_col
992                    .entry(col.to_string())
993                    .or_default()
994                    .push((key, value.clone()));
995            }
996        };
997
998        if self.use_flat {
999            for entity in &self.flat_entities {
1000                if self.deleted.contains(&entity.id) {
1001                    continue;
1002                }
1003                if let EntityData::Row(row) = &entity.data {
1004                    observe_row(row);
1005                }
1006            }
1007        } else {
1008            for entity in self.entities.values() {
1009                if self.deleted.contains(&entity.id) {
1010                    continue;
1011                }
1012                if let EntityData::Row(row) = &entity.data {
1013                    observe_row(row);
1014                }
1015            }
1016        }
1017
1018        let mut sealed_col_zones = HashMap::new();
1019        for (col, mut entries) in values_by_col {
1020            if mixed_family_cols.contains(&col)
1021                || unsupported_cols.contains(&col)
1022                || entries.is_empty()
1023            {
1024                continue;
1025            }
1026            entries.sort_unstable_by(|left, right| left.0.cmp(&right.0));
1027            entries.dedup_by(|left, right| left.0 == right.0);
1028
1029            let intervals = build_minmax_multi_intervals(&entries, SEALED_MULTI_ZONE_MAX_INTERVALS);
1030            if intervals.len() > 1 {
1031                sealed_col_zones.insert(col, MultiColZone { intervals });
1032            }
1033        }
1034
1035        self.sealed_col_zones = sealed_col_zones;
1036    }
1037
1038    /// Returns `true` when this segment can be entirely skipped for the given predicates.
1039    /// A segment is skipped only if ALL predicates say so (conservative: any non-skippable
1040    /// predicate forces the scan to proceed).
1041    pub fn can_skip_zone_preds(&self, preds: &[(&str, ZoneColPred<'_>)]) -> bool {
1042        if preds.is_empty() {
1043            return false;
1044        }
1045        for (col, pred) in preds {
1046            if let Some(zone) = self.sealed_col_zones.get(*col) {
1047                if zone.can_skip(pred) {
1048                    return true;
1049                }
1050                continue;
1051            }
1052            if let Some(zone) = self.col_zones.get(*col) {
1053                if pred.can_skip(zone) {
1054                    return true; // ONE predicate suffices to skip
1055                }
1056            }
1057        }
1058        false
1059    }
1060
1061    /// Index an entity
1062    fn index_entity(&mut self, entity: &UnifiedEntity) {
1063        // Kind index
1064        let kind_key = entity.kind.storage_type().to_string();
1065        self.kind_index
1066            .entry(kind_key)
1067            .or_default()
1068            .insert(entity.id);
1069
1070        // Bloom filter: insert entity ID bytes for fast negative lookups
1071        let id_bytes = entity.id.raw().to_le_bytes();
1072        self.bloom.insert(&id_bytes);
1073
1074        // Primary key index (if applicable)
1075        if let EntityData::Row(row) = &entity.data {
1076            if let Some(first_col) = row.columns.first() {
1077                let pk_str = format!("{:?}", first_col);
1078                // Also add PK to bloom filter
1079                self.bloom.insert(pk_str.as_bytes());
1080                self.pk_index
1081                    .insert((entity.kind.collection().to_string(), pk_str), entity.id);
1082            }
1083        }
1084
1085        // Cross-reference indices
1086        for cross_ref in entity.cross_refs() {
1087            self.cross_ref_forward
1088                .entry(cross_ref.source)
1089                .or_default()
1090                .push((cross_ref.target, cross_ref.ref_type));
1091
1092            self.cross_ref_reverse
1093                .entry(cross_ref.target)
1094                .or_default()
1095                .push((cross_ref.source, cross_ref.ref_type));
1096        }
1097    }
1098
1099    /// Check if a primary key value might exist in this segment via bloom filter.
1100    pub fn bloom_might_contain_key(&self, key: &[u8]) -> bool {
1101        self.bloom.contains(key)
1102    }
1103
1104    /// Get bloom filter statistics
1105    pub fn bloom_stats(&self) -> (f64, u32) {
1106        (self.bloom.fill_ratio(), self.bloom.count_set_bits())
1107    }
1108
1109    /// Remove entity from indices
1110    fn unindex_entity(&mut self, entity: &UnifiedEntity) {
1111        // Kind index
1112        let kind_key = entity.kind.storage_type().to_string();
1113        if let Some(set) = self.kind_index.get_mut(&kind_key) {
1114            set.remove(&entity.id);
1115        }
1116
1117        // Primary key index
1118        if let EntityData::Row(row) = &entity.data {
1119            if let Some(first_col) = row.columns.first() {
1120                let pk_str = format!("{:?}", first_col);
1121                self.pk_index
1122                    .remove(&(entity.kind.collection().to_string(), pk_str));
1123            }
1124        }
1125
1126        // Cross-reference indices
1127        self.cross_ref_forward.remove(&entity.id);
1128        // Note: reverse refs from this entity still need cleanup
1129    }
1130
1131    /// Selective re-index for updates.
1132    ///
1133    /// Skips index work that is not needed:
1134    /// - `kind_index`: entity kind is immutable, so remove+reinsert is always
1135    ///   a no-op; we skip it entirely and keep the existing entry.
1136    /// - `pk_index`: only updated when the primary-key column (first column of
1137    ///   a Row entity) actually changed. When `modified_columns` is provided,
1138    ///   we check membership; otherwise we compare old vs new pk value.
1139    /// - `bloom`: add-only by design, so we only insert the new pk when it
1140    ///   genuinely changes (old entry is a benign false positive).
1141    /// - `cross_ref`: only rebuilt when the refs actually differ.
1142    fn reindex_for_update(
1143        &mut self,
1144        old: &UpdateIndexSnapshot,
1145        new: &UnifiedEntity,
1146        modified_columns: Option<&[String]>,
1147    ) {
1148        // kind_index: kind is immutable — the existing entry is already correct.
1149        // No remove + reinsert needed.
1150
1151        // bloom: entity ID never changes; already present from insert.
1152
1153        // pk_index: only update when pk column is touched
1154        let pk_changed = match &new.data {
1155            EntityData::Row(new_row) => {
1156                if let Some(cols) = modified_columns {
1157                    // Caller told us exactly what changed — check if first schema column modified
1158                    // pk is the first column; check by name against the schema or by position 0
1159                    let pk_col_name = old.pk_column_name.as_deref().or_else(|| {
1160                        new_row
1161                            .schema
1162                            .as_deref()
1163                            .and_then(|schema| schema.first().map(|name| name.as_str()))
1164                    });
1165                    match pk_col_name {
1166                        Some(pk_name) => cols.iter().any(|c| c.eq_ignore_ascii_case(pk_name)),
1167                        // No schema — fall back to value comparison
1168                        None => old.pk_value.as_ref() != new_row.columns.first(),
1169                    }
1170                } else {
1171                    old.pk_value.as_ref() != new_row.columns.first()
1172                }
1173            }
1174            // Non-row types don't use pk_index
1175            _ => false,
1176        };
1177
1178        if pk_changed {
1179            // Remove old pk entry
1180            if let Some((collection, pk_str)) = &old.pk_index_key {
1181                self.pk_index.remove(&(collection.clone(), pk_str.clone()));
1182            }
1183            // Insert new pk entry
1184            if let EntityData::Row(row) = &new.data {
1185                if let Some(first_col) = row.columns.first() {
1186                    let pk_str = format!("{:?}", first_col);
1187                    self.bloom.insert(pk_str.as_bytes());
1188                    self.pk_index
1189                        .insert((new.kind.collection().to_string(), pk_str), new.id);
1190                }
1191            }
1192        }
1193
1194        // cross_ref: only rebuild when refs actually changed
1195        let new_refs = new.cross_refs();
1196        if old.cross_refs.as_slice() != new_refs {
1197            // Remove stale forward refs
1198            self.cross_ref_forward.remove(&new.id);
1199            // Prune stale entries from reverse index
1200            for cross_ref in &old.cross_refs {
1201                if let Some(rev) = self.cross_ref_reverse.get_mut(&cross_ref.target) {
1202                    rev.retain(|(src, _)| *src != new.id);
1203                }
1204            }
1205            // Add new refs
1206            for cross_ref in new_refs {
1207                self.cross_ref_forward
1208                    .entry(cross_ref.source)
1209                    .or_default()
1210                    .push((cross_ref.target, cross_ref.ref_type));
1211                self.cross_ref_reverse
1212                    .entry(cross_ref.target)
1213                    .or_default()
1214                    .push((cross_ref.source, cross_ref.ref_type));
1215            }
1216        }
1217    }
1218
1219    /// Get entities referencing the given entity
1220    pub fn get_references_to(&self, id: EntityId) -> Vec<(EntityId, RefType)> {
1221        self.cross_ref_reverse.get(&id).cloned().unwrap_or_default()
1222    }
1223
1224    /// Get entities referenced by the given entity
1225    pub fn get_references_from(&self, id: EntityId) -> Vec<(EntityId, RefType)> {
1226        self.cross_ref_forward.get(&id).cloned().unwrap_or_default()
1227    }
1228
1229    /// Get age in seconds
1230    pub fn age_secs(&self) -> u64 {
1231        let now = current_unix_secs();
1232        now.saturating_sub(self.created_at)
1233    }
1234
1235    /// Get time since last write
1236    pub fn idle_secs(&self) -> u64 {
1237        let now = current_unix_secs();
1238        now.saturating_sub(self.last_write_at)
1239    }
1240
1241    /// Turbo bulk insert — minimal allocations per entity.
1242    ///
1243    /// Optimizations vs normal insert:
1244    /// - Skips bloom filter, cross-refs, memory tracking
1245    /// - Computes kind_key ONCE (not per entity)
1246    /// - Pre-allocates kind_index HashSet
1247    /// - Skips contains_key check (caller guarantees unique IDs)
1248    /// - Uses Relaxed ordering for sequence counter
1249    pub fn bulk_insert(
1250        &mut self,
1251        entities: Vec<UnifiedEntity>,
1252    ) -> Result<Vec<EntityId>, SegmentError> {
1253        if !self.state.is_writable() {
1254            return Err(SegmentError::NotWritable);
1255        }
1256
1257        let n = entities.len();
1258
1259        // Compute kind_key ONCE
1260        let kind_key = if let Some(first) = entities.first() {
1261            first.kind.storage_type().to_string()
1262        } else {
1263            return Ok(Vec::new());
1264        };
1265
1266        let kind_set = self.kind_index.entry(kind_key).or_default();
1267        kind_set.reserve(n);
1268
1269        let now = current_unix_secs();
1270
1271        let base_seq = self.sequence.fetch_add(n as u64, Ordering::Relaxed);
1272
1273        let mut ids = Vec::with_capacity(n);
1274
1275        // Use flat storage (Vec) instead of HashMap — saves ~80 bytes/entity overhead
1276        if self.flat_entities.is_empty() && self.entities.is_empty() {
1277            // First bulk insert: initialize flat storage
1278            self.base_entity_id = entities.first().map(|e| e.id.raw()).unwrap_or(0);
1279            self.use_flat = true;
1280        }
1281
1282        // Collect zone-update values per column position, not per
1283        // (row, col). The old code did `col.to_string()` + HashMap
1284        // probe per cell — 15 cols × 25k rows = 375K String
1285        // allocations per bulk_insert call for the typed_insert bench.
1286        //
1287        // New shape: accumulate `Vec<Value>` per column index; after
1288        // the scan, walk each non-empty column exactly once, resolve
1289        // its name via the shared schema, and apply all observations
1290        // under a single HashMap entry. 375K allocs → ~ncols allocs.
1291        let mut columnar_zone_updates: Vec<Vec<Value>> = Vec::new();
1292        let mut columnar_schema: Option<std::sync::Arc<Vec<String>>> = None;
1293        // Named-row fallback (non-prevalidated path) still uses the
1294        // per-cell vec — these rows don't share a schema, so there's
1295        // no columnar shortcut.
1296        let mut named_zone_updates: Vec<(String, Value)> = Vec::new();
1297
1298        // Payload bytes for the whole batch, applied with one atomic add.
1299        // `bulk_insert` used to skip memory tracking entirely, which left the
1300        // budget blind to the rows that dominate a bulk-loaded store — and
1301        // blind to what consolidation reclaims from them.
1302        let mut batch_bytes: usize = 0;
1303
1304        if self.use_flat {
1305            self.flat_entities.reserve(n);
1306            for (i, mut entity) in entities.into_iter().enumerate() {
1307                entity.sequence_id = base_seq + i as u64;
1308                let id = entity.id;
1309                kind_set.insert(id);
1310                ids.push(id);
1311                batch_bytes += Self::estimate_entity_size(&entity);
1312                if let EntityData::Row(row) = &entity.data {
1313                    if row.schema.is_some() && !row.columns.is_empty() {
1314                        if columnar_zone_updates.is_empty() {
1315                            columnar_zone_updates = vec![Vec::with_capacity(n); row.columns.len()];
1316                            columnar_schema = row.schema.clone();
1317                        }
1318                        for (ci, val) in row.columns.iter().enumerate() {
1319                            if !matches!(val, Value::Null) {
1320                                if let Some(bucket) = columnar_zone_updates.get_mut(ci) {
1321                                    bucket.push(val.clone());
1322                                }
1323                            }
1324                        }
1325                    } else {
1326                        for (col, val) in row.iter_fields() {
1327                            if !matches!(val, Value::Null) {
1328                                named_zone_updates.push((col.to_string(), val.clone()));
1329                            }
1330                        }
1331                    }
1332                }
1333                // Position-based `flat_entities` access in `get(id)`
1334                // assumes IDs are contiguous starting at
1335                // `base_entity_id`. When IDs ARE contiguous (the bulk
1336                // ingest hot path) this saves a HashMap probe per
1337                // lookup. But the global ID counter can jump between
1338                // bulk calls — CREATE INDEX, catalog DDL, even other
1339                // collections in the same store reserve IDs from the
1340                // same allocator. If we blindly `push` a gap entity
1341                // its actual position in the Vec no longer matches
1342                // `id - base`, and `flat_entities[id - base].id == id`
1343                // fails — the entity becomes invisible to `get(id)`.
1344                //
1345                // Route gap entities into the HashMap fallback
1346                // (`self.entities`). `get(id)` already falls through
1347                // there when the flat probe misses.
1348                let expected = self.base_entity_id + self.flat_entities.len() as u64;
1349                if id.raw() == expected {
1350                    self.flat_entities.push(entity);
1351                } else {
1352                    self.entities.insert(id, entity);
1353                }
1354            }
1355        } else {
1356            // Fallback to HashMap for non-sequential inserts
1357            self.entities.reserve(n);
1358            let mut pairs = Vec::with_capacity(n);
1359            for (i, mut entity) in entities.into_iter().enumerate() {
1360                entity.sequence_id = base_seq + i as u64;
1361                let id = entity.id;
1362                kind_set.insert(id);
1363                ids.push(id);
1364                batch_bytes += Self::estimate_entity_size(&entity);
1365                if let EntityData::Row(row) = &entity.data {
1366                    for (col, val) in row.iter_fields() {
1367                        if !matches!(val, Value::Null) {
1368                            named_zone_updates.push((col.to_string(), val.clone()));
1369                        }
1370                    }
1371                }
1372                pairs.push((id, entity));
1373            }
1374            self.entities.extend(pairs);
1375        }
1376
1377        // Apply zone updates now that kind_set borrow is released.
1378        // Columnar path: one `col_zones.entry` call per column (not
1379        // per cell). Named-fallback path: unchanged.
1380        let _ = kind_set;
1381        if !columnar_zone_updates.is_empty() {
1382            let schema = columnar_schema.as_ref();
1383            for (ci, values) in columnar_zone_updates.into_iter().enumerate() {
1384                if values.is_empty() {
1385                    continue;
1386                }
1387                let Some(col_name) = schema.and_then(|s| s.get(ci)) else {
1388                    continue;
1389                };
1390                // Enter the HashMap once per column. `raw_entry_mut`
1391                // avoids the String::clone when the column already
1392                // exists in the map — but that's nightly-only, so we
1393                // pay one `clone()` per call-per-column (ncols total)
1394                // instead of the old ncols×nrows.
1395                let mut iter = values.into_iter();
1396                if let Some(first) = iter.next() {
1397                    let zone = self
1398                        .col_zones
1399                        .entry(col_name.clone())
1400                        .and_modify(|z| z.update(&first))
1401                        .or_insert_with(|| ColZone::new(first));
1402                    for v in iter {
1403                        zone.update(&v);
1404                    }
1405                }
1406            }
1407        }
1408        for (col, val) in named_zone_updates {
1409            self.col_zones
1410                .entry(col)
1411                .and_modify(|z| z.update(&val))
1412                .or_insert_with(|| ColZone::new(val));
1413        }
1414
1415        self.add_memory(batch_bytes);
1416        self.last_write_at = now;
1417
1418        // Publish the new flat length so lock-free readers can see the new entities.
1419        if self.use_flat {
1420            self.published_flat_len
1421                .store(self.flat_entities.len(), Ordering::Release);
1422        }
1423
1424        Ok(ids)
1425    }
1426
1427    /// Delete from this segment regardless of its seal state.
1428    /// Used to mutate sealed segments when DELETE touches bulk-inserted entities.
1429    pub(crate) fn force_delete(&mut self, id: EntityId) -> bool {
1430        if self.use_flat {
1431            let raw = id.raw();
1432            if raw >= self.base_entity_id {
1433                let idx = (raw - self.base_entity_id) as usize;
1434                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
1435                    self.tombstone_flat_entity(idx, id);
1436                    return true;
1437                }
1438            }
1439            return self.remove_hashmap_entity(id);
1440        }
1441
1442        self.remove_hashmap_entity(id)
1443    }
1444
1445    /// Update an entity in this segment regardless of its seal state.
1446    /// Used to mutate sealed segments when UPDATE touches bulk-inserted entities.
1447    pub(crate) fn force_update_with_metadata(
1448        &mut self,
1449        entity: &UnifiedEntity,
1450        modified_columns: &[String],
1451        metadata: Option<&Metadata>,
1452    ) -> Result<(), SegmentError> {
1453        self.apply_hot_update_with_metadata(entity, modified_columns, metadata)
1454    }
1455}
1456
1457impl UnifiedSegment for GrowingSegment {
1458    fn id(&self) -> SegmentId {
1459        self.id
1460    }
1461
1462    fn state(&self) -> SegmentState {
1463        self.state
1464    }
1465
1466    fn collection(&self) -> &str {
1467        &self.collection
1468    }
1469
1470    fn stats(&self) -> SegmentStats {
1471        let mut stats = SegmentStats {
1472            entity_count: self.live_entity_count(),
1473            deleted_count: self.deleted.len(),
1474            memory_bytes: self.resident_bytes() as usize,
1475            ..Default::default()
1476        };
1477
1478        for entity in self.entities.values() {
1479            match &entity.kind {
1480                EntityKind::TableRow { .. } => stats.row_count += 1,
1481                EntityKind::GraphNode(_) => stats.node_count += 1,
1482                EntityKind::GraphEdge(_) => stats.edge_count += 1,
1483                EntityKind::Vector { .. } => stats.vector_count += 1,
1484                EntityKind::TimeSeriesPoint(_) => stats.row_count += 1,
1485                EntityKind::QueueMessage { .. } => stats.row_count += 1,
1486            }
1487            stats.cross_ref_count += entity.cross_refs().len();
1488        }
1489
1490        stats
1491    }
1492
1493    fn entity_count(&self) -> usize {
1494        self.live_entity_count()
1495    }
1496
1497    fn contains(&self, id: EntityId) -> bool {
1498        self.has_live_entity(id)
1499    }
1500
1501    fn get(&self, id: EntityId) -> Option<&UnifiedEntity> {
1502        if self.deleted.contains(&id) {
1503            return None;
1504        }
1505        if self.use_flat {
1506            let raw = id.raw();
1507            if raw >= self.base_entity_id {
1508                let idx = (raw - self.base_entity_id) as usize;
1509                if let Some(entity) = self.flat_entities.get(idx).filter(|e| e.id == id) {
1510                    return Some(entity);
1511                }
1512            }
1513            // Fall through: once flat mode is initialized by a bulk_insert,
1514            // subsequent per-row `insert()` calls still write into the
1515            // HashMap (see the impl below) so reads must check both. Without
1516            // this fallback, a post-bulk single-row insert silently
1517            // disappears from `get()` / `query_all()`.
1518            self.entities.get(&id)
1519        } else {
1520            self.entities.get(&id)
1521        }
1522    }
1523
1524    fn get_mut(&mut self, id: EntityId) -> Option<&mut UnifiedEntity> {
1525        if self.deleted.contains(&id) || !self.state.is_writable() {
1526            return None;
1527        }
1528        if self.use_flat {
1529            let raw = id.raw();
1530            if raw >= self.base_entity_id {
1531                let idx = (raw - self.base_entity_id) as usize;
1532                if self
1533                    .flat_entities
1534                    .get(idx)
1535                    .map(|e| e.id == id)
1536                    .unwrap_or(false)
1537                {
1538                    return self.flat_entities.get_mut(idx);
1539                }
1540            }
1541            // Fall through to HashMap for entities inserted via
1542            // per-row `insert()` after flat mode was activated.
1543            self.entities.get_mut(&id)
1544        } else {
1545            self.entities.get_mut(&id)
1546        }
1547    }
1548
1549    fn insert(&mut self, mut entity: UnifiedEntity) -> Result<EntityId, SegmentError> {
1550        if !self.state.is_writable() {
1551            return Err(SegmentError::NotWritable);
1552        }
1553
1554        if self.entities.contains_key(&entity.id) {
1555            return Err(SegmentError::AlreadyExists(entity.id));
1556        }
1557
1558        // Assign sequence ID
1559        entity.sequence_id = self.next_sequence();
1560
1561        // Estimate and track memory
1562        let size = Self::estimate_entity_size(&entity);
1563        self.add_memory(size);
1564
1565        // Index the entity
1566        self.index_entity(&entity);
1567
1568        // Update column zone maps for range-based segment pruning
1569        self.update_col_zones_from_entity(&entity);
1570
1571        // Store
1572        let id = entity.id;
1573        self.entities.insert(id, entity);
1574
1575        // Update write timestamp
1576        self.last_write_at = current_unix_secs();
1577
1578        Ok(id)
1579    }
1580
1581    fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError> {
1582        if !self.state.is_writable() {
1583            return Err(SegmentError::NotWritable);
1584        }
1585
1586        self.apply_update_with_metadata(&entity, None)?;
1587        self.last_write_at = current_unix_secs();
1588
1589        Ok(())
1590    }
1591
1592    fn update_hot(
1593        &mut self,
1594        entity: UnifiedEntity,
1595        modified_columns: &[String],
1596    ) -> Result<(), SegmentError> {
1597        if !self.state.is_writable() {
1598            return Err(SegmentError::NotWritable);
1599        }
1600
1601        self.apply_hot_update_with_metadata(&entity, modified_columns, None)?;
1602        self.last_write_at = current_unix_secs();
1603        Ok(())
1604    }
1605
1606    fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError> {
1607        if !self.state.is_writable() {
1608            return Err(SegmentError::NotWritable);
1609        }
1610
1611        // For flat storage, use tombstone (don't remove from Vec to keep indices valid)
1612        if self.use_flat {
1613            let raw = id.raw();
1614            if raw >= self.base_entity_id {
1615                let idx = (raw - self.base_entity_id) as usize;
1616                if idx < self.flat_entities.len() && self.flat_entities[idx].id == id {
1617                    self.tombstone_flat_entity(idx, id);
1618                    return Ok(true);
1619                }
1620            }
1621            return Ok(self.remove_hashmap_entity(id));
1622        }
1623
1624        Ok(self.remove_hashmap_entity(id))
1625    }
1626
1627    fn get_metadata(&self, id: EntityId) -> Option<Metadata> {
1628        if !self.has_live_entity(id) {
1629            return None;
1630        }
1631        Some(self.metadata.get_all(id))
1632    }
1633
1634    fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError> {
1635        if !self.state.is_writable() {
1636            return Err(SegmentError::NotWritable);
1637        }
1638
1639        if !self.has_live_entity(id) {
1640            return Err(SegmentError::NotFound(id));
1641        }
1642
1643        self.metadata.set_all(id, &metadata);
1644        Ok(())
1645    }
1646
1647    fn seal(&mut self) -> Result<(), SegmentError> {
1648        if self.state != SegmentState::Growing {
1649            return Err(SegmentError::InvalidState(self.state));
1650        }
1651
1652        self.state = SegmentState::Sealing;
1653
1654        // Build indices on the sealed data:
1655        // - Bloom filter is already populated from insert()
1656        // - HNSW/IVF for vectors (future)
1657        // - B-tree for sorted access (future)
1658        // - Inverted index for text search (future)
1659        self.rebuild_sealed_col_zones();
1660
1661        self.state = SegmentState::Sealed;
1662        Ok(())
1663    }
1664
1665    fn should_seal(&self, config: &SegmentConfig) -> bool {
1666        // Check entity count
1667        if self.entities.len() >= config.max_entities {
1668            return true;
1669        }
1670
1671        // Check memory usage
1672        if self.memory_bytes.load(Ordering::Relaxed) as usize >= config.max_bytes {
1673            return true;
1674        }
1675
1676        // Check age
1677        if self.age_secs() >= config.max_age_secs {
1678            return true;
1679        }
1680
1681        false
1682    }
1683
1684    fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
1685        let base: Box<dyn Iterator<Item = &UnifiedEntity>> = if self.use_flat {
1686            // Chain flat entities (from bulk_insert) with any entities
1687            // that landed in the HashMap via per-row `insert()` after
1688            // flat mode was activated.
1689            Box::new(self.flat_entities.iter().chain(self.entities.values()))
1690        } else {
1691            Box::new(self.entities.values())
1692        };
1693        if self.deleted.is_empty() {
1694            base
1695        } else {
1696            Box::new(base.filter(|e| !self.deleted.contains(&e.id)))
1697        }
1698    }
1699
1700    fn iter_kind(&self, kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_> {
1701        let ids = self.kind_index.get(kind_filter).cloned();
1702        // In flat mode entities live in `flat_entities`, not `entities` —
1703        // chain both so iter_kind doesn't drop bulk-inserted entities.
1704        let flat: Box<dyn Iterator<Item = &UnifiedEntity>> = if self.use_flat {
1705            Box::new(self.flat_entities.iter())
1706        } else {
1707            Box::new(std::iter::empty())
1708        };
1709        Box::new(flat.chain(self.entities.values()).filter(move |e| {
1710            if self.deleted.contains(&e.id) {
1711                return false;
1712            }
1713            if let Some(ref ids) = ids {
1714                ids.contains(&e.id)
1715            } else {
1716                false
1717            }
1718        }))
1719    }
1720
1721    fn filter_metadata(
1722        &self,
1723        filters: &[(String, super::metadata::MetadataFilter)],
1724    ) -> Vec<EntityId> {
1725        // For growing segments, we iterate and filter. In flat mode the
1726        // IDs live in `flat_entities`, not `entities`, so chain both.
1727        let flat_ids: Box<dyn Iterator<Item = EntityId> + '_> = if self.use_flat {
1728            Box::new(self.flat_entities.iter().map(|e| e.id))
1729        } else {
1730            Box::new(std::iter::empty())
1731        };
1732        flat_ids
1733            .chain(self.entities.keys().copied())
1734            .filter(|id| {
1735                if self.deleted.contains(id) {
1736                    return false;
1737                }
1738                let metadata = self.metadata.get_all(*id);
1739                metadata.matches_all(filters)
1740            })
1741            .collect()
1742    }
1743}
1744
1745fn build_minmax_multi_intervals(
1746    entries: &[(CanonicalKey, Value)],
1747    max_intervals: usize,
1748) -> Vec<ColZone> {
1749    if entries.is_empty() {
1750        return Vec::new();
1751    }
1752    if entries.len() == 1 || max_intervals <= 1 {
1753        return vec![ColZone::with_bounds(
1754            entries[0].1.clone(),
1755            entries[entries.len() - 1].1.clone(),
1756        )];
1757    }
1758
1759    let mut split_points = if entries.len() <= max_intervals {
1760        (1..entries.len()).collect::<Vec<_>>()
1761    } else {
1762        let target_splits = max_intervals - 1;
1763        let mut selected = select_gap_split_points(entries, target_splits);
1764        if selected.len() < target_splits {
1765            for bucket in 1..max_intervals {
1766                let idx = bucket * entries.len() / max_intervals;
1767                if idx == 0 || idx >= entries.len() || selected.contains(&idx) {
1768                    continue;
1769                }
1770                selected.push(idx);
1771                if selected.len() >= target_splits {
1772                    break;
1773                }
1774            }
1775        }
1776        selected.sort_unstable();
1777        selected.dedup();
1778        selected
1779    };
1780
1781    split_points.push(entries.len());
1782
1783    let mut out = Vec::with_capacity(split_points.len());
1784    let mut start = 0usize;
1785    for end in split_points {
1786        if end <= start {
1787            continue;
1788        }
1789        out.push(ColZone::with_bounds(
1790            entries[start].1.clone(),
1791            entries[end - 1].1.clone(),
1792        ));
1793        start = end;
1794    }
1795
1796    if out.is_empty() {
1797        out.push(ColZone::with_bounds(
1798            entries[0].1.clone(),
1799            entries[entries.len() - 1].1.clone(),
1800        ));
1801    }
1802
1803    out
1804}
1805
1806fn select_gap_split_points(entries: &[(CanonicalKey, Value)], max_splits: usize) -> Vec<usize> {
1807    let mut gaps = Vec::new();
1808    for idx in 1..entries.len() {
1809        if let Some(score) = canonical_gap_score(&entries[idx - 1].0, &entries[idx].0) {
1810            if score > 0.0 {
1811                gaps.push((score, idx));
1812            }
1813        }
1814    }
1815    gaps.sort_by(|left, right| {
1816        right
1817            .0
1818            .partial_cmp(&left.0)
1819            .unwrap_or(std::cmp::Ordering::Equal)
1820            .then_with(|| left.1.cmp(&right.1))
1821    });
1822    gaps.into_iter()
1823        .take(max_splits)
1824        .map(|(_, idx)| idx)
1825        .collect()
1826}
1827
1828fn canonical_gap_score(left: &CanonicalKey, right: &CanonicalKey) -> Option<f64> {
1829    if left.family() != right.family() {
1830        return None;
1831    }
1832    match (left, right) {
1833        (CanonicalKey::Signed(_, l), CanonicalKey::Signed(_, r)) => {
1834            Some(r.saturating_sub(*l) as f64)
1835        }
1836        (CanonicalKey::Unsigned(_, l), CanonicalKey::Unsigned(_, r)) => {
1837            Some(r.saturating_sub(*l) as f64)
1838        }
1839        (CanonicalKey::Float(l), CanonicalKey::Float(r)) => {
1840            Some((f64::from_bits(*r) - f64::from_bits(*l)).abs())
1841        }
1842        _ => None,
1843    }
1844}
1845
1846#[cfg(test)]
1847mod tests {
1848    use super::*;
1849    use crate::storage::schema::Value;
1850    use crate::storage::unified::entity::RowData;
1851    use crate::storage::unified::MetadataValue;
1852
1853    #[test]
1854    fn test_growing_segment_basic() {
1855        let mut segment = GrowingSegment::new(1, "test");
1856
1857        let entity = UnifiedEntity::table_row(
1858            EntityId::new(1),
1859            "users",
1860            1,
1861            vec![Value::text("Alice".to_string())],
1862        );
1863
1864        let id = segment.insert(entity).unwrap();
1865        assert_eq!(id, EntityId::new(1));
1866        assert!(segment.contains(id));
1867
1868        let stats = segment.stats();
1869        assert_eq!(stats.entity_count, 1);
1870        assert_eq!(stats.row_count, 1);
1871    }
1872
1873    #[test]
1874    fn test_segment_metadata() {
1875        let mut segment = GrowingSegment::new(1, "test");
1876
1877        let entity = UnifiedEntity::table_row(
1878            EntityId::new(1),
1879            "users",
1880            1,
1881            vec![Value::text("Alice".to_string())],
1882        );
1883        segment.insert(entity).unwrap();
1884
1885        let mut meta = Metadata::new();
1886        meta.set("role", MetadataValue::String("admin".to_string()));
1887        meta.set("level", MetadataValue::Int(5));
1888
1889        segment.set_metadata(EntityId::new(1), meta).unwrap();
1890
1891        let retrieved = segment.get_metadata(EntityId::new(1)).unwrap();
1892        assert_eq!(
1893            retrieved.get("role"),
1894            Some(&MetadataValue::String("admin".to_string()))
1895        );
1896    }
1897
1898    #[test]
1899    fn test_segment_seal() {
1900        let mut segment = GrowingSegment::new(1, "test");
1901
1902        let entity = UnifiedEntity::vector(EntityId::new(1), "embeddings", vec![0.1, 0.2, 0.3]);
1903        segment.insert(entity).unwrap();
1904
1905        // Can write before sealing
1906        assert!(segment.state().is_writable());
1907
1908        // Seal the segment
1909        segment.seal().unwrap();
1910        assert_eq!(segment.state(), SegmentState::Sealed);
1911
1912        // Cannot write after sealing
1913        let entity2 = UnifiedEntity::vector(EntityId::new(2), "embeddings", vec![0.4, 0.5, 0.6]);
1914        assert!(segment.insert(entity2).is_err());
1915    }
1916
1917    #[test]
1918    fn test_should_seal() {
1919        let mut segment = GrowingSegment::new(1, "test");
1920
1921        let config = SegmentConfig {
1922            max_entities: 2,
1923            ..Default::default()
1924        };
1925
1926        assert!(!segment.should_seal(&config));
1927
1928        segment
1929            .insert(UnifiedEntity::vector(EntityId::new(1), "v", vec![0.1]))
1930            .unwrap();
1931        assert!(!segment.should_seal(&config));
1932
1933        segment
1934            .insert(UnifiedEntity::vector(EntityId::new(2), "v", vec![0.2]))
1935            .unwrap();
1936        assert!(segment.should_seal(&config));
1937    }
1938
1939    #[test]
1940    fn test_cross_references() {
1941        let mut segment = GrowingSegment::new(1, "test");
1942
1943        let mut entity1 = UnifiedEntity::table_row(
1944            EntityId::new(1),
1945            "hosts",
1946            1,
1947            vec![Value::text("192.168.1.1".to_string())],
1948        );
1949        entity1.add_cross_ref(CrossRef::new(
1950            EntityId::new(1),
1951            EntityId::new(2),
1952            "nodes",
1953            RefType::RowToNode,
1954        ));
1955        segment.insert(entity1).unwrap();
1956
1957        let refs_from = segment.get_references_from(EntityId::new(1));
1958        assert_eq!(refs_from.len(), 1);
1959        assert_eq!(refs_from[0], (EntityId::new(2), RefType::RowToNode));
1960
1961        let refs_to = segment.get_references_to(EntityId::new(2));
1962        assert_eq!(refs_to.len(), 1);
1963        assert_eq!(refs_to[0], (EntityId::new(1), RefType::RowToNode));
1964    }
1965
1966    #[test]
1967    fn test_zone_predicate_uses_canonical_fallback_for_email_values() {
1968        let mut zone = ColZone::new(Value::Email("bravo@example.com".to_string()));
1969        zone.update(&Value::Email("delta@example.com".to_string()));
1970
1971        let probe = Value::Email("alpha@example.com".to_string());
1972        assert!(ZoneColPred::Eq(&probe).can_skip(&zone));
1973
1974        let in_range = Value::Email("charlie@example.com".to_string());
1975        assert!(!ZoneColPred::Eq(&in_range).can_skip(&zone));
1976    }
1977
1978    #[test]
1979    fn test_sealed_multi_zone_prunes_numeric_gap_outlier() {
1980        let mut segment = GrowingSegment::new(1, "test");
1981
1982        for (row_id, age) in [(1_u64, 1_i64), (2, 2), (3, 3), (4, 1000)] {
1983            let entity = UnifiedEntity::new(
1984                EntityId::new(row_id),
1985                EntityKind::TableRow {
1986                    table: "users".into(),
1987                    row_id,
1988                },
1989                EntityData::Row(RowData::with_names(
1990                    vec![Value::Integer(age)],
1991                    vec!["age".to_string()],
1992                )),
1993            );
1994            segment.insert(entity).unwrap();
1995        }
1996
1997        segment.seal().unwrap();
1998
1999        let miss = Value::Integer(500);
2000        assert!(segment.can_skip_zone_preds(&[("age", ZoneColPred::Eq(&miss))]));
2001
2002        let hit = Value::Integer(1000);
2003        assert!(!segment.can_skip_zone_preds(&[("age", ZoneColPred::Eq(&hit))]));
2004    }
2005}