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