1use std::collections::HashMap;
7use std::fmt;
8use std::sync::Arc;
9
10use crate::storage::schema::Value;
11
12pub const FIRST_USER_ENTITY_ID: u64 = 1024;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct EntityId(pub u64);
28
29impl EntityId {
30 pub fn new(id: u64) -> Self {
32 Self(id)
33 }
34
35 pub fn raw(&self) -> u64 {
37 self.0
38 }
39}
40
41impl fmt::Display for EntityId {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "e{}", self.0)
44 }
45}
46
47impl From<u64> for EntityId {
48 fn from(id: u64) -> Self {
49 Self(id)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum EntityKind {
56 TableRow { table: Arc<str>, row_id: u64 },
58 GraphNode(Box<GraphNodeKind>),
60 GraphEdge(Box<GraphEdgeKind>),
62 Vector { collection: String },
64 TimeSeriesPoint(Box<TimeSeriesPointKind>),
66 QueueMessage { queue: String, position: u64 },
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct GraphNodeKind {
72 pub label: String,
73 pub node_type: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub struct GraphEdgeKind {
78 pub label: String,
79 pub from_node: String,
80 pub to_node: String,
81 pub weight: u32,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub struct TimeSeriesPointKind {
86 pub series: String,
87 pub metric: String,
88}
89
90impl EntityKind {
91 pub fn storage_type(&self) -> &'static str {
93 match self {
94 Self::TableRow { .. } => "table",
95 Self::GraphNode(_) => "graph_node",
96 Self::GraphEdge(_) => "graph_edge",
97 Self::Vector { .. } => "vector",
98 Self::TimeSeriesPoint(_) => "timeseries",
99 Self::QueueMessage { .. } => "queue",
100 }
101 }
102
103 pub fn collection(&self) -> &str {
105 match self {
106 Self::TableRow { table, .. } => table,
107 Self::GraphNode(n) => &n.label,
108 Self::GraphEdge(e) => &e.label,
109 Self::Vector { collection } => collection,
110 Self::TimeSeriesPoint(ts) => &ts.series,
111 Self::QueueMessage { queue, .. } => queue,
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
118pub enum EntityData {
119 Row(RowData),
121 Node(NodeData),
123 Edge(EdgeData),
125 Vector(VectorData),
127 TimeSeries(TimeSeriesData),
129 QueueMessage(QueueMessageData),
131}
132
133impl EntityData {
134 pub fn is_row(&self) -> bool {
136 matches!(self, Self::Row(_))
137 }
138
139 pub fn is_node(&self) -> bool {
141 matches!(self, Self::Node(_))
142 }
143
144 pub fn is_edge(&self) -> bool {
146 matches!(self, Self::Edge(_))
147 }
148
149 pub fn is_vector(&self) -> bool {
151 matches!(self, Self::Vector(_))
152 }
153
154 pub fn as_row(&self) -> Option<&RowData> {
156 match self {
157 Self::Row(r) => Some(r),
158 _ => None,
159 }
160 }
161
162 pub fn as_node(&self) -> Option<&NodeData> {
164 match self {
165 Self::Node(n) => Some(n),
166 _ => None,
167 }
168 }
169
170 pub fn as_edge(&self) -> Option<&EdgeData> {
172 match self {
173 Self::Edge(e) => Some(e),
174 _ => None,
175 }
176 }
177
178 pub fn as_vector(&self) -> Option<&VectorData> {
180 match self {
181 Self::Vector(v) => Some(v),
182 _ => None,
183 }
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct RowData {
190 pub columns: Vec<Value>,
192 pub named: Option<HashMap<String, Value>>,
194 pub schema: Option<std::sync::Arc<Vec<String>>>,
198}
199
200impl RowData {
201 pub fn new(columns: Vec<Value>) -> Self {
203 Self {
204 columns,
205 named: None,
206 schema: None,
207 }
208 }
209
210 pub fn with_names(columns: Vec<Value>, names: Vec<String>) -> Self {
212 let named: HashMap<String, Value> =
213 names.into_iter().zip(columns.iter().cloned()).collect();
214 Self {
215 columns,
216 named: Some(named),
217 schema: None,
218 }
219 }
220
221 pub fn get_field(&self, name: &str) -> Option<&Value> {
223 if let Some(ref named) = self.named {
225 return named.get(name);
226 }
227 if let Some(ref schema) = self.schema {
229 if let Some(idx) = schema.iter().position(|s| s == name) {
230 return self.columns.get(idx);
231 }
232 }
233 None
234 }
235
236 pub fn iter_fields(&self) -> Box<dyn Iterator<Item = (&str, &Value)> + '_> {
238 if let Some(ref named) = self.named {
239 Box::new(named.iter().map(|(k, v)| (k.as_str(), v)))
240 } else if let Some(ref schema) = self.schema {
241 Box::new(
242 schema
243 .iter()
244 .zip(self.columns.iter())
245 .map(|(k, v)| (k.as_str(), v)),
246 )
247 } else {
248 Box::new(std::iter::empty())
249 }
250 }
251
252 pub fn get(&self, index: usize) -> Option<&Value> {
254 self.columns.get(index)
255 }
256
257 pub fn get_by_name(&self, name: &str) -> Option<&Value> {
259 self.named.as_ref()?.get(name)
260 }
261
262 pub fn len(&self) -> usize {
264 self.columns.len()
265 }
266
267 pub fn is_empty(&self) -> bool {
269 self.columns.is_empty()
270 }
271}
272
273#[derive(Debug, Clone)]
275pub struct NodeData {
276 pub properties: HashMap<String, Value>,
278}
279
280impl NodeData {
281 pub fn new() -> Self {
283 Self {
284 properties: HashMap::new(),
285 }
286 }
287
288 pub fn with_properties(properties: HashMap<String, Value>) -> Self {
290 Self { properties }
291 }
292
293 pub fn set(&mut self, key: impl Into<String>, value: Value) {
295 self.properties.insert(key.into(), value);
296 }
297
298 pub fn get(&self, key: &str) -> Option<&Value> {
300 self.properties.get(key)
301 }
302
303 pub fn has(&self, key: &str) -> bool {
305 self.properties.contains_key(key)
306 }
307}
308
309impl Default for NodeData {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315#[derive(Debug, Clone)]
317pub struct EdgeData {
318 pub properties: HashMap<String, Value>,
320 pub weight: f32,
322}
323
324impl EdgeData {
325 pub fn new(weight: f32) -> Self {
327 Self {
328 properties: HashMap::new(),
329 weight,
330 }
331 }
332
333 pub fn with_properties(weight: f32, properties: HashMap<String, Value>) -> Self {
335 Self { properties, weight }
336 }
337
338 pub fn set(&mut self, key: impl Into<String>, value: Value) {
340 self.properties.insert(key.into(), value);
341 }
342
343 pub fn get(&self, key: &str) -> Option<&Value> {
345 self.properties.get(key)
346 }
347}
348
349impl Default for EdgeData {
350 fn default() -> Self {
351 Self::new(1.0)
352 }
353}
354
355#[derive(Debug, Clone)]
357pub struct VectorData {
358 pub dense: Vec<f32>,
360 pub sparse: Option<SparseVector>,
362 pub content: Option<String>,
364}
365
366impl VectorData {
367 pub fn new(dense: Vec<f32>) -> Self {
369 Self {
370 dense,
371 sparse: None,
372 content: None,
373 }
374 }
375
376 pub fn with_sparse(dense: Vec<f32>, sparse: SparseVector) -> Self {
378 Self {
379 dense,
380 sparse: Some(sparse),
381 content: None,
382 }
383 }
384
385 pub fn with_content(mut self, content: impl Into<String>) -> Self {
387 self.content = Some(content.into());
388 self
389 }
390
391 pub fn dimension(&self) -> usize {
393 self.dense.len()
394 }
395
396 pub fn is_hybrid(&self) -> bool {
398 self.sparse.is_some()
399 }
400}
401
402#[derive(Debug, Clone)]
404pub struct TimeSeriesData {
405 pub metric: String,
407 pub series_id: Option<u64>,
409 pub timestamp_ns: u64,
411 pub value: f64,
413 pub tags: std::collections::HashMap<String, String>,
415 pub fields: std::collections::HashMap<String, Value>,
417}
418
419#[derive(Debug, Clone)]
421pub struct QueueMessageData {
422 pub payload: Value,
424 pub priority: Option<i32>,
426 pub enqueued_at_ns: u64,
428 pub attempts: u32,
430 pub max_attempts: u32,
432 pub acked: bool,
434}
435
436#[derive(Debug, Clone)]
438pub struct SparseVector {
439 pub indices: Vec<u32>,
441 pub values: Vec<f32>,
443 pub dimension: usize,
445}
446
447impl SparseVector {
448 pub fn new(indices: Vec<u32>, values: Vec<f32>, dimension: usize) -> Self {
450 debug_assert_eq!(indices.len(), values.len());
451 Self {
452 indices,
453 values,
454 dimension,
455 }
456 }
457
458 pub fn nnz(&self) -> usize {
460 self.indices.len()
461 }
462
463 pub fn sparsity(&self) -> f32 {
465 if self.dimension == 0 {
466 1.0
467 } else {
468 1.0 - (self.nnz() as f32 / self.dimension as f32)
469 }
470 }
471
472 pub fn get(&self, index: u32) -> f32 {
474 self.indices
475 .iter()
476 .position(|&i| i == index)
477 .map(|pos| self.values[pos])
478 .unwrap_or(0.0)
479 }
480}
481
482#[derive(Debug, Clone)]
484pub struct EmbeddingSlot {
485 pub name: String,
487 pub vector: Vec<f32>,
489 pub model: String,
491 pub dimension: usize,
493 pub generated_at: u64,
495}
496
497fn current_unix_secs() -> u64 {
498 std::time::SystemTime::now()
499 .duration_since(std::time::UNIX_EPOCH)
500 .unwrap_or_default()
501 .as_secs()
502}
503
504impl EmbeddingSlot {
505 pub fn new(name: impl Into<String>, vector: Vec<f32>, model: impl Into<String>) -> Self {
507 let dimension = vector.len();
508 Self {
509 name: name.into(),
510 vector,
511 model: model.into(),
512 dimension,
513 generated_at: current_unix_secs(),
514 }
515 }
516}
517
518#[derive(Debug, Clone)]
520pub struct UnifiedEntity {
521 pub id: EntityId,
523 logical_id: Option<EntityId>,
527 pub kind: EntityKind,
529 pub created_at: u64,
531 pub updated_at: u64,
533 pub data: EntityData,
535 pub sequence_id: u64,
537 pub field_bloom: u64,
548 pub xmin: u64,
557 pub xmax: u64,
563 aux: Option<Box<EntityAux>>,
566}
567
568#[derive(Debug, Clone, Default)]
570pub struct EntityAux {
571 pub embeddings: Vec<EmbeddingSlot>,
573 pub cross_refs: Vec<CrossRef>,
575}
576
577impl UnifiedEntity {
578 pub fn embeddings(&self) -> &[EmbeddingSlot] {
580 self.aux
581 .as_ref()
582 .map(|a| a.embeddings.as_slice())
583 .unwrap_or(&[])
584 }
585
586 pub fn cross_refs(&self) -> &[CrossRef] {
588 self.aux
589 .as_ref()
590 .map(|a| a.cross_refs.as_slice())
591 .unwrap_or(&[])
592 }
593
594 pub fn embeddings_mut(&mut self) -> &mut Vec<EmbeddingSlot> {
596 &mut self.aux.get_or_insert_with(Default::default).embeddings
597 }
598
599 pub fn cross_refs_mut(&mut self) -> &mut Vec<CrossRef> {
601 &mut self.aux.get_or_insert_with(Default::default).cross_refs
602 }
603
604 pub fn has_aux(&self) -> bool {
606 self.aux.is_some()
607 }
608}
609
610#[inline]
616pub fn field_name_bloom(name: &str) -> u64 {
617 let b = name.as_bytes();
618 if b.is_empty() {
619 return 0;
620 }
621 1u64 << (b[b.len() / 2] & 63)
622}
623
624pub fn compute_entity_field_bloom(data: &EntityData) -> u64 {
628 match data {
629 EntityData::Row(row) => {
630 if row.schema.is_some() {
631 return 0;
634 }
635 if let Some(named) = &row.named {
636 let mut bloom = named.keys().fold(0u64, |acc, k| acc | field_name_bloom(k));
637 if let Some(Value::Json(bytes)) = named.get("body") {
645 if let Some(names) = crate::document_body::container_field_names(bytes) {
646 for name in names {
647 bloom |= field_name_bloom(&name);
648 }
649 }
650 }
651 bloom
652 } else {
653 0
654 }
655 }
656 EntityData::Node(node) => node
657 .properties
658 .keys()
659 .fold(0u64, |acc, k| acc | field_name_bloom(k)),
660 EntityData::Edge(edge) => edge
661 .properties
662 .keys()
663 .fold(0u64, |acc, k| acc | field_name_bloom(k)),
664 _ => 0,
666 }
667}
668
669impl UnifiedEntity {
670 pub fn new(id: EntityId, kind: EntityKind, data: EntityData) -> Self {
672 let now = current_unix_secs();
673 let field_bloom = compute_entity_field_bloom(&data);
674
675 Self {
676 id,
677 logical_id: None,
678 kind,
679 created_at: now,
680 updated_at: now,
681 data,
682 sequence_id: 0,
683 field_bloom,
684 xmin: 0,
687 xmax: 0,
688 aux: None,
689 }
690 }
691
692 #[inline]
703 pub fn is_visible(&self, snapshot_xid: u64) -> bool {
704 if self.xmin != 0 && self.xmin > snapshot_xid {
705 return false;
706 }
707 if self.xmax != 0 && self.xmax <= snapshot_xid {
708 return false;
709 }
710 true
711 }
712
713 #[inline]
716 pub fn set_xmin(&mut self, xid: u64) {
717 self.xmin = xid;
718 }
719
720 #[inline]
724 pub fn set_xmax(&mut self, xid: u64) {
725 self.xmax = xid;
726 }
727
728 #[inline]
731 pub fn logical_id(&self) -> EntityId {
732 self.logical_id.unwrap_or(self.id)
733 }
734
735 #[inline]
737 pub fn has_explicit_logical_id(&self) -> bool {
738 self.logical_id.is_some()
739 }
740
741 #[inline]
743 pub fn set_logical_id(&mut self, logical_id: EntityId) {
744 self.logical_id = Some(logical_id);
745 }
746
747 #[inline]
759 pub(crate) fn ensure_table_logical_id(&mut self) {
760 if self.logical_id.is_none()
761 && matches!(
762 self.kind,
763 EntityKind::TableRow { .. } | EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
764 )
765 {
766 self.logical_id = Some(self.id);
767 }
768 }
769
770 pub fn table_row(
772 id: EntityId,
773 table: impl Into<Arc<str>>,
774 row_id: u64,
775 columns: Vec<Value>,
776 ) -> Self {
777 Self::new(
778 id,
779 EntityKind::TableRow {
780 table: table.into(),
781 row_id,
782 },
783 EntityData::Row(RowData::new(columns)),
784 )
785 }
786
787 pub fn graph_node(
789 id: EntityId,
790 label: impl Into<String>,
791 node_type: impl Into<String>,
792 properties: HashMap<String, Value>,
793 ) -> Self {
794 Self::new(
795 id,
796 EntityKind::GraphNode(Box::new(GraphNodeKind {
797 label: label.into(),
798 node_type: node_type.into(),
799 })),
800 EntityData::Node(NodeData::with_properties(properties)),
801 )
802 }
803
804 pub fn graph_edge(
806 id: EntityId,
807 label: impl Into<String>,
808 from: impl Into<String>,
809 to: impl Into<String>,
810 weight: f32,
811 properties: HashMap<String, Value>,
812 ) -> Self {
813 Self::new(
814 id,
815 EntityKind::GraphEdge(Box::new(GraphEdgeKind {
816 label: label.into(),
817 from_node: from.into(),
818 to_node: to.into(),
819 weight: (weight * 1000.0) as u32,
820 })),
821 EntityData::Edge(EdgeData::with_properties(weight, properties)),
822 )
823 }
824
825 pub fn vector(id: EntityId, collection: impl Into<String>, vector: Vec<f32>) -> Self {
827 Self::new(
828 id,
829 EntityKind::Vector {
830 collection: collection.into(),
831 },
832 EntityData::Vector(VectorData::new(vector)),
833 )
834 }
835
836 pub fn add_embedding(&mut self, slot: EmbeddingSlot) {
838 self.embeddings_mut().push(slot);
839 self.touch();
840 }
841
842 pub fn add_cross_ref(&mut self, cross_ref: CrossRef) {
844 self.cross_refs_mut().push(cross_ref);
845 self.touch();
846 }
847
848 pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingSlot> {
850 self.embeddings().iter().find(|e| e.name == name)
851 }
852
853 fn touch(&mut self) {
855 self.updated_at = current_unix_secs();
856 }
857
858 pub fn is_stale(&self, max_age_secs: u64) -> bool {
860 let now = current_unix_secs();
861 now.saturating_sub(self.updated_at) > max_age_secs
862 }
863}
864
865#[derive(Debug, Clone, PartialEq)]
867pub struct CrossRef {
868 pub source: EntityId,
870 pub target: EntityId,
872 pub target_collection: String,
874 pub ref_type: RefType,
876 pub weight: f32,
878 pub created_at: u64,
880}
881
882impl CrossRef {
883 pub fn new(
885 source: EntityId,
886 target: EntityId,
887 target_collection: impl Into<String>,
888 ref_type: RefType,
889 ) -> Self {
890 Self {
891 source,
892 target,
893 target_collection: target_collection.into(),
894 ref_type,
895 weight: 1.0,
896 created_at: current_unix_secs(),
897 }
898 }
899
900 pub fn with_weight(
902 source: EntityId,
903 target: EntityId,
904 target_collection: impl Into<String>,
905 ref_type: RefType,
906 weight: f32,
907 ) -> Self {
908 let mut cr = Self::new(source, target, target_collection, ref_type);
909 cr.weight = weight;
910 cr
911 }
912}
913
914#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
916pub enum RefType {
917 RowToNode, RowToEdge, NodeToRow, RowToVector, VectorToRow, NodeToVector, EdgeToVector, VectorToNode, SimilarTo, RelatedTo, DerivesFrom, Mentions, Contains, DependsOn, }
939
940impl RefType {
941 pub fn inverse(&self) -> Option<Self> {
943 match self {
944 Self::RowToNode => Some(Self::NodeToRow),
945 Self::NodeToRow => Some(Self::RowToNode),
946 Self::RowToVector => Some(Self::VectorToRow),
947 Self::VectorToRow => Some(Self::RowToVector),
948 Self::NodeToVector => Some(Self::VectorToNode),
949 Self::VectorToNode => Some(Self::NodeToVector),
950 Self::SimilarTo => Some(Self::SimilarTo), Self::RelatedTo => Some(Self::RelatedTo), _ => None, }
954 }
955
956 pub fn is_symmetric(&self) -> bool {
958 matches!(self, Self::SimilarTo | Self::RelatedTo)
959 }
960
961 pub fn to_byte(&self) -> u8 {
963 match self {
964 Self::RowToNode => 0,
965 Self::RowToEdge => 1,
966 Self::NodeToRow => 2,
967 Self::RowToVector => 3,
968 Self::VectorToRow => 4,
969 Self::NodeToVector => 5,
970 Self::EdgeToVector => 6,
971 Self::VectorToNode => 7,
972 Self::SimilarTo => 8,
973 Self::RelatedTo => 9,
974 Self::DerivesFrom => 10,
975 Self::Mentions => 11,
976 Self::Contains => 12,
977 Self::DependsOn => 13,
978 }
979 }
980
981 pub fn from_byte(byte: u8) -> Self {
983 match byte {
984 0 => Self::RowToNode,
985 1 => Self::RowToEdge,
986 2 => Self::NodeToRow,
987 3 => Self::RowToVector,
988 4 => Self::VectorToRow,
989 5 => Self::NodeToVector,
990 6 => Self::EdgeToVector,
991 7 => Self::VectorToNode,
992 8 => Self::SimilarTo,
993 9 => Self::RelatedTo,
994 10 => Self::DerivesFrom,
995 11 => Self::Mentions,
996 12 => Self::Contains,
997 13 => Self::DependsOn,
998 _ => Self::RelatedTo, }
1000 }
1001}
1002
1003impl From<Vec<Value>> for RowData {
1005 fn from(columns: Vec<Value>) -> Self {
1006 RowData::new(columns)
1007 }
1008}
1009
1010impl From<HashMap<String, Value>> for NodeData {
1012 fn from(properties: HashMap<String, Value>) -> Self {
1013 NodeData::with_properties(properties)
1014 }
1015}
1016
1017impl From<Vec<f32>> for VectorData {
1019 fn from(dense: Vec<f32>) -> Self {
1020 VectorData::new(dense)
1021 }
1022}
1023
1024impl From<(Vec<f32>, SparseVector)> for VectorData {
1026 fn from((dense, sparse): (Vec<f32>, SparseVector)) -> Self {
1027 VectorData::with_sparse(dense, sparse)
1028 }
1029}
1030
1031impl UnifiedEntity {
1033 pub fn from_properties(
1035 id: EntityId,
1036 label: impl Into<String>,
1037 node_type: impl Into<String>,
1038 properties: impl IntoIterator<Item = (impl Into<String>, Value)>,
1039 ) -> Self {
1040 let props: HashMap<String, Value> =
1041 properties.into_iter().map(|(k, v)| (k.into(), v)).collect();
1042 Self::graph_node(id, label, node_type, props)
1043 }
1044
1045 pub fn into_row(self) -> Option<RowData> {
1047 match self.data {
1048 EntityData::Row(r) => Some(r),
1049 _ => None,
1050 }
1051 }
1052
1053 pub fn into_node(self) -> Option<NodeData> {
1055 match self.data {
1056 EntityData::Node(n) => Some(n),
1057 _ => None,
1058 }
1059 }
1060
1061 pub fn into_edge(self) -> Option<EdgeData> {
1063 match self.data {
1064 EntityData::Edge(e) => Some(e),
1065 _ => None,
1066 }
1067 }
1068
1069 pub fn into_vector(self) -> Option<VectorData> {
1071 match self.data {
1072 EntityData::Vector(v) => Some(v),
1073 _ => None,
1074 }
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081
1082 #[test]
1083 fn test_entity_creation() {
1084 let id = EntityId::new(1);
1085 let entity = UnifiedEntity::table_row(
1086 id,
1087 "users",
1088 100,
1089 vec![Value::text("alice".to_string()), Value::Integer(25)],
1090 );
1091
1092 assert!(entity.data.is_row());
1093 assert_eq!(entity.kind.storage_type(), "table");
1094 assert_eq!(entity.kind.collection(), "users");
1095 }
1096
1097 #[test]
1098 fn test_cross_refs() {
1099 let id1 = EntityId::new(1);
1100 let id2 = EntityId::new(2);
1101
1102 let cross_ref = CrossRef::new(id1, id2, "nodes", RefType::RowToNode);
1103 assert_eq!(cross_ref.source, id1);
1104 assert_eq!(cross_ref.target, id2);
1105 assert_eq!(cross_ref.ref_type.inverse(), Some(RefType::NodeToRow));
1106 }
1107
1108 #[test]
1109 fn test_sparse_vector() {
1110 let sparse = SparseVector::new(vec![0, 5, 10], vec![1.0, 2.0, 3.0], 100);
1111
1112 assert_eq!(sparse.nnz(), 3);
1113 assert_eq!(sparse.get(5), 2.0);
1114 assert_eq!(sparse.get(3), 0.0);
1115 assert!(sparse.sparsity() > 0.9);
1116 }
1117
1118 #[test]
1119 fn test_embedding_slots() {
1120 let mut entity = UnifiedEntity::table_row(
1121 EntityId::new(1),
1122 "documents",
1123 1,
1124 vec![Value::text("Hello world".to_string())],
1125 );
1126
1127 entity.add_embedding(EmbeddingSlot::new(
1128 "content",
1129 vec![0.1, 0.2, 0.3],
1130 "text-embedding-3-small",
1131 ));
1132
1133 assert_eq!(entity.embeddings().len(), 1);
1134 assert!(entity.get_embedding("content").is_some());
1135 assert!(entity.get_embedding("summary").is_none());
1136 }
1137}