1use std::collections::HashMap;
7use std::sync::Arc;
8
9use reddb_types::Value;
10
11pub const FIRST_USER_ENTITY_ID: u64 = 1024;
23
24pub use super::entity_id::EntityId;
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum EntityKind {
29 TableRow { table: Arc<str>, row_id: u64 },
31 GraphNode(Box<GraphNodeKind>),
33 GraphEdge(Box<GraphEdgeKind>),
35 Vector { collection: String },
37 TimeSeriesPoint(Box<TimeSeriesPointKind>),
39 QueueMessage { queue: String, position: u64 },
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct GraphNodeKind {
45 pub label: String,
46 pub node_type: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub struct GraphEdgeKind {
51 pub label: String,
52 pub from_node: String,
53 pub to_node: String,
54 pub weight: u32,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct TimeSeriesPointKind {
59 pub series: String,
60 pub metric: String,
61}
62
63impl EntityKind {
64 pub fn storage_type(&self) -> &'static str {
66 match self {
67 Self::TableRow { .. } => "table",
68 Self::GraphNode(_) => "graph_node",
69 Self::GraphEdge(_) => "graph_edge",
70 Self::Vector { .. } => "vector",
71 Self::TimeSeriesPoint(_) => "timeseries",
72 Self::QueueMessage { .. } => "queue",
73 }
74 }
75
76 pub fn collection(&self) -> &str {
78 match self {
79 Self::TableRow { table, .. } => table,
80 Self::GraphNode(n) => &n.label,
81 Self::GraphEdge(e) => &e.label,
82 Self::Vector { collection } => collection,
83 Self::TimeSeriesPoint(ts) => &ts.series,
84 Self::QueueMessage { queue, .. } => queue,
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub enum EntityData {
92 Row(RowData),
94 Node(NodeData),
96 Edge(EdgeData),
98 Vector(VectorData),
100 TimeSeries(TimeSeriesData),
102 QueueMessage(QueueMessageData),
104}
105
106impl EntityData {
107 pub fn is_row(&self) -> bool {
109 matches!(self, Self::Row(_))
110 }
111
112 pub fn is_node(&self) -> bool {
114 matches!(self, Self::Node(_))
115 }
116
117 pub fn is_edge(&self) -> bool {
119 matches!(self, Self::Edge(_))
120 }
121
122 pub fn is_vector(&self) -> bool {
124 matches!(self, Self::Vector(_))
125 }
126
127 pub fn as_row(&self) -> Option<&RowData> {
129 match self {
130 Self::Row(r) => Some(r),
131 _ => None,
132 }
133 }
134
135 pub fn as_node(&self) -> Option<&NodeData> {
137 match self {
138 Self::Node(n) => Some(n),
139 _ => None,
140 }
141 }
142
143 pub fn as_edge(&self) -> Option<&EdgeData> {
145 match self {
146 Self::Edge(e) => Some(e),
147 _ => None,
148 }
149 }
150
151 pub fn as_vector(&self) -> Option<&VectorData> {
153 match self {
154 Self::Vector(v) => Some(v),
155 _ => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct RowData {
163 pub columns: Vec<Value>,
165 pub named: Option<HashMap<String, Value>>,
167 pub schema: Option<std::sync::Arc<Vec<String>>>,
171}
172
173impl RowData {
174 pub fn new(columns: Vec<Value>) -> Self {
176 Self {
177 columns,
178 named: None,
179 schema: None,
180 }
181 }
182
183 pub fn with_names(columns: Vec<Value>, names: Vec<String>) -> Self {
185 let named: HashMap<String, Value> =
186 names.into_iter().zip(columns.iter().cloned()).collect();
187 Self {
188 columns,
189 named: Some(named),
190 schema: None,
191 }
192 }
193
194 pub(crate) fn compact_with_schema(&mut self, schema: &std::sync::Arc<Vec<String>>) -> bool {
197 let Some(named) = &self.named else {
198 return false;
199 };
200 if named.len() != schema.len() || schema.iter().any(|name| !named.contains_key(name)) {
201 return false;
202 }
203 if !self.columns.is_empty()
206 && (self.columns.len() != schema.len()
207 || schema
208 .iter()
209 .zip(&self.columns)
210 .any(|(name, value)| named.get(name) != Some(value)))
211 {
212 return false;
213 }
214 let mut named = self.named.take().expect("validated named row");
215 self.columns = schema
216 .iter()
217 .map(|name| named.remove(name).expect("validated schema field"))
218 .collect();
219 self.schema = Some(std::sync::Arc::clone(schema));
220 true
221 }
222
223 pub fn get_field(&self, name: &str) -> Option<&Value> {
225 if let Some(ref named) = self.named {
227 return named.get(name);
228 }
229 if let Some(ref schema) = self.schema {
231 if let Some(idx) = schema.iter().position(|s| s == name) {
232 return self.columns.get(idx);
233 }
234 }
235 None
236 }
237
238 pub fn iter_fields(&self) -> Box<dyn Iterator<Item = (&str, &Value)> + '_> {
240 if let Some(ref named) = self.named {
241 Box::new(named.iter().map(|(k, v)| (k.as_str(), v)))
242 } else if let Some(ref schema) = self.schema {
243 Box::new(
244 schema
245 .iter()
246 .zip(self.columns.iter())
247 .map(|(k, v)| (k.as_str(), v)),
248 )
249 } else {
250 Box::new(std::iter::empty())
251 }
252 }
253
254 pub fn get(&self, index: usize) -> Option<&Value> {
256 self.columns.get(index)
257 }
258
259 pub fn get_by_name(&self, name: &str) -> Option<&Value> {
261 self.named.as_ref()?.get(name)
262 }
263
264 pub fn len(&self) -> usize {
266 self.columns.len()
267 }
268
269 pub fn is_empty(&self) -> bool {
271 self.columns.is_empty()
272 }
273}
274
275#[derive(Debug, Clone)]
277pub struct NodeData {
278 pub properties: HashMap<String, Value>,
280}
281
282impl NodeData {
283 pub fn new() -> Self {
285 Self {
286 properties: HashMap::new(),
287 }
288 }
289
290 pub fn with_properties(properties: HashMap<String, Value>) -> Self {
292 Self { properties }
293 }
294
295 pub fn set(&mut self, key: impl Into<String>, value: Value) {
297 self.properties.insert(key.into(), value);
298 }
299
300 pub fn get(&self, key: &str) -> Option<&Value> {
302 self.properties.get(key)
303 }
304
305 pub fn has(&self, key: &str) -> bool {
307 self.properties.contains_key(key)
308 }
309}
310
311impl Default for NodeData {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct EdgeData {
320 pub properties: HashMap<String, Value>,
322 pub weight: f32,
324}
325
326impl EdgeData {
327 pub fn new(weight: f32) -> Self {
329 Self {
330 properties: HashMap::new(),
331 weight,
332 }
333 }
334
335 pub fn with_properties(weight: f32, properties: HashMap<String, Value>) -> Self {
337 Self { properties, weight }
338 }
339
340 pub fn set(&mut self, key: impl Into<String>, value: Value) {
342 self.properties.insert(key.into(), value);
343 }
344
345 pub fn get(&self, key: &str) -> Option<&Value> {
347 self.properties.get(key)
348 }
349}
350
351impl Default for EdgeData {
352 fn default() -> Self {
353 Self::new(1.0)
354 }
355}
356
357#[derive(Debug, Clone)]
359pub struct VectorData {
360 pub dense: Vec<f32>,
362 pub sparse: Option<SparseVector>,
364 pub content: Option<String>,
366}
367
368impl VectorData {
369 pub fn new(dense: Vec<f32>) -> Self {
371 Self {
372 dense,
373 sparse: None,
374 content: None,
375 }
376 }
377
378 pub fn with_sparse(dense: Vec<f32>, sparse: SparseVector) -> Self {
380 Self {
381 dense,
382 sparse: Some(sparse),
383 content: None,
384 }
385 }
386
387 pub fn with_content(mut self, content: impl Into<String>) -> Self {
389 self.content = Some(content.into());
390 self
391 }
392
393 pub fn dimension(&self) -> usize {
395 self.dense.len()
396 }
397
398 pub fn is_hybrid(&self) -> bool {
400 self.sparse.is_some()
401 }
402}
403
404#[derive(Debug, Clone)]
406pub struct TimeSeriesData {
407 pub metric: String,
409 pub series_id: Option<u64>,
411 pub timestamp_ns: u64,
413 pub value: f64,
415 pub tags: std::collections::HashMap<String, String>,
417 pub fields: std::collections::HashMap<String, Value>,
419}
420
421#[derive(Debug, Clone)]
423pub struct QueueMessageData {
424 pub payload: Value,
426 pub priority: Option<i32>,
428 pub enqueued_at_ns: u64,
430 pub attempts: u32,
432 pub max_attempts: u32,
434 pub acked: bool,
436}
437
438#[derive(Debug, Clone)]
440pub struct SparseVector {
441 pub indices: Vec<u32>,
443 pub values: Vec<f32>,
445 pub dimension: usize,
447}
448
449impl SparseVector {
450 pub fn new(indices: Vec<u32>, values: Vec<f32>, dimension: usize) -> Self {
452 debug_assert_eq!(indices.len(), values.len());
453 Self {
454 indices,
455 values,
456 dimension,
457 }
458 }
459
460 pub fn nnz(&self) -> usize {
462 self.indices.len()
463 }
464
465 pub fn sparsity(&self) -> f32 {
467 if self.dimension == 0 {
468 1.0
469 } else {
470 1.0 - (self.nnz() as f32 / self.dimension as f32)
471 }
472 }
473
474 pub fn get(&self, index: u32) -> f32 {
476 self.indices
477 .iter()
478 .position(|&i| i == index)
479 .map(|pos| self.values[pos])
480 .unwrap_or(0.0)
481 }
482}
483
484#[derive(Debug, Clone)]
486pub struct EmbeddingSlot {
487 pub name: String,
489 pub vector: Vec<f32>,
491 pub model: String,
493 pub dimension: usize,
495 pub generated_at: u64,
497}
498
499use crate::clock::current_unix_secs;
500
501impl EmbeddingSlot {
502 pub fn new(name: impl Into<String>, vector: Vec<f32>, model: impl Into<String>) -> Self {
504 let dimension = vector.len();
505 Self {
506 name: name.into(),
507 vector,
508 model: model.into(),
509 dimension,
510 generated_at: current_unix_secs(),
511 }
512 }
513}
514
515#[derive(Debug, Clone)]
517pub struct UnifiedEntity {
518 pub id: EntityId,
520 logical_id: Option<EntityId>,
524 pub kind: EntityKind,
526 pub created_at: u64,
528 pub updated_at: u64,
530 pub data: EntityData,
532 pub sequence_id: u64,
534 pub field_bloom: u64,
545 pub xmin: u64,
554 pub xmax: u64,
560 aux: Option<Box<EntityAux>>,
563}
564
565#[derive(Debug, Clone, Default)]
567pub struct EntityAux {
568 pub embeddings: Vec<EmbeddingSlot>,
570 pub cross_refs: Vec<CrossRef>,
572}
573
574impl UnifiedEntity {
575 pub fn canonicalize_system_collection_names(&mut self) {
579 match &mut self.kind {
580 EntityKind::TableRow { table, .. } => {
581 if let Some(canonical) = super::system_collection_names::canonical_name(table) {
582 *table = Arc::from(canonical);
583 }
584 }
585 EntityKind::Vector { collection } => {
586 if let Some(canonical) = super::system_collection_names::canonical_name(collection)
587 {
588 *collection = canonical.to_owned();
589 }
590 }
591 EntityKind::TimeSeriesPoint(point) => {
592 if let Some(canonical) =
593 super::system_collection_names::canonical_name(&point.series)
594 {
595 point.series = canonical.to_owned();
596 }
597 }
598 EntityKind::QueueMessage { queue, .. } => {
599 if let Some(canonical) = super::system_collection_names::canonical_name(queue) {
600 *queue = canonical.to_owned();
601 }
602 }
603 EntityKind::GraphNode(_) | EntityKind::GraphEdge(_) => {}
604 }
605 if let Some(aux) = &mut self.aux {
606 for reference in &mut aux.cross_refs {
607 if let Some(canonical) =
608 super::system_collection_names::canonical_name(&reference.target_collection)
609 {
610 reference.target_collection = canonical.to_owned();
611 }
612 }
613 }
614 }
615
616 pub fn embeddings(&self) -> &[EmbeddingSlot] {
618 self.aux
619 .as_ref()
620 .map(|a| a.embeddings.as_slice())
621 .unwrap_or(&[])
622 }
623
624 pub fn cross_refs(&self) -> &[CrossRef] {
626 self.aux
627 .as_ref()
628 .map(|a| a.cross_refs.as_slice())
629 .unwrap_or(&[])
630 }
631
632 pub fn embeddings_mut(&mut self) -> &mut Vec<EmbeddingSlot> {
634 &mut self.aux.get_or_insert_with(Default::default).embeddings
635 }
636
637 pub fn cross_refs_mut(&mut self) -> &mut Vec<CrossRef> {
639 &mut self.aux.get_or_insert_with(Default::default).cross_refs
640 }
641
642 pub fn has_aux(&self) -> bool {
644 self.aux.is_some()
645 }
646}
647
648#[inline]
654pub fn field_name_bloom(name: &str) -> u64 {
655 let b = name.as_bytes();
656 if b.is_empty() {
657 return 0;
658 }
659 1u64 << (b[b.len() / 2] & 63)
660}
661
662pub fn compute_entity_field_bloom(data: &EntityData) -> u64 {
666 match data {
667 EntityData::Row(row) => {
668 if row.schema.is_some() {
669 return 0;
672 }
673 if let Some(named) = &row.named {
674 let mut bloom = named.keys().fold(0u64, |acc, k| acc | field_name_bloom(k));
675 if let Some(Value::Json(bytes)) = named.get("body") {
683 if let Ok(names) = reddb_types::document_body_codec::field_names(bytes) {
684 for name in names {
685 bloom |= field_name_bloom(&name);
686 }
687 }
688 }
689 bloom
690 } else {
691 0
692 }
693 }
694 EntityData::Node(node) => node
695 .properties
696 .keys()
697 .fold(0u64, |acc, k| acc | field_name_bloom(k)),
698 EntityData::Edge(edge) => edge
699 .properties
700 .keys()
701 .fold(0u64, |acc, k| acc | field_name_bloom(k)),
702 _ => 0,
704 }
705}
706
707impl UnifiedEntity {
708 pub fn new(id: EntityId, kind: EntityKind, data: EntityData) -> Self {
710 let now = current_unix_secs();
711 let field_bloom = compute_entity_field_bloom(&data);
712
713 Self {
714 id,
715 logical_id: None,
716 kind,
717 created_at: now,
718 updated_at: now,
719 data,
720 sequence_id: 0,
721 field_bloom,
722 xmin: 0,
725 xmax: 0,
726 aux: None,
727 }
728 }
729
730 #[inline]
741 pub fn is_visible(&self, snapshot_xid: u64) -> bool {
742 if self.xmin != 0 && self.xmin > snapshot_xid {
743 return false;
744 }
745 if self.xmax != 0 && self.xmax <= snapshot_xid {
746 return false;
747 }
748 true
749 }
750
751 #[inline]
754 pub fn set_xmin(&mut self, xid: u64) {
755 self.xmin = xid;
756 }
757
758 #[inline]
762 pub fn set_xmax(&mut self, xid: u64) {
763 self.xmax = xid;
764 }
765
766 #[inline]
769 pub fn logical_id(&self) -> EntityId {
770 self.logical_id.unwrap_or(self.id)
771 }
772
773 #[inline]
775 pub fn has_explicit_logical_id(&self) -> bool {
776 self.logical_id.is_some()
777 }
778
779 #[inline]
781 pub fn set_logical_id(&mut self, logical_id: EntityId) {
782 self.logical_id = Some(logical_id);
783 }
784
785 #[inline]
797 pub fn ensure_table_logical_id(&mut self) {
798 if self.logical_id.is_none()
799 && matches!(
800 self.kind,
801 EntityKind::TableRow { .. } | EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
802 )
803 {
804 self.logical_id = Some(self.id);
805 }
806 }
807
808 pub fn table_row(
810 id: EntityId,
811 table: impl Into<Arc<str>>,
812 row_id: u64,
813 columns: Vec<Value>,
814 ) -> Self {
815 Self::new(
816 id,
817 EntityKind::TableRow {
818 table: table.into(),
819 row_id,
820 },
821 EntityData::Row(RowData::new(columns)),
822 )
823 }
824
825 pub fn graph_node(
827 id: EntityId,
828 label: impl Into<String>,
829 node_type: impl Into<String>,
830 properties: HashMap<String, Value>,
831 ) -> Self {
832 Self::new(
833 id,
834 EntityKind::GraphNode(Box::new(GraphNodeKind {
835 label: label.into(),
836 node_type: node_type.into(),
837 })),
838 EntityData::Node(NodeData::with_properties(properties)),
839 )
840 }
841
842 pub fn graph_edge(
844 id: EntityId,
845 label: impl Into<String>,
846 from: impl Into<String>,
847 to: impl Into<String>,
848 weight: f32,
849 properties: HashMap<String, Value>,
850 ) -> Self {
851 Self::new(
852 id,
853 EntityKind::GraphEdge(Box::new(GraphEdgeKind {
854 label: label.into(),
855 from_node: from.into(),
856 to_node: to.into(),
857 weight: (weight * 1000.0) as u32,
858 })),
859 EntityData::Edge(EdgeData::with_properties(weight, properties)),
860 )
861 }
862
863 pub fn vector(id: EntityId, collection: impl Into<String>, vector: Vec<f32>) -> Self {
865 Self::new(
866 id,
867 EntityKind::Vector {
868 collection: collection.into(),
869 },
870 EntityData::Vector(VectorData::new(vector)),
871 )
872 }
873
874 pub fn add_embedding(&mut self, slot: EmbeddingSlot) {
876 self.embeddings_mut().push(slot);
877 self.touch();
878 }
879
880 pub fn add_cross_ref(&mut self, cross_ref: CrossRef) {
882 self.cross_refs_mut().push(cross_ref);
883 self.touch();
884 }
885
886 pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingSlot> {
888 self.embeddings().iter().find(|e| e.name == name)
889 }
890
891 fn touch(&mut self) {
893 self.updated_at = current_unix_secs();
894 }
895
896 pub fn is_stale(&self, max_age_secs: u64) -> bool {
898 let now = current_unix_secs();
899 now.saturating_sub(self.updated_at) > max_age_secs
900 }
901}
902
903#[derive(Debug, Clone, PartialEq)]
905pub struct CrossRef {
906 pub source: EntityId,
908 pub target: EntityId,
910 pub target_collection: String,
912 pub ref_type: RefType,
914 pub weight: f32,
916 pub created_at: u64,
918}
919
920impl CrossRef {
921 pub fn new(
923 source: EntityId,
924 target: EntityId,
925 target_collection: impl Into<String>,
926 ref_type: RefType,
927 ) -> Self {
928 Self {
929 source,
930 target,
931 target_collection: target_collection.into(),
932 ref_type,
933 weight: 1.0,
934 created_at: current_unix_secs(),
935 }
936 }
937
938 pub fn with_weight(
940 source: EntityId,
941 target: EntityId,
942 target_collection: impl Into<String>,
943 ref_type: RefType,
944 weight: f32,
945 ) -> Self {
946 let mut cr = Self::new(source, target, target_collection, ref_type);
947 cr.weight = weight;
948 cr
949 }
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
954pub enum RefType {
955 RowToNode, RowToEdge, NodeToRow, RowToVector, VectorToRow, NodeToVector, EdgeToVector, VectorToNode, SimilarTo, RelatedTo, DerivesFrom, Mentions, Contains, DependsOn, }
977
978impl RefType {
979 pub fn inverse(&self) -> Option<Self> {
981 match self {
982 Self::RowToNode => Some(Self::NodeToRow),
983 Self::NodeToRow => Some(Self::RowToNode),
984 Self::RowToVector => Some(Self::VectorToRow),
985 Self::VectorToRow => Some(Self::RowToVector),
986 Self::NodeToVector => Some(Self::VectorToNode),
987 Self::VectorToNode => Some(Self::NodeToVector),
988 Self::SimilarTo => Some(Self::SimilarTo), Self::RelatedTo => Some(Self::RelatedTo), _ => None, }
992 }
993
994 pub fn is_symmetric(&self) -> bool {
996 matches!(self, Self::SimilarTo | Self::RelatedTo)
997 }
998
999 pub fn to_byte(&self) -> u8 {
1001 match self {
1002 Self::RowToNode => 0,
1003 Self::RowToEdge => 1,
1004 Self::NodeToRow => 2,
1005 Self::RowToVector => 3,
1006 Self::VectorToRow => 4,
1007 Self::NodeToVector => 5,
1008 Self::EdgeToVector => 6,
1009 Self::VectorToNode => 7,
1010 Self::SimilarTo => 8,
1011 Self::RelatedTo => 9,
1012 Self::DerivesFrom => 10,
1013 Self::Mentions => 11,
1014 Self::Contains => 12,
1015 Self::DependsOn => 13,
1016 }
1017 }
1018
1019 pub fn from_byte(byte: u8) -> Self {
1021 match byte {
1022 0 => Self::RowToNode,
1023 1 => Self::RowToEdge,
1024 2 => Self::NodeToRow,
1025 3 => Self::RowToVector,
1026 4 => Self::VectorToRow,
1027 5 => Self::NodeToVector,
1028 6 => Self::EdgeToVector,
1029 7 => Self::VectorToNode,
1030 8 => Self::SimilarTo,
1031 9 => Self::RelatedTo,
1032 10 => Self::DerivesFrom,
1033 11 => Self::Mentions,
1034 12 => Self::Contains,
1035 13 => Self::DependsOn,
1036 _ => Self::RelatedTo, }
1038 }
1039}
1040
1041impl From<Vec<Value>> for RowData {
1043 fn from(columns: Vec<Value>) -> Self {
1044 RowData::new(columns)
1045 }
1046}
1047
1048impl From<HashMap<String, Value>> for NodeData {
1050 fn from(properties: HashMap<String, Value>) -> Self {
1051 NodeData::with_properties(properties)
1052 }
1053}
1054
1055impl From<Vec<f32>> for VectorData {
1057 fn from(dense: Vec<f32>) -> Self {
1058 VectorData::new(dense)
1059 }
1060}
1061
1062impl From<(Vec<f32>, SparseVector)> for VectorData {
1064 fn from((dense, sparse): (Vec<f32>, SparseVector)) -> Self {
1065 VectorData::with_sparse(dense, sparse)
1066 }
1067}
1068
1069impl UnifiedEntity {
1071 pub fn from_properties(
1073 id: EntityId,
1074 label: impl Into<String>,
1075 node_type: impl Into<String>,
1076 properties: impl IntoIterator<Item = (impl Into<String>, Value)>,
1077 ) -> Self {
1078 let props: HashMap<String, Value> =
1079 properties.into_iter().map(|(k, v)| (k.into(), v)).collect();
1080 Self::graph_node(id, label, node_type, props)
1081 }
1082
1083 pub fn into_row(self) -> Option<RowData> {
1085 match self.data {
1086 EntityData::Row(r) => Some(r),
1087 _ => None,
1088 }
1089 }
1090
1091 pub fn into_node(self) -> Option<NodeData> {
1093 match self.data {
1094 EntityData::Node(n) => Some(n),
1095 _ => None,
1096 }
1097 }
1098
1099 pub fn into_edge(self) -> Option<EdgeData> {
1101 match self.data {
1102 EntityData::Edge(e) => Some(e),
1103 _ => None,
1104 }
1105 }
1106
1107 pub fn into_vector(self) -> Option<VectorData> {
1109 match self.data {
1110 EntityData::Vector(v) => Some(v),
1111 _ => None,
1112 }
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 #[test]
1121 fn test_entity_creation() {
1122 let id = EntityId::new(1);
1123 let entity = UnifiedEntity::table_row(
1124 id,
1125 "users",
1126 100,
1127 vec![Value::text("alice".to_string()), Value::Integer(25)],
1128 );
1129
1130 assert!(entity.data.is_row());
1131 assert_eq!(entity.kind.storage_type(), "table");
1132 assert_eq!(entity.kind.collection(), "users");
1133 }
1134
1135 #[test]
1136 fn test_cross_refs() {
1137 let id1 = EntityId::new(1);
1138 let id2 = EntityId::new(2);
1139
1140 let cross_ref = CrossRef::new(id1, id2, "nodes", RefType::RowToNode);
1141 assert_eq!(cross_ref.source, id1);
1142 assert_eq!(cross_ref.target, id2);
1143 assert_eq!(cross_ref.ref_type.inverse(), Some(RefType::NodeToRow));
1144 }
1145
1146 #[test]
1147 fn test_sparse_vector() {
1148 let sparse = SparseVector::new(vec![0, 5, 10], vec![1.0, 2.0, 3.0], 100);
1149
1150 assert_eq!(sparse.nnz(), 3);
1151 assert_eq!(sparse.get(5), 2.0);
1152 assert_eq!(sparse.get(3), 0.0);
1153 assert!(sparse.sparsity() > 0.9);
1154 }
1155
1156 #[test]
1157 fn test_embedding_slots() {
1158 let mut entity = UnifiedEntity::table_row(
1159 EntityId::new(1),
1160 "documents",
1161 1,
1162 vec![Value::text("Hello world".to_string())],
1163 );
1164
1165 entity.add_embedding(EmbeddingSlot::new(
1166 "content",
1167 vec![0.1, 0.2, 0.3],
1168 "text-embedding-3-small",
1169 ));
1170
1171 assert_eq!(entity.embeddings().len(), 1);
1172 assert!(entity.get_embedding("content").is_some());
1173 assert!(entity.get_embedding("summary").is_none());
1174 }
1175}