1use crate::index::zone_map::ZoneMapEntry;
24use crate::storage::{
25 CompressedData, CompressionCodec, DictionaryBuilder, DictionaryEncoding, TypeSpecificCompressor,
26};
27use grafeo_common::types::{EdgeId, NodeId, PropertyKey, Value};
28use grafeo_common::utils::hash::FxHashMap;
29use parking_lot::RwLock;
30use std::cmp::Ordering;
31use std::hash::Hash;
32use std::marker::PhantomData;
33use std::sync::Arc;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum CompressionMode {
38 #[default]
40 None,
41 Auto,
43 Eager,
45}
46
47const COMPRESSION_THRESHOLD: usize = 1000;
49
50const HOT_BUFFER_SIZE: usize = 4096;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CompareOp {
61 Eq,
63 Ne,
65 Lt,
67 Le,
69 Gt,
71 Ge,
73}
74
75pub trait EntityId: Copy + Eq + Hash + 'static {}
79
80impl EntityId for NodeId {}
81impl EntityId for EdgeId {}
82
83pub struct PropertyStorage<Id: EntityId = NodeId> {
108 columns: RwLock<FxHashMap<PropertyKey, PropertyColumn<Id>>>,
110 default_compression: CompressionMode,
112 _marker: PhantomData<Id>,
113}
114
115impl<Id: EntityId> PropertyStorage<Id> {
116 #[must_use]
118 pub fn new() -> Self {
119 Self {
120 columns: RwLock::new(FxHashMap::default()),
121 default_compression: CompressionMode::None,
122 _marker: PhantomData,
123 }
124 }
125
126 #[must_use]
128 pub fn with_compression(mode: CompressionMode) -> Self {
129 Self {
130 columns: RwLock::new(FxHashMap::default()),
131 default_compression: mode,
132 _marker: PhantomData,
133 }
134 }
135
136 pub fn set_default_compression(&mut self, mode: CompressionMode) {
138 self.default_compression = mode;
139 }
140
141 pub fn set(&self, id: Id, key: PropertyKey, value: Value) {
143 let mut columns = self.columns.write();
144 let mode = self.default_compression;
145 columns
146 .entry(key)
147 .or_insert_with(|| PropertyColumn::with_compression(mode))
148 .set(id, value);
149 }
150
151 pub fn enable_compression(&self, key: &PropertyKey, mode: CompressionMode) {
153 let mut columns = self.columns.write();
154 if let Some(col) = columns.get_mut(key) {
155 col.set_compression_mode(mode);
156 }
157 }
158
159 pub fn compress_all(&self) {
161 let mut columns = self.columns.write();
162 for col in columns.values_mut() {
163 if col.compression_mode() != CompressionMode::None {
164 col.compress();
165 }
166 }
167 }
168
169 pub fn force_compress_all(&self) {
171 let mut columns = self.columns.write();
172 for col in columns.values_mut() {
173 col.force_compress();
174 }
175 }
176
177 #[must_use]
179 pub fn compression_stats(&self) -> FxHashMap<PropertyKey, CompressionStats> {
180 let columns = self.columns.read();
181 columns
182 .iter()
183 .map(|(key, col)| (key.clone(), col.compression_stats()))
184 .collect()
185 }
186
187 #[must_use]
189 pub fn memory_usage(&self) -> usize {
190 let columns = self.columns.read();
191 columns
192 .values()
193 .map(|col| col.compression_stats().compressed_size)
194 .sum()
195 }
196
197 #[must_use]
199 pub fn get(&self, id: Id, key: &PropertyKey) -> Option<Value> {
200 let columns = self.columns.read();
201 columns.get(key).and_then(|col| col.get(id))
202 }
203
204 pub fn remove(&self, id: Id, key: &PropertyKey) -> Option<Value> {
206 let mut columns = self.columns.write();
207 columns.get_mut(key).and_then(|col| col.remove(id))
208 }
209
210 pub fn remove_all(&self, id: Id) {
212 let mut columns = self.columns.write();
213 for col in columns.values_mut() {
214 col.remove(id);
215 }
216 }
217
218 #[must_use]
220 pub fn get_all(&self, id: Id) -> FxHashMap<PropertyKey, Value> {
221 let columns = self.columns.read();
222 let mut result = FxHashMap::default();
223 for (key, col) in columns.iter() {
224 if let Some(value) = col.get(id) {
225 result.insert(key.clone(), value);
226 }
227 }
228 result
229 }
230
231 #[must_use]
233 pub fn column_count(&self) -> usize {
234 self.columns.read().len()
235 }
236
237 #[must_use]
239 pub fn keys(&self) -> Vec<PropertyKey> {
240 self.columns.read().keys().cloned().collect()
241 }
242
243 #[must_use]
245 pub fn column(&self, key: &PropertyKey) -> Option<PropertyColumnRef<'_, Id>> {
246 let columns = self.columns.read();
247 if columns.contains_key(key) {
248 Some(PropertyColumnRef {
249 _guard: columns,
250 key: key.clone(),
251 _marker: PhantomData,
252 })
253 } else {
254 None
255 }
256 }
257
258 #[must_use]
264 pub fn might_match(&self, key: &PropertyKey, op: CompareOp, value: &Value) -> bool {
265 let columns = self.columns.read();
266 columns
267 .get(key)
268 .map(|col| col.might_match(op, value))
269 .unwrap_or(true) }
271
272 #[must_use]
274 pub fn zone_map(&self, key: &PropertyKey) -> Option<ZoneMapEntry> {
275 let columns = self.columns.read();
276 columns.get(key).map(|col| col.zone_map().clone())
277 }
278
279 #[must_use]
284 pub fn might_match_range(
285 &self,
286 key: &PropertyKey,
287 min: Option<&Value>,
288 max: Option<&Value>,
289 min_inclusive: bool,
290 max_inclusive: bool,
291 ) -> bool {
292 let columns = self.columns.read();
293 columns
294 .get(key)
295 .map(|col| {
296 col.zone_map()
297 .might_contain_range(min, max, min_inclusive, max_inclusive)
298 })
299 .unwrap_or(true) }
301
302 pub fn rebuild_zone_maps(&self) {
304 let mut columns = self.columns.write();
305 for col in columns.values_mut() {
306 col.rebuild_zone_map();
307 }
308 }
309}
310
311impl<Id: EntityId> Default for PropertyStorage<Id> {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[derive(Debug)]
322pub enum CompressedColumnData {
323 Integers {
325 data: CompressedData,
327 id_to_index: Vec<u64>,
329 index_to_id: Vec<u64>,
331 },
332 Strings {
334 encoding: DictionaryEncoding,
336 id_to_index: Vec<u64>,
338 index_to_id: Vec<u64>,
340 },
341 Booleans {
343 data: CompressedData,
345 id_to_index: Vec<u64>,
347 index_to_id: Vec<u64>,
349 },
350}
351
352impl CompressedColumnData {
353 #[must_use]
355 pub fn memory_usage(&self) -> usize {
356 match self {
357 CompressedColumnData::Integers {
358 data,
359 id_to_index,
360 index_to_id,
361 } => {
362 data.data.len()
363 + id_to_index.len() * std::mem::size_of::<u64>()
364 + index_to_id.len() * std::mem::size_of::<u64>()
365 }
366 CompressedColumnData::Strings {
367 encoding,
368 id_to_index,
369 index_to_id,
370 } => {
371 encoding.codes().len() * std::mem::size_of::<u32>()
372 + encoding.dictionary().iter().map(|s| s.len()).sum::<usize>()
373 + id_to_index.len() * std::mem::size_of::<u64>()
374 + index_to_id.len() * std::mem::size_of::<u64>()
375 }
376 CompressedColumnData::Booleans {
377 data,
378 id_to_index,
379 index_to_id,
380 } => {
381 data.data.len()
382 + id_to_index.len() * std::mem::size_of::<u64>()
383 + index_to_id.len() * std::mem::size_of::<u64>()
384 }
385 }
386 }
387
388 #[must_use]
390 #[allow(dead_code)]
391 pub fn compression_ratio(&self) -> f64 {
392 match self {
393 CompressedColumnData::Integers { data, .. } => data.compression_ratio(),
394 CompressedColumnData::Strings { encoding, .. } => encoding.compression_ratio(),
395 CompressedColumnData::Booleans { data, .. } => data.compression_ratio(),
396 }
397 }
398}
399
400#[derive(Debug, Clone, Default)]
402pub struct CompressionStats {
403 pub uncompressed_size: usize,
405 pub compressed_size: usize,
407 pub value_count: usize,
409 pub codec: Option<CompressionCodec>,
411}
412
413impl CompressionStats {
414 #[must_use]
416 pub fn compression_ratio(&self) -> f64 {
417 if self.compressed_size == 0 {
418 return 1.0;
419 }
420 self.uncompressed_size as f64 / self.compressed_size as f64
421 }
422}
423
424pub struct PropertyColumn<Id: EntityId = NodeId> {
434 values: FxHashMap<Id, Value>,
437 zone_map: ZoneMapEntry,
439 zone_map_dirty: bool,
441 compression_mode: CompressionMode,
443 compressed: Option<CompressedColumnData>,
445 compressed_count: usize,
447}
448
449impl<Id: EntityId> PropertyColumn<Id> {
450 #[must_use]
452 pub fn new() -> Self {
453 Self {
454 values: FxHashMap::default(),
455 zone_map: ZoneMapEntry::new(),
456 zone_map_dirty: false,
457 compression_mode: CompressionMode::None,
458 compressed: None,
459 compressed_count: 0,
460 }
461 }
462
463 #[must_use]
465 pub fn with_compression(mode: CompressionMode) -> Self {
466 Self {
467 values: FxHashMap::default(),
468 zone_map: ZoneMapEntry::new(),
469 zone_map_dirty: false,
470 compression_mode: mode,
471 compressed: None,
472 compressed_count: 0,
473 }
474 }
475
476 pub fn set_compression_mode(&mut self, mode: CompressionMode) {
478 self.compression_mode = mode;
479 if mode == CompressionMode::None {
480 if self.compressed.is_some() {
482 self.decompress_all();
483 }
484 }
485 }
486
487 #[must_use]
489 pub fn compression_mode(&self) -> CompressionMode {
490 self.compression_mode
491 }
492
493 pub fn set(&mut self, id: Id, value: Value) {
495 self.update_zone_map_on_insert(&value);
497 self.values.insert(id, value);
498
499 if self.compression_mode == CompressionMode::Auto {
501 let total_count = self.values.len() + self.compressed_count;
502 let hot_buffer_count = self.values.len();
503
504 if hot_buffer_count >= HOT_BUFFER_SIZE && total_count >= COMPRESSION_THRESHOLD {
506 self.compress();
507 }
508 }
509 }
510
511 fn update_zone_map_on_insert(&mut self, value: &Value) {
513 self.zone_map.row_count += 1;
514
515 if matches!(value, Value::Null) {
516 self.zone_map.null_count += 1;
517 return;
518 }
519
520 match &self.zone_map.min {
522 None => self.zone_map.min = Some(value.clone()),
523 Some(current) => {
524 if compare_values(value, current) == Some(Ordering::Less) {
525 self.zone_map.min = Some(value.clone());
526 }
527 }
528 }
529
530 match &self.zone_map.max {
532 None => self.zone_map.max = Some(value.clone()),
533 Some(current) => {
534 if compare_values(value, current) == Some(Ordering::Greater) {
535 self.zone_map.max = Some(value.clone());
536 }
537 }
538 }
539 }
540
541 #[must_use]
546 pub fn get(&self, id: Id) -> Option<Value> {
547 if let Some(value) = self.values.get(&id) {
549 return Some(value.clone());
550 }
551
552 None
557 }
558
559 pub fn remove(&mut self, id: Id) -> Option<Value> {
561 let removed = self.values.remove(&id);
562 if removed.is_some() {
563 self.zone_map_dirty = true;
565 }
566 removed
567 }
568
569 #[must_use]
571 #[allow(dead_code)]
572 pub fn len(&self) -> usize {
573 self.values.len() + self.compressed_count
574 }
575
576 #[must_use]
578 #[allow(dead_code)]
579 pub fn is_empty(&self) -> bool {
580 self.values.is_empty() && self.compressed_count == 0
581 }
582
583 #[allow(dead_code)]
588 pub fn iter(&self) -> impl Iterator<Item = (Id, &Value)> {
589 self.values.iter().map(|(&id, v)| (id, v))
590 }
591
592 #[must_use]
594 pub fn compression_stats(&self) -> CompressionStats {
595 let hot_size = self.values.len() * std::mem::size_of::<Value>();
596 let compressed_size = self.compressed.as_ref().map_or(0, |c| c.memory_usage());
597 let codec = match &self.compressed {
598 Some(CompressedColumnData::Integers { data, .. }) => Some(data.codec),
599 Some(CompressedColumnData::Strings { .. }) => Some(CompressionCodec::Dictionary),
600 Some(CompressedColumnData::Booleans { data, .. }) => Some(data.codec),
601 None => None,
602 };
603
604 CompressionStats {
605 uncompressed_size: hot_size + self.compressed_count * std::mem::size_of::<Value>(),
606 compressed_size: hot_size + compressed_size,
607 value_count: self.len(),
608 codec,
609 }
610 }
611
612 #[must_use]
614 #[allow(dead_code)]
615 pub fn is_compressed(&self) -> bool {
616 self.compressed.is_some()
617 }
618
619 pub fn compress(&mut self) {
628 if self.values.is_empty() {
629 return;
630 }
631
632 if self.compressed.is_some() {
635 return;
636 }
637
638 let (int_count, str_count, bool_count) = self.count_types();
640 let total = self.values.len();
641
642 if int_count > total / 2 {
643 self.compress_as_integers();
644 } else if str_count > total / 2 {
645 self.compress_as_strings();
646 } else if bool_count > total / 2 {
647 self.compress_as_booleans();
648 }
649 }
651
652 fn count_types(&self) -> (usize, usize, usize) {
654 let mut int_count = 0;
655 let mut str_count = 0;
656 let mut bool_count = 0;
657
658 for value in self.values.values() {
659 match value {
660 Value::Int64(_) => int_count += 1,
661 Value::String(_) => str_count += 1,
662 Value::Bool(_) => bool_count += 1,
663 _ => {}
664 }
665 }
666
667 (int_count, str_count, bool_count)
668 }
669
670 #[allow(unsafe_code)]
672 fn compress_as_integers(&mut self) {
673 let mut values: Vec<(u64, i64)> = Vec::new();
675 let mut non_int_values: FxHashMap<Id, Value> = FxHashMap::default();
676
677 for (&id, value) in &self.values {
678 match value {
679 Value::Int64(v) => {
680 let id_u64 = unsafe { std::mem::transmute_copy::<Id, u64>(&id) };
682 values.push((id_u64, *v));
683 }
684 _ => {
685 non_int_values.insert(id, value.clone());
686 }
687 }
688 }
689
690 if values.len() < 8 {
691 return;
693 }
694
695 values.sort_by_key(|(id, _)| *id);
697
698 let id_to_index: Vec<u64> = values.iter().map(|(id, _)| *id).collect();
699 let index_to_id: Vec<u64> = id_to_index.clone();
700 let int_values: Vec<i64> = values.iter().map(|(_, v)| *v).collect();
701
702 let compressed = TypeSpecificCompressor::compress_signed_integers(&int_values);
704
705 if compressed.compression_ratio() > 1.2 {
707 self.compressed = Some(CompressedColumnData::Integers {
708 data: compressed,
709 id_to_index,
710 index_to_id,
711 });
712 self.compressed_count = values.len();
713 self.values = non_int_values;
714 }
715 }
716
717 #[allow(unsafe_code)]
719 fn compress_as_strings(&mut self) {
720 let mut values: Vec<(u64, Arc<str>)> = Vec::new();
721 let mut non_str_values: FxHashMap<Id, Value> = FxHashMap::default();
722
723 for (&id, value) in &self.values {
724 match value {
725 Value::String(s) => {
726 let id_u64 = unsafe { std::mem::transmute_copy::<Id, u64>(&id) };
727 values.push((id_u64, Arc::clone(s)));
728 }
729 _ => {
730 non_str_values.insert(id, value.clone());
731 }
732 }
733 }
734
735 if values.len() < 8 {
736 return;
737 }
738
739 values.sort_by_key(|(id, _)| *id);
741
742 let id_to_index: Vec<u64> = values.iter().map(|(id, _)| *id).collect();
743 let index_to_id: Vec<u64> = id_to_index.clone();
744
745 let mut builder = DictionaryBuilder::new();
747 for (_, s) in &values {
748 builder.add(s.as_ref());
749 }
750 let encoding = builder.build();
751
752 if encoding.compression_ratio() > 1.2 {
754 self.compressed = Some(CompressedColumnData::Strings {
755 encoding,
756 id_to_index,
757 index_to_id,
758 });
759 self.compressed_count = values.len();
760 self.values = non_str_values;
761 }
762 }
763
764 #[allow(unsafe_code)]
766 fn compress_as_booleans(&mut self) {
767 let mut values: Vec<(u64, bool)> = Vec::new();
768 let mut non_bool_values: FxHashMap<Id, Value> = FxHashMap::default();
769
770 for (&id, value) in &self.values {
771 match value {
772 Value::Bool(b) => {
773 let id_u64 = unsafe { std::mem::transmute_copy::<Id, u64>(&id) };
774 values.push((id_u64, *b));
775 }
776 _ => {
777 non_bool_values.insert(id, value.clone());
778 }
779 }
780 }
781
782 if values.len() < 8 {
783 return;
784 }
785
786 values.sort_by_key(|(id, _)| *id);
788
789 let id_to_index: Vec<u64> = values.iter().map(|(id, _)| *id).collect();
790 let index_to_id: Vec<u64> = id_to_index.clone();
791 let bool_values: Vec<bool> = values.iter().map(|(_, v)| *v).collect();
792
793 let compressed = TypeSpecificCompressor::compress_booleans(&bool_values);
794
795 self.compressed = Some(CompressedColumnData::Booleans {
797 data: compressed,
798 id_to_index,
799 index_to_id,
800 });
801 self.compressed_count = values.len();
802 self.values = non_bool_values;
803 }
804
805 #[allow(unsafe_code)]
807 fn decompress_all(&mut self) {
808 let Some(compressed) = self.compressed.take() else {
809 return;
810 };
811
812 match compressed {
813 CompressedColumnData::Integers {
814 data, index_to_id, ..
815 } => {
816 if let Ok(values) = TypeSpecificCompressor::decompress_integers(&data) {
817 let signed: Vec<i64> = values
819 .iter()
820 .map(|&v| crate::storage::zigzag_decode(v))
821 .collect();
822
823 for (i, id_u64) in index_to_id.iter().enumerate() {
824 if let Some(&value) = signed.get(i) {
825 let id: Id = unsafe { std::mem::transmute_copy(id_u64) };
826 self.values.insert(id, Value::Int64(value));
827 }
828 }
829 }
830 }
831 CompressedColumnData::Strings {
832 encoding,
833 index_to_id,
834 ..
835 } => {
836 for (i, id_u64) in index_to_id.iter().enumerate() {
837 if let Some(s) = encoding.get(i) {
838 let id: Id = unsafe { std::mem::transmute_copy(id_u64) };
839 self.values.insert(id, Value::String(Arc::from(s)));
840 }
841 }
842 }
843 CompressedColumnData::Booleans {
844 data, index_to_id, ..
845 } => {
846 if let Ok(values) = TypeSpecificCompressor::decompress_booleans(&data) {
847 for (i, id_u64) in index_to_id.iter().enumerate() {
848 if let Some(&value) = values.get(i) {
849 let id: Id = unsafe { std::mem::transmute_copy(id_u64) };
850 self.values.insert(id, Value::Bool(value));
851 }
852 }
853 }
854 }
855 }
856
857 self.compressed_count = 0;
858 }
859
860 pub fn force_compress(&mut self) {
864 self.compress();
865 }
866
867 #[must_use]
869 pub fn zone_map(&self) -> &ZoneMapEntry {
870 &self.zone_map
871 }
872
873 #[must_use]
878 pub fn might_match(&self, op: CompareOp, value: &Value) -> bool {
879 if self.zone_map_dirty {
880 return true;
882 }
883
884 match op {
885 CompareOp::Eq => self.zone_map.might_contain_equal(value),
886 CompareOp::Ne => {
887 match (&self.zone_map.min, &self.zone_map.max) {
890 (Some(min), Some(max)) => {
891 !(compare_values(min, value) == Some(Ordering::Equal)
892 && compare_values(max, value) == Some(Ordering::Equal))
893 }
894 _ => true,
895 }
896 }
897 CompareOp::Lt => self.zone_map.might_contain_less_than(value, false),
898 CompareOp::Le => self.zone_map.might_contain_less_than(value, true),
899 CompareOp::Gt => self.zone_map.might_contain_greater_than(value, false),
900 CompareOp::Ge => self.zone_map.might_contain_greater_than(value, true),
901 }
902 }
903
904 pub fn rebuild_zone_map(&mut self) {
906 let mut zone_map = ZoneMapEntry::new();
907
908 for value in self.values.values() {
909 zone_map.row_count += 1;
910
911 if matches!(value, Value::Null) {
912 zone_map.null_count += 1;
913 continue;
914 }
915
916 match &zone_map.min {
918 None => zone_map.min = Some(value.clone()),
919 Some(current) => {
920 if compare_values(value, current) == Some(Ordering::Less) {
921 zone_map.min = Some(value.clone());
922 }
923 }
924 }
925
926 match &zone_map.max {
928 None => zone_map.max = Some(value.clone()),
929 Some(current) => {
930 if compare_values(value, current) == Some(Ordering::Greater) {
931 zone_map.max = Some(value.clone());
932 }
933 }
934 }
935 }
936
937 self.zone_map = zone_map;
938 self.zone_map_dirty = false;
939 }
940}
941
942fn compare_values(a: &Value, b: &Value) -> Option<Ordering> {
944 match (a, b) {
945 (Value::Int64(a), Value::Int64(b)) => Some(a.cmp(b)),
946 (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b),
947 (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
948 (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
949 (Value::Int64(a), Value::Float64(b)) => (*a as f64).partial_cmp(b),
950 (Value::Float64(a), Value::Int64(b)) => a.partial_cmp(&(*b as f64)),
951 _ => None,
952 }
953}
954
955impl<Id: EntityId> Default for PropertyColumn<Id> {
956 fn default() -> Self {
957 Self::new()
958 }
959}
960
961pub struct PropertyColumnRef<'a, Id: EntityId = NodeId> {
965 _guard: parking_lot::RwLockReadGuard<'a, FxHashMap<PropertyKey, PropertyColumn<Id>>>,
966 #[allow(dead_code)]
967 key: PropertyKey,
968 _marker: PhantomData<Id>,
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[test]
976 fn test_property_storage_basic() {
977 let storage = PropertyStorage::new();
978
979 let node1 = NodeId::new(1);
980 let node2 = NodeId::new(2);
981 let name_key = PropertyKey::new("name");
982 let age_key = PropertyKey::new("age");
983
984 storage.set(node1, name_key.clone(), "Alice".into());
985 storage.set(node1, age_key.clone(), 30i64.into());
986 storage.set(node2, name_key.clone(), "Bob".into());
987
988 assert_eq!(
989 storage.get(node1, &name_key),
990 Some(Value::String("Alice".into()))
991 );
992 assert_eq!(storage.get(node1, &age_key), Some(Value::Int64(30)));
993 assert_eq!(
994 storage.get(node2, &name_key),
995 Some(Value::String("Bob".into()))
996 );
997 assert!(storage.get(node2, &age_key).is_none());
998 }
999
1000 #[test]
1001 fn test_property_storage_remove() {
1002 let storage = PropertyStorage::new();
1003
1004 let node = NodeId::new(1);
1005 let key = PropertyKey::new("name");
1006
1007 storage.set(node, key.clone(), "Alice".into());
1008 assert!(storage.get(node, &key).is_some());
1009
1010 let removed = storage.remove(node, &key);
1011 assert!(removed.is_some());
1012 assert!(storage.get(node, &key).is_none());
1013 }
1014
1015 #[test]
1016 fn test_property_storage_get_all() {
1017 let storage = PropertyStorage::new();
1018
1019 let node = NodeId::new(1);
1020 storage.set(node, PropertyKey::new("name"), "Alice".into());
1021 storage.set(node, PropertyKey::new("age"), 30i64.into());
1022 storage.set(node, PropertyKey::new("active"), true.into());
1023
1024 let props = storage.get_all(node);
1025 assert_eq!(props.len(), 3);
1026 }
1027
1028 #[test]
1029 fn test_property_storage_remove_all() {
1030 let storage = PropertyStorage::new();
1031
1032 let node = NodeId::new(1);
1033 storage.set(node, PropertyKey::new("name"), "Alice".into());
1034 storage.set(node, PropertyKey::new("age"), 30i64.into());
1035
1036 storage.remove_all(node);
1037
1038 assert!(storage.get(node, &PropertyKey::new("name")).is_none());
1039 assert!(storage.get(node, &PropertyKey::new("age")).is_none());
1040 }
1041
1042 #[test]
1043 fn test_property_column() {
1044 let mut col = PropertyColumn::new();
1045
1046 col.set(NodeId::new(1), "Alice".into());
1047 col.set(NodeId::new(2), "Bob".into());
1048
1049 assert_eq!(col.len(), 2);
1050 assert!(!col.is_empty());
1051
1052 assert_eq!(col.get(NodeId::new(1)), Some(Value::String("Alice".into())));
1053
1054 col.remove(NodeId::new(1));
1055 assert!(col.get(NodeId::new(1)).is_none());
1056 assert_eq!(col.len(), 1);
1057 }
1058
1059 #[test]
1060 fn test_compression_mode() {
1061 let col: PropertyColumn<NodeId> = PropertyColumn::new();
1062 assert_eq!(col.compression_mode(), CompressionMode::None);
1063
1064 let col: PropertyColumn<NodeId> = PropertyColumn::with_compression(CompressionMode::Auto);
1065 assert_eq!(col.compression_mode(), CompressionMode::Auto);
1066 }
1067
1068 #[test]
1069 fn test_property_storage_with_compression() {
1070 let storage = PropertyStorage::with_compression(CompressionMode::Auto);
1071
1072 for i in 0..100 {
1073 storage.set(
1074 NodeId::new(i),
1075 PropertyKey::new("age"),
1076 Value::Int64(20 + (i as i64 % 50)),
1077 );
1078 }
1079
1080 assert_eq!(
1082 storage.get(NodeId::new(0), &PropertyKey::new("age")),
1083 Some(Value::Int64(20))
1084 );
1085 assert_eq!(
1086 storage.get(NodeId::new(50), &PropertyKey::new("age")),
1087 Some(Value::Int64(20))
1088 );
1089 }
1090
1091 #[test]
1092 fn test_compress_integer_column() {
1093 let mut col: PropertyColumn<NodeId> =
1094 PropertyColumn::with_compression(CompressionMode::Auto);
1095
1096 for i in 0..2000 {
1098 col.set(NodeId::new(i), Value::Int64(1000 + i as i64));
1099 }
1100
1101 let stats = col.compression_stats();
1104 assert_eq!(stats.value_count, 2000);
1105
1106 let last_value = col.get(NodeId::new(1999));
1109 assert!(last_value.is_some() || col.is_compressed());
1110 }
1111
1112 #[test]
1113 fn test_compress_string_column() {
1114 let mut col: PropertyColumn<NodeId> =
1115 PropertyColumn::with_compression(CompressionMode::Auto);
1116
1117 let categories = ["Person", "Company", "Product", "Location"];
1119 for i in 0..2000 {
1120 let cat = categories[i % 4];
1121 col.set(NodeId::new(i as u64), Value::String(Arc::from(cat)));
1122 }
1123
1124 assert_eq!(col.len(), 2000);
1126
1127 let last_value = col.get(NodeId::new(1999));
1129 assert!(last_value.is_some() || col.is_compressed());
1130 }
1131
1132 #[test]
1133 fn test_compress_boolean_column() {
1134 let mut col: PropertyColumn<NodeId> =
1135 PropertyColumn::with_compression(CompressionMode::Auto);
1136
1137 for i in 0..2000 {
1139 col.set(NodeId::new(i as u64), Value::Bool(i % 2 == 0));
1140 }
1141
1142 assert_eq!(col.len(), 2000);
1144
1145 let last_value = col.get(NodeId::new(1999));
1147 assert!(last_value.is_some() || col.is_compressed());
1148 }
1149
1150 #[test]
1151 fn test_force_compress() {
1152 let mut col: PropertyColumn<NodeId> = PropertyColumn::new();
1153
1154 for i in 0..100 {
1156 col.set(NodeId::new(i), Value::Int64(i as i64));
1157 }
1158
1159 col.force_compress();
1161
1162 let stats = col.compression_stats();
1164 assert_eq!(stats.value_count, 100);
1165 }
1166
1167 #[test]
1168 fn test_compression_stats() {
1169 let mut col: PropertyColumn<NodeId> = PropertyColumn::new();
1170
1171 for i in 0..50 {
1172 col.set(NodeId::new(i), Value::Int64(i as i64));
1173 }
1174
1175 let stats = col.compression_stats();
1176 assert_eq!(stats.value_count, 50);
1177 assert!(stats.uncompressed_size > 0);
1178 }
1179
1180 #[test]
1181 fn test_storage_compression_stats() {
1182 let storage = PropertyStorage::with_compression(CompressionMode::Auto);
1183
1184 for i in 0..100 {
1185 storage.set(
1186 NodeId::new(i),
1187 PropertyKey::new("age"),
1188 Value::Int64(i as i64),
1189 );
1190 storage.set(
1191 NodeId::new(i),
1192 PropertyKey::new("name"),
1193 Value::String(Arc::from("Alice")),
1194 );
1195 }
1196
1197 let stats = storage.compression_stats();
1198 assert_eq!(stats.len(), 2); assert!(stats.contains_key(&PropertyKey::new("age")));
1200 assert!(stats.contains_key(&PropertyKey::new("name")));
1201 }
1202
1203 #[test]
1204 fn test_memory_usage() {
1205 let storage = PropertyStorage::new();
1206
1207 for i in 0..100 {
1208 storage.set(
1209 NodeId::new(i),
1210 PropertyKey::new("value"),
1211 Value::Int64(i as i64),
1212 );
1213 }
1214
1215 let usage = storage.memory_usage();
1216 assert!(usage > 0);
1217 }
1218}