Skip to main content

reddb_engine/storage/
entity.rs

1//! Unified Entity Model
2//!
3//! Provides a single entity type that can represent table rows, graph nodes,
4//! graph edges, or vectors with seamless interoperability.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use reddb_types::Value;
10
11/// The first entity-id handed to user-inserted data. Ids `1..FIRST_USER_ENTITY_ID`
12/// are reserved for the internal collection-descriptor and config-default entities
13/// the engine seeds at boot, so the first user-inserted `rid` is a STABLE,
14/// documented value regardless of how many config defaults a build ships.
15///
16/// Before this floor existed the offset drifted upward by one for every config
17/// default added (101 → 114 over time), silently breaking the documented
18/// file-format invariant (#1369). The boot sequence bumps the allocator up to
19/// this floor after seeding internals; it only ever raises the counter, so a
20/// database that already holds user data is untouched. Mirrors
21/// `FIRST_USER_LABEL_ID` in the graph label registry.
22pub const FIRST_USER_ENTITY_ID: u64 = 1024;
23
24pub use super::entity_id::EntityId;
25
26/// The kind of entity (what storage type it belongs to)
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum EntityKind {
29    /// A row in a structured table (hot path — kept inline for cache performance)
30    TableRow { table: Arc<str>, row_id: u64 },
31    /// A node in the graph (boxed — saves ~56 bytes per entity for table rows)
32    GraphNode(Box<GraphNodeKind>),
33    /// An edge in the graph (boxed)
34    GraphEdge(Box<GraphEdgeKind>),
35    /// A vector in a collection
36    Vector { collection: String },
37    /// A time-series data point (boxed)
38    TimeSeriesPoint(Box<TimeSeriesPointKind>),
39    /// A queue message
40    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    /// Get the storage type as a string
65    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    /// Get the collection/table name
77    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/// The actual data content of an entity
90#[derive(Debug, Clone)]
91pub enum EntityData {
92    /// Table row data
93    Row(RowData),
94    /// Graph node data
95    Node(NodeData),
96    /// Graph edge data
97    Edge(EdgeData),
98    /// Vector data
99    Vector(VectorData),
100    /// Time-series data point
101    TimeSeries(TimeSeriesData),
102    /// Queue message data
103    QueueMessage(QueueMessageData),
104}
105
106impl EntityData {
107    /// Check if this is row data
108    pub fn is_row(&self) -> bool {
109        matches!(self, Self::Row(_))
110    }
111
112    /// Check if this is node data
113    pub fn is_node(&self) -> bool {
114        matches!(self, Self::Node(_))
115    }
116
117    /// Check if this is edge data
118    pub fn is_edge(&self) -> bool {
119        matches!(self, Self::Edge(_))
120    }
121
122    /// Check if this is vector data
123    pub fn is_vector(&self) -> bool {
124        matches!(self, Self::Vector(_))
125    }
126
127    /// Get as row data
128    pub fn as_row(&self) -> Option<&RowData> {
129        match self {
130            Self::Row(r) => Some(r),
131            _ => None,
132        }
133    }
134
135    /// Get as node data
136    pub fn as_node(&self) -> Option<&NodeData> {
137        match self {
138            Self::Node(n) => Some(n),
139            _ => None,
140        }
141    }
142
143    /// Get as edge data
144    pub fn as_edge(&self) -> Option<&EdgeData> {
145        match self {
146            Self::Edge(e) => Some(e),
147            _ => None,
148        }
149    }
150
151    /// Get as vector data
152    pub fn as_vector(&self) -> Option<&VectorData> {
153        match self {
154            Self::Vector(v) => Some(v),
155            _ => None,
156        }
157    }
158}
159
160/// Data for a table row
161#[derive(Debug, Clone)]
162pub struct RowData {
163    /// Column values in schema order
164    pub columns: Vec<Value>,
165    /// Named column access (optional, for convenience)
166    pub named: Option<HashMap<String, Value>>,
167    /// Shared column schema: column names in order (maps index → name).
168    /// When set, `columns` holds the values and `named` is None.
169    /// This saves ~60% memory vs per-row HashMap.
170    pub schema: Option<std::sync::Arc<Vec<String>>>,
171}
172
173impl RowData {
174    /// Create new row data from column values
175    pub fn new(columns: Vec<Value>) -> Self {
176        Self {
177            columns,
178            named: None,
179            schema: None,
180        }
181    }
182
183    /// Create row data with named columns
184    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    /// Replace a decoded named row with values in the authoritative schema's
195    /// order. Different dynamic shapes retain their named representation.
196    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        // Existing positional values are independently observable. Only drop
204        // their duplicate copy when the declared order agrees with them.
205        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    /// Get a named field value — checks named HashMap first, then schema+columns.
224    pub fn get_field(&self, name: &str) -> Option<&Value> {
225        // Fast path: named HashMap
226        if let Some(ref named) = self.named {
227            return named.get(name);
228        }
229        // Columnar path: use schema to find index
230        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    /// Iterate over all (name, value) pairs — works for both named and columnar.
239    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    /// Get column by index
255    pub fn get(&self, index: usize) -> Option<&Value> {
256        self.columns.get(index)
257    }
258
259    /// Get column by name
260    pub fn get_by_name(&self, name: &str) -> Option<&Value> {
261        self.named.as_ref()?.get(name)
262    }
263
264    /// Number of columns
265    pub fn len(&self) -> usize {
266        self.columns.len()
267    }
268
269    /// Check if empty
270    pub fn is_empty(&self) -> bool {
271        self.columns.is_empty()
272    }
273}
274
275/// Data for a graph node
276#[derive(Debug, Clone)]
277pub struct NodeData {
278    /// Node properties
279    pub properties: HashMap<String, Value>,
280}
281
282impl NodeData {
283    /// Create new node data
284    pub fn new() -> Self {
285        Self {
286            properties: HashMap::new(),
287        }
288    }
289
290    /// Create with properties
291    pub fn with_properties(properties: HashMap<String, Value>) -> Self {
292        Self { properties }
293    }
294
295    /// Set a property
296    pub fn set(&mut self, key: impl Into<String>, value: Value) {
297        self.properties.insert(key.into(), value);
298    }
299
300    /// Get a property
301    pub fn get(&self, key: &str) -> Option<&Value> {
302        self.properties.get(key)
303    }
304
305    /// Check if property exists
306    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/// Data for a graph edge
318#[derive(Debug, Clone)]
319pub struct EdgeData {
320    /// Edge properties
321    pub properties: HashMap<String, Value>,
322    /// Edge weight (for weighted graphs)
323    pub weight: f32,
324}
325
326impl EdgeData {
327    /// Create new edge data
328    pub fn new(weight: f32) -> Self {
329        Self {
330            properties: HashMap::new(),
331            weight,
332        }
333    }
334
335    /// Create with properties
336    pub fn with_properties(weight: f32, properties: HashMap<String, Value>) -> Self {
337        Self { properties, weight }
338    }
339
340    /// Set a property
341    pub fn set(&mut self, key: impl Into<String>, value: Value) {
342        self.properties.insert(key.into(), value);
343    }
344
345    /// Get a property
346    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/// Data for a vector
358#[derive(Debug, Clone)]
359pub struct VectorData {
360    /// Dense vector (primary embedding)
361    pub dense: Vec<f32>,
362    /// Optional sparse vector
363    pub sparse: Option<SparseVector>,
364    /// Original content (if applicable)
365    pub content: Option<String>,
366}
367
368impl VectorData {
369    /// Create new vector data from dense vector
370    pub fn new(dense: Vec<f32>) -> Self {
371        Self {
372            dense,
373            sparse: None,
374            content: None,
375        }
376    }
377
378    /// Create with sparse vector
379    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    /// Set content
388    pub fn with_content(mut self, content: impl Into<String>) -> Self {
389        self.content = Some(content.into());
390        self
391    }
392
393    /// Get dimension
394    pub fn dimension(&self) -> usize {
395        self.dense.len()
396    }
397
398    /// Check if has sparse component
399    pub fn is_hybrid(&self) -> bool {
400        self.sparse.is_some()
401    }
402}
403
404/// Time-series data point
405#[derive(Debug, Clone)]
406pub struct TimeSeriesData {
407    /// Metric name (e.g., "cpu.idle")
408    pub metric: String,
409    /// Interned `(metric, canonical tags)` id for native time-series points.
410    pub series_id: Option<u64>,
411    /// Timestamp in nanoseconds since epoch
412    pub timestamp_ns: u64,
413    /// Metric value
414    pub value: f64,
415    /// Legacy/read-through dimensional tags (e.g., {"host": "srv1"}).
416    pub tags: std::collections::HashMap<String, String>,
417    /// Per-point fields that are not part of the immutable series identity.
418    pub fields: std::collections::HashMap<String, Value>,
419}
420
421/// Queue message data
422#[derive(Debug, Clone)]
423pub struct QueueMessageData {
424    /// Message payload
425    pub payload: Value,
426    /// Optional priority (higher = more urgent)
427    pub priority: Option<i32>,
428    /// Enqueue timestamp (nanoseconds)
429    pub enqueued_at_ns: u64,
430    /// Number of delivery attempts
431    pub attempts: u32,
432    /// Maximum delivery attempts before DLQ
433    pub max_attempts: u32,
434    /// Whether the message has been acknowledged
435    pub acked: bool,
436}
437
438/// Sparse vector representation
439#[derive(Debug, Clone)]
440pub struct SparseVector {
441    /// Indices of non-zero elements
442    pub indices: Vec<u32>,
443    /// Values at those indices
444    pub values: Vec<f32>,
445    /// Total dimension (may be larger than indices.len())
446    pub dimension: usize,
447}
448
449impl SparseVector {
450    /// Create new sparse vector
451    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    /// Number of non-zero elements
461    pub fn nnz(&self) -> usize {
462        self.indices.len()
463    }
464
465    /// Sparsity ratio
466    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    /// Get value at index (0 if not present)
475    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/// A slot for embedding a specific aspect of an entity
485#[derive(Debug, Clone)]
486pub struct EmbeddingSlot {
487    /// Slot name (e.g., "content", "summary", "title", "code")
488    pub name: String,
489    /// The embedding vector
490    pub vector: Vec<f32>,
491    /// Model used to generate embedding
492    pub model: String,
493    /// Vector dimension
494    pub dimension: usize,
495    /// Generation timestamp
496    pub generated_at: u64,
497}
498
499use crate::clock::current_unix_secs;
500
501impl EmbeddingSlot {
502    /// Create a new embedding slot
503    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/// A unified entity that can represent any storage type
516#[derive(Debug, Clone)]
517pub struct UnifiedEntity {
518    /// Unique entity identifier
519    pub id: EntityId,
520    /// Stable user-visible identity shared by all physical versions.
521    ///
522    /// `None` is the legacy encoding and resolves to `id`.
523    logical_id: Option<EntityId>,
524    /// What kind of entity this is
525    pub kind: EntityKind,
526    /// Creation timestamp
527    pub created_at: u64,
528    /// Last update timestamp
529    pub updated_at: u64,
530    /// The actual data content
531    pub data: EntityData,
532    /// Sequence ID for ordering/versioning
533    pub sequence_id: u64,
534    /// Field-name bloom filter (u64, zero-allocation).
535    ///
536    /// Each bit encodes one possible mid-character value: for field name `n`
537    /// the bit position is `n.as_bytes()[n.len()/2] & 63`. OR of all user
538    /// field names present in this entity. Cleared for schema-based bulk rows
539    /// (all rows share the same schema so bloom is segment-level).
540    ///
541    /// The compiled filter computes `required_bloom` from predicate field names
542    /// at compile time. If `entity.field_bloom & required_bloom != required_bloom`,
543    /// the entity cannot match and is skipped before any HashMap probe.
544    pub field_bloom: u64,
545    /// MVCC creation transaction ID (Phase 2.3 PG parity).
546    ///
547    /// `0` means "pre-MVCC" / auto-commit — visible to every snapshot. When
548    /// a BEGIN-wrapped INSERT runs, it stamps `xmin` with the transaction's
549    /// snapshot id so other concurrent transactions only see the row after
550    /// the writer commits (snapshot isolation semantics).
551    ///
552    /// Visibility rule: `xmin <= snapshot.xid && (xmax == 0 || xmax > snapshot.xid)`.
553    pub xmin: u64,
554    /// MVCC deletion transaction ID (Phase 2.3 PG parity).
555    ///
556    /// `0` means "live". Set to the deleting transaction's snapshot id on
557    /// DELETE/UPDATE (row is kept until VACUUM reclaims it). Snapshots with
558    /// `xid < xmax` still see the row; newer snapshots skip it.
559    pub xmax: u64,
560    /// Optional auxiliary data (embeddings, cross-refs).
561    /// None for most table rows — saves 40 bytes/entity.
562    aux: Option<Box<EntityAux>>,
563}
564
565/// Auxiliary entity data — only allocated when needed.
566#[derive(Debug, Clone, Default)]
567pub struct EntityAux {
568    /// Embedding slots (for multi-vector support)
569    pub embeddings: Vec<EmbeddingSlot>,
570    /// Cross-references to other entities
571    pub cross_refs: Vec<CrossRef>,
572}
573
574impl UnifiedEntity {
575    /// Access embeddings (returns empty slice if no aux data).
576    pub fn embeddings(&self) -> &[EmbeddingSlot] {
577        self.aux
578            .as_ref()
579            .map(|a| a.embeddings.as_slice())
580            .unwrap_or(&[])
581    }
582
583    /// Access cross-references (returns empty slice if no aux data).
584    pub fn cross_refs(&self) -> &[CrossRef] {
585        self.aux
586            .as_ref()
587            .map(|a| a.cross_refs.as_slice())
588            .unwrap_or(&[])
589    }
590
591    /// Get mutable embeddings (allocates aux if needed).
592    pub fn embeddings_mut(&mut self) -> &mut Vec<EmbeddingSlot> {
593        &mut self.aux.get_or_insert_with(Default::default).embeddings
594    }
595
596    /// Get mutable cross-refs (allocates aux if needed).
597    pub fn cross_refs_mut(&mut self) -> &mut Vec<CrossRef> {
598        &mut self.aux.get_or_insert_with(Default::default).cross_refs
599    }
600
601    /// Check if entity has any auxiliary data.
602    pub fn has_aux(&self) -> bool {
603        self.aux.is_some()
604    }
605}
606
607/// Compute one bit of a field-name bloom filter.
608///
609/// Uses the mid-character trick from MongoDB's field-name bloom:
610/// the bit position is the mid-byte value clamped to 0..63. Zero-allocation,
611/// ~1.5% false-positive rate for ≤5 distinct field names.
612#[inline]
613pub fn field_name_bloom(name: &str) -> u64 {
614    let b = name.as_bytes();
615    if b.is_empty() {
616        return 0;
617    }
618    1u64 << (b[b.len() / 2] & 63)
619}
620
621/// Compute the combined field-name bloom for all user-level fields in `data`.
622/// Returns 0 for schema-based rows (all rows share the same schema, so the
623/// per-entity bloom would be identical — caller uses a segment-level bloom).
624pub fn compute_entity_field_bloom(data: &EntityData) -> u64 {
625    match data {
626        EntityData::Row(row) => {
627            if row.schema.is_some() {
628                // Schema path: bloom is identical for every row in this table.
629                // Don't store per-entity — use segment-level bloom instead.
630                return 0;
631            }
632            if let Some(named) = &row.named {
633                let mut bloom = named.keys().fold(0u64, |acc, k| acc | field_name_bloom(k));
634                // Single-source documents no longer materialise promoted
635                // columns, so the body's top-level keys are the only place a
636                // `WHERE`/projection field lives. Fold them into the bloom so
637                // the field-bloom gate doesn't reject a row before the body
638                // read fallback ever runs. Inert for non-document rows (no
639                // binary-container `body` field) — `container_field_names`
640                // self-gates on the RDOC magic.
641                if let Some(Value::Json(bytes)) = named.get("body") {
642                    if let Ok(names) = reddb_types::document_body_codec::field_names(bytes) {
643                        for name in names {
644                            bloom |= field_name_bloom(&name);
645                        }
646                    }
647                }
648                bloom
649            } else {
650                0
651            }
652        }
653        EntityData::Node(node) => node
654            .properties
655            .keys()
656            .fold(0u64, |acc, k| acc | field_name_bloom(k)),
657        EntityData::Edge(edge) => edge
658            .properties
659            .keys()
660            .fold(0u64, |acc, k| acc | field_name_bloom(k)),
661        // Vectors, time-series, queue: no user-named fields worth blooming.
662        _ => 0,
663    }
664}
665
666impl UnifiedEntity {
667    /// Create a new unified entity
668    pub fn new(id: EntityId, kind: EntityKind, data: EntityData) -> Self {
669        let now = current_unix_secs();
670        let field_bloom = compute_entity_field_bloom(&data);
671
672        Self {
673            id,
674            logical_id: None,
675            kind,
676            created_at: now,
677            updated_at: now,
678            data,
679            sequence_id: 0,
680            field_bloom,
681            // Pre-MVCC default: xmin/xmax = 0 means visible to every snapshot.
682            // Transactional writers stamp real snapshot IDs after allocation.
683            xmin: 0,
684            xmax: 0,
685            aux: None,
686        }
687    }
688
689    /// MVCC visibility check (Phase 2.3 PG parity).
690    ///
691    /// Returns `true` when this tuple is visible under the provided
692    /// snapshot xid. Pre-MVCC rows (`xmin == 0`, `xmax == 0`) are visible
693    /// to every snapshot — preserves full compatibility with existing
694    /// data inserted before the MVCC headers existed.
695    ///
696    /// Snapshot isolation rule:
697    ///   - `xmin == 0 || xmin <= snapshot_xid`  (creator committed before snapshot)
698    ///   - `xmax == 0 || xmax > snapshot_xid`   (deleter committed after snapshot)
699    #[inline]
700    pub fn is_visible(&self, snapshot_xid: u64) -> bool {
701        if self.xmin != 0 && self.xmin > snapshot_xid {
702            return false;
703        }
704        if self.xmax != 0 && self.xmax <= snapshot_xid {
705            return false;
706        }
707        true
708    }
709
710    /// Stamp `xmin` (creation transaction ID). Called by the runtime on
711    /// INSERT inside an active transaction.
712    #[inline]
713    pub fn set_xmin(&mut self, xid: u64) {
714        self.xmin = xid;
715    }
716
717    /// Stamp `xmax` (deletion transaction ID). Called by the runtime on
718    /// DELETE/UPDATE inside an active transaction — the tuple survives
719    /// until VACUUM reclaims it.
720    #[inline]
721    pub fn set_xmax(&mut self, xid: u64) {
722        self.xmax = xid;
723    }
724
725    /// Stable user-visible identity. Legacy rows without an explicit
726    /// logical id map to their physical entity id.
727    #[inline]
728    pub fn logical_id(&self) -> EntityId {
729        self.logical_id.unwrap_or(self.id)
730    }
731
732    /// Returns true when the entity carries an explicit logical id on disk.
733    #[inline]
734    pub fn has_explicit_logical_id(&self) -> bool {
735        self.logical_id.is_some()
736    }
737
738    /// Set the stable user-visible identity for this physical version.
739    #[inline]
740    pub fn set_logical_id(&mut self, logical_id: EntityId) {
741        self.logical_id = Some(logical_id);
742    }
743
744    /// Ensure entities written by the current engine carry explicit
745    /// logical identity. Table rows + documents (Phase 1/2) and graph
746    /// nodes/edges (Phase 3) participate in the multi-model MVCC
747    /// versioning rollout and so need a stable logical id for
748    /// version-chain selection. Stamping the logical id is inert for
749    /// non-versioned collections: history only accrues through
750    /// `install_versioned_table_row_update`, which is gated on the
751    /// collection `versioned` flag, so a stamped-but-never-superseded
752    /// entity keeps `logical_id == id` and behaves exactly as before.
753    /// Vectors are intentionally excluded pending their read-path follow
754    /// up (no snapshot-honoring `VECTOR SEARCH`).
755    #[inline]
756    pub fn ensure_table_logical_id(&mut self) {
757        if self.logical_id.is_none()
758            && matches!(
759                self.kind,
760                EntityKind::TableRow { .. } | EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
761            )
762        {
763            self.logical_id = Some(self.id);
764        }
765    }
766
767    /// Create a table row entity
768    pub fn table_row(
769        id: EntityId,
770        table: impl Into<Arc<str>>,
771        row_id: u64,
772        columns: Vec<Value>,
773    ) -> Self {
774        Self::new(
775            id,
776            EntityKind::TableRow {
777                table: table.into(),
778                row_id,
779            },
780            EntityData::Row(RowData::new(columns)),
781        )
782    }
783
784    /// Create a graph node entity
785    pub fn graph_node(
786        id: EntityId,
787        label: impl Into<String>,
788        node_type: impl Into<String>,
789        properties: HashMap<String, Value>,
790    ) -> Self {
791        Self::new(
792            id,
793            EntityKind::GraphNode(Box::new(GraphNodeKind {
794                label: label.into(),
795                node_type: node_type.into(),
796            })),
797            EntityData::Node(NodeData::with_properties(properties)),
798        )
799    }
800
801    /// Create a graph edge entity
802    pub fn graph_edge(
803        id: EntityId,
804        label: impl Into<String>,
805        from: impl Into<String>,
806        to: impl Into<String>,
807        weight: f32,
808        properties: HashMap<String, Value>,
809    ) -> Self {
810        Self::new(
811            id,
812            EntityKind::GraphEdge(Box::new(GraphEdgeKind {
813                label: label.into(),
814                from_node: from.into(),
815                to_node: to.into(),
816                weight: (weight * 1000.0) as u32,
817            })),
818            EntityData::Edge(EdgeData::with_properties(weight, properties)),
819        )
820    }
821
822    /// Create a vector entity
823    pub fn vector(id: EntityId, collection: impl Into<String>, vector: Vec<f32>) -> Self {
824        Self::new(
825            id,
826            EntityKind::Vector {
827                collection: collection.into(),
828            },
829            EntityData::Vector(VectorData::new(vector)),
830        )
831    }
832
833    /// Add an embedding to this entity
834    pub fn add_embedding(&mut self, slot: EmbeddingSlot) {
835        self.embeddings_mut().push(slot);
836        self.touch();
837    }
838
839    /// Add a cross-reference
840    pub fn add_cross_ref(&mut self, cross_ref: CrossRef) {
841        self.cross_refs_mut().push(cross_ref);
842        self.touch();
843    }
844
845    /// Get embedding by slot name
846    pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingSlot> {
847        self.embeddings().iter().find(|e| e.name == name)
848    }
849
850    /// Update timestamp
851    fn touch(&mut self) {
852        self.updated_at = current_unix_secs();
853    }
854
855    /// Check if entity is stale (not updated in given seconds)
856    pub fn is_stale(&self, max_age_secs: u64) -> bool {
857        let now = current_unix_secs();
858        now.saturating_sub(self.updated_at) > max_age_secs
859    }
860}
861
862/// A cross-reference between entities
863#[derive(Debug, Clone, PartialEq)]
864pub struct CrossRef {
865    /// Source entity ID (the entity that holds this reference)
866    pub source: EntityId,
867    /// Target entity ID
868    pub target: EntityId,
869    /// Target collection name
870    pub target_collection: String,
871    /// Type of reference
872    pub ref_type: RefType,
873    /// Reference weight/strength (0.0-1.0)
874    pub weight: f32,
875    /// When this reference was created
876    pub created_at: u64,
877}
878
879impl CrossRef {
880    /// Create a new cross-reference
881    pub fn new(
882        source: EntityId,
883        target: EntityId,
884        target_collection: impl Into<String>,
885        ref_type: RefType,
886    ) -> Self {
887        Self {
888            source,
889            target,
890            target_collection: target_collection.into(),
891            ref_type,
892            weight: 1.0,
893            created_at: current_unix_secs(),
894        }
895    }
896
897    /// Create with weight
898    pub fn with_weight(
899        source: EntityId,
900        target: EntityId,
901        target_collection: impl Into<String>,
902        ref_type: RefType,
903        weight: f32,
904    ) -> Self {
905        let mut cr = Self::new(source, target, target_collection, ref_type);
906        cr.weight = weight;
907        cr
908    }
909}
910
911/// Types of cross-references between entities
912#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
913pub enum RefType {
914    // Table ↔ Graph
915    RowToNode, // Table row represents a graph node
916    RowToEdge, // Table row represents a graph edge
917    NodeToRow, // Node links back to source row
918
919    // Table ↔ Vector
920    RowToVector, // Table row has embeddings
921    VectorToRow, // Vector search → source row
922
923    // Graph ↔ Vector
924    NodeToVector, // Node has embeddings
925    EdgeToVector, // Edge has embeddings
926    VectorToNode, // Vector search → source node
927
928    // Semantic links (discovered)
929    SimilarTo,   // Discovered by vector similarity
930    RelatedTo,   // Domain-specific relationship
931    DerivesFrom, // Data lineage
932    Mentions,    // Text mentions another entity
933    Contains,    // Structural containment
934    DependsOn,   // Dependency relationship
935}
936
937impl RefType {
938    /// Get the inverse reference type (for bidirectional tracking)
939    pub fn inverse(&self) -> Option<Self> {
940        match self {
941            Self::RowToNode => Some(Self::NodeToRow),
942            Self::NodeToRow => Some(Self::RowToNode),
943            Self::RowToVector => Some(Self::VectorToRow),
944            Self::VectorToRow => Some(Self::RowToVector),
945            Self::NodeToVector => Some(Self::VectorToNode),
946            Self::VectorToNode => Some(Self::NodeToVector),
947            Self::SimilarTo => Some(Self::SimilarTo), // Symmetric
948            Self::RelatedTo => Some(Self::RelatedTo), // Symmetric
949            _ => None,                                // One-directional references
950        }
951    }
952
953    /// Check if this is a symmetric reference type
954    pub fn is_symmetric(&self) -> bool {
955        matches!(self, Self::SimilarTo | Self::RelatedTo)
956    }
957
958    /// Convert RefType to byte for binary serialization
959    pub fn to_byte(&self) -> u8 {
960        match self {
961            Self::RowToNode => 0,
962            Self::RowToEdge => 1,
963            Self::NodeToRow => 2,
964            Self::RowToVector => 3,
965            Self::VectorToRow => 4,
966            Self::NodeToVector => 5,
967            Self::EdgeToVector => 6,
968            Self::VectorToNode => 7,
969            Self::SimilarTo => 8,
970            Self::RelatedTo => 9,
971            Self::DerivesFrom => 10,
972            Self::Mentions => 11,
973            Self::Contains => 12,
974            Self::DependsOn => 13,
975        }
976    }
977
978    /// Create RefType from byte (binary deserialization)
979    pub fn from_byte(byte: u8) -> Self {
980        match byte {
981            0 => Self::RowToNode,
982            1 => Self::RowToEdge,
983            2 => Self::NodeToRow,
984            3 => Self::RowToVector,
985            4 => Self::VectorToRow,
986            5 => Self::NodeToVector,
987            6 => Self::EdgeToVector,
988            7 => Self::VectorToNode,
989            8 => Self::SimilarTo,
990            9 => Self::RelatedTo,
991            10 => Self::DerivesFrom,
992            11 => Self::Mentions,
993            12 => Self::Contains,
994            13 => Self::DependsOn,
995            _ => Self::RelatedTo, // Default fallback
996        }
997    }
998}
999
1000/// Convert Vec<Value> to RowData
1001impl From<Vec<Value>> for RowData {
1002    fn from(columns: Vec<Value>) -> Self {
1003        RowData::new(columns)
1004    }
1005}
1006
1007/// Convert HashMap to NodeData
1008impl From<HashMap<String, Value>> for NodeData {
1009    fn from(properties: HashMap<String, Value>) -> Self {
1010        NodeData::with_properties(properties)
1011    }
1012}
1013
1014/// Convert dense vector to VectorData
1015impl From<Vec<f32>> for VectorData {
1016    fn from(dense: Vec<f32>) -> Self {
1017        VectorData::new(dense)
1018    }
1019}
1020
1021/// Convert tuple (dense, sparse) to VectorData
1022impl From<(Vec<f32>, SparseVector)> for VectorData {
1023    fn from((dense, sparse): (Vec<f32>, SparseVector)) -> Self {
1024        VectorData::with_sparse(dense, sparse)
1025    }
1026}
1027
1028// Helper trait for uniform entity creation
1029impl UnifiedEntity {
1030    /// Create a graph node entity from properties map
1031    pub fn from_properties(
1032        id: EntityId,
1033        label: impl Into<String>,
1034        node_type: impl Into<String>,
1035        properties: impl IntoIterator<Item = (impl Into<String>, Value)>,
1036    ) -> Self {
1037        let props: HashMap<String, Value> =
1038            properties.into_iter().map(|(k, v)| (k.into(), v)).collect();
1039        Self::graph_node(id, label, node_type, props)
1040    }
1041
1042    /// Convert entity to row data if applicable
1043    pub fn into_row(self) -> Option<RowData> {
1044        match self.data {
1045            EntityData::Row(r) => Some(r),
1046            _ => None,
1047        }
1048    }
1049
1050    /// Convert entity to node data if applicable
1051    pub fn into_node(self) -> Option<NodeData> {
1052        match self.data {
1053            EntityData::Node(n) => Some(n),
1054            _ => None,
1055        }
1056    }
1057
1058    /// Convert entity to edge data if applicable
1059    pub fn into_edge(self) -> Option<EdgeData> {
1060        match self.data {
1061            EntityData::Edge(e) => Some(e),
1062            _ => None,
1063        }
1064    }
1065
1066    /// Convert entity to vector data if applicable
1067    pub fn into_vector(self) -> Option<VectorData> {
1068        match self.data {
1069            EntityData::Vector(v) => Some(v),
1070            _ => None,
1071        }
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078
1079    #[test]
1080    fn test_entity_creation() {
1081        let id = EntityId::new(1);
1082        let entity = UnifiedEntity::table_row(
1083            id,
1084            "users",
1085            100,
1086            vec![Value::text("alice".to_string()), Value::Integer(25)],
1087        );
1088
1089        assert!(entity.data.is_row());
1090        assert_eq!(entity.kind.storage_type(), "table");
1091        assert_eq!(entity.kind.collection(), "users");
1092    }
1093
1094    #[test]
1095    fn test_cross_refs() {
1096        let id1 = EntityId::new(1);
1097        let id2 = EntityId::new(2);
1098
1099        let cross_ref = CrossRef::new(id1, id2, "nodes", RefType::RowToNode);
1100        assert_eq!(cross_ref.source, id1);
1101        assert_eq!(cross_ref.target, id2);
1102        assert_eq!(cross_ref.ref_type.inverse(), Some(RefType::NodeToRow));
1103    }
1104
1105    #[test]
1106    fn test_sparse_vector() {
1107        let sparse = SparseVector::new(vec![0, 5, 10], vec![1.0, 2.0, 3.0], 100);
1108
1109        assert_eq!(sparse.nnz(), 3);
1110        assert_eq!(sparse.get(5), 2.0);
1111        assert_eq!(sparse.get(3), 0.0);
1112        assert!(sparse.sparsity() > 0.9);
1113    }
1114
1115    #[test]
1116    fn test_embedding_slots() {
1117        let mut entity = UnifiedEntity::table_row(
1118            EntityId::new(1),
1119            "documents",
1120            1,
1121            vec![Value::text("Hello world".to_string())],
1122        );
1123
1124        entity.add_embedding(EmbeddingSlot::new(
1125            "content",
1126            vec![0.1, 0.2, 0.3],
1127            "text-embedding-3-small",
1128        ));
1129
1130        assert_eq!(entity.embeddings().len(), 1);
1131        assert!(entity.get_embedding("content").is_some());
1132        assert!(entity.get_embedding("summary").is_none());
1133    }
1134}