1use 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
41pub type SegmentId = u64;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum SegmentState {
47 Growing,
49 Sealing,
51 Sealed,
53 Flushed,
55 Archived,
57}
58
59impl SegmentState {
60 pub fn is_writable(&self) -> bool {
62 matches!(self, Self::Growing)
63 }
64
65 pub fn is_queryable(&self) -> bool {
67 !matches!(self, Self::Sealing)
68 }
69
70 pub fn is_immutable(&self) -> bool {
72 matches!(self, Self::Sealed | Self::Flushed | Self::Archived)
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct SegmentConfig {
79 pub max_entities: usize,
81 pub max_bytes: usize,
83 pub max_age_secs: u64,
85 pub build_vector_index: bool,
87 pub build_graph_index: bool,
89 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, max_age_secs: 3600, build_vector_index: true,
100 build_graph_index: true,
101 compression_level: 6,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default)]
108pub struct SegmentStats {
109 pub entity_count: usize,
111 pub deleted_count: usize,
113 pub memory_bytes: usize,
115 pub vector_count: usize,
117 pub node_count: usize,
119 pub edge_count: usize,
121 pub row_count: usize,
123 pub cross_ref_count: usize,
125}
126
127#[derive(Debug, Clone)]
129pub enum SegmentError {
130 NotWritable,
132 NotFound(EntityId),
134 AlreadyExists(EntityId),
136 Full,
138 InvalidState(SegmentState),
140 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
199pub trait UnifiedSegment: Send + Sync {
201 fn id(&self) -> SegmentId;
203
204 fn state(&self) -> SegmentState;
206
207 fn collection(&self) -> &str;
209
210 fn stats(&self) -> SegmentStats;
212
213 fn entity_count(&self) -> usize;
215
216 fn contains(&self, id: EntityId) -> bool;
218
219 fn get(&self, id: EntityId) -> Option<&UnifiedEntity>;
221
222 fn get_mut(&mut self, id: EntityId) -> Option<&mut UnifiedEntity>;
224
225 fn insert(&mut self, entity: UnifiedEntity) -> Result<EntityId, SegmentError>;
227
228 fn update(&mut self, entity: UnifiedEntity) -> Result<(), SegmentError>;
230
231 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 fn delete(&mut self, id: EntityId) -> Result<bool, SegmentError>;
245
246 fn get_metadata(&self, id: EntityId) -> Option<Metadata>;
248
249 fn set_metadata(&mut self, id: EntityId, metadata: Metadata) -> Result<(), SegmentError>;
251
252 fn seal(&mut self) -> Result<(), SegmentError>;
254
255 fn should_seal(&self, config: &SegmentConfig) -> bool;
257
258 fn iter(&self) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
260
261 fn iter_kind(&self, kind_filter: &str) -> Box<dyn Iterator<Item = &UnifiedEntity> + '_>;
263
264 fn filter_metadata(
266 &self,
267 filters: &[(String, super::metadata::MetadataFilter)],
268 ) -> Vec<EntityId>;
269}
270
271#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum ZoneColPredKind {
351 Eq,
352 Gt,
353 Gte,
354 Lt,
355 Lte,
356}
357
358#[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 pub fn can_skip(&self, zone: &ColZone) -> bool {
371 match self {
372 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 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 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 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 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
409pub struct GrowingSegment {
411 id: SegmentId,
413 collection: String,
415 state: SegmentState,
417 created_at: u64,
419 last_write_at: u64,
421
422 entities: HashMap<EntityId, UnifiedEntity>,
424 flat_entities: Vec<UnifiedEntity>,
427 base_entity_id: u64,
429 use_flat: bool,
431 deleted: HashSet<EntityId>,
433 metadata: MetadataStorage,
435
436 pk_index: BTreeMap<(String, String), EntityId>,
438 kind_index: HashMap<String, HashSet<EntityId>>,
440 cross_ref_forward: HashMap<EntityId, Vec<(EntityId, RefType)>>,
442 cross_ref_reverse: HashMap<EntityId, Vec<(EntityId, RefType)>>,
444
445 bloom: BloomFilter,
447
448 col_zones: HashMap<String, ColZone>,
450 sealed_col_zones: HashMap<String, MultiColZone>,
452
453 sequence: AtomicU64,
455 memory_bytes: AtomicU64,
457
458 pub(crate) published_flat_len: AtomicUsize,
465}
466
467impl GrowingSegment {
468 #[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 if self.deleted.is_empty() {
478 for entity in &self.flat_entities {
479 if !f(entity) {
480 return false;
481 }
482 }
483 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 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 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 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 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 fn add_memory(&self, bytes: usize) {
730 self.memory_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
731 }
732
733 fn estimate_entity_size(entity: &UnifiedEntity) -> usize {
735 let mut size = std::mem::size_of::<UnifiedEntity>();
736
737 size += match &entity.data {
739 EntityData::Row(row) => row.columns.len() * 64, 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 for emb in entity.embeddings() {
751 size += emb.vector.len() * 4 + emb.name.len() + emb.model.len();
752 }
753
754 size += std::mem::size_of_val(entity.cross_refs());
756
757 size
758 }
759
760 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 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 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 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; }
886 }
887 }
888 false
889 }
890
891 fn index_entity(&mut self, entity: &UnifiedEntity) {
893 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 let id_bytes = entity.id.raw().to_le_bytes();
902 self.bloom.insert(&id_bytes);
903
904 if let EntityData::Row(row) = &entity.data {
906 if let Some(first_col) = row.columns.first() {
907 let pk_str = format!("{:?}", first_col);
908 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 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 pub fn bloom_might_contain_key(&self, key: &[u8]) -> bool {
931 self.bloom.contains(key)
932 }
933
934 pub fn bloom_stats(&self) -> (f64, u32) {
936 (self.bloom.fill_ratio(), self.bloom.count_set_bits())
937 }
938
939 fn unindex_entity(&mut self, entity: &UnifiedEntity) {
941 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 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 self.cross_ref_forward.remove(&entity.id);
958 }
960
961 fn reindex_for_update(
973 &mut self,
974 old: &UpdateIndexSnapshot,
975 new: &UnifiedEntity,
976 modified_columns: Option<&[String]>,
977 ) {
978 let pk_changed = match &new.data {
985 EntityData::Row(new_row) => {
986 if let Some(cols) = modified_columns {
987 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 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 _ => false,
1006 };
1007
1008 if pk_changed {
1009 if let Some((collection, pk_str)) = &old.pk_index_key {
1011 self.pk_index.remove(&(collection.clone(), pk_str.clone()));
1012 }
1013 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 let new_refs = new.cross_refs();
1026 if old.cross_refs.as_slice() != new_refs {
1027 self.cross_ref_forward.remove(&new.id);
1029 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 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 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 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 pub fn age_secs(&self) -> u64 {
1061 let now = current_unix_secs();
1062 now.saturating_sub(self.created_at)
1063 }
1064
1065 pub fn idle_secs(&self) -> u64 {
1067 let now = current_unix_secs();
1068 now.saturating_sub(self.last_write_at)
1069 }
1070
1071 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 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 if self.flat_entities.is_empty() && self.entities.is_empty() {
1107 self.base_entity_id = entities.first().map(|e| e.id.raw()).unwrap_or(0);
1109 self.use_flat = true;
1110 }
1111
1112 let mut columnar_zone_updates: Vec<Vec<Value>> = Vec::new();
1122 let mut columnar_schema: Option<std::sync::Arc<Vec<String>>> = None;
1123 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 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 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 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 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 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 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 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 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 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 entity.sequence_id = self.next_sequence();
1394
1395 let size = Self::estimate_entity_size(&entity);
1397 self.add_memory(size);
1398
1399 self.index_entity(&entity);
1401
1402 self.update_col_zones_from_entity(&entity);
1404
1405 let id = entity.id;
1407 self.entities.insert(id, entity);
1408
1409 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 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 let entity = self.entities.remove(&id);
1467 if entity.is_none() {
1468 return Ok(false);
1469 }
1470
1471 if let Some(ref e) = entity {
1473 self.unindex_entity(e);
1474 }
1475
1476 self.metadata.remove_all(id);
1478
1479 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 self.rebuild_sealed_col_zones();
1518
1519 self.state = SegmentState::Sealed;
1520 Ok(())
1521 }
1522
1523 fn should_seal(&self, config: &SegmentConfig) -> bool {
1524 if self.entities.len() >= config.max_entities {
1526 return true;
1527 }
1528
1529 if self.memory_bytes.load(Ordering::Relaxed) as usize >= config.max_bytes {
1531 return true;
1532 }
1533
1534 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 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 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 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 assert!(segment.state().is_writable());
1765
1766 segment.seal().unwrap();
1768 assert_eq!(segment.state(), SegmentState::Sealed);
1769
1770 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}