Skip to main content

reddb_server/storage/unified/
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::fmt;
8use std::sync::Arc;
9
10use crate::storage::schema::Value;
11
12/// The first entity-id handed to user-inserted data. Ids `1..FIRST_USER_ENTITY_ID`
13/// are reserved for the internal collection-descriptor and config-default entities
14/// the engine seeds at boot, so the first user-inserted `rid` is a STABLE,
15/// documented value regardless of how many config defaults a build ships.
16///
17/// Before this floor existed the offset drifted upward by one for every config
18/// default added (101 → 114 over time), silently breaking the documented
19/// file-format invariant (#1369). The boot sequence bumps the allocator up to
20/// this floor after seeding internals; it only ever raises the counter, so a
21/// database that already holds user data is untouched. Mirrors
22/// `FIRST_USER_LABEL_ID` in the graph label registry.
23pub const FIRST_USER_ENTITY_ID: u64 = 1024;
24
25/// Unique identifier for any entity in the unified storage
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct EntityId(pub u64);
28
29impl EntityId {
30    /// Create a new entity ID
31    pub fn new(id: u64) -> Self {
32        Self(id)
33    }
34
35    /// Get the raw ID value
36    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/// The kind of entity (what storage type it belongs to)
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum EntityKind {
56    /// A row in a structured table (hot path — kept inline for cache performance)
57    TableRow { table: Arc<str>, row_id: u64 },
58    /// A node in the graph (boxed — saves ~56 bytes per entity for table rows)
59    GraphNode(Box<GraphNodeKind>),
60    /// An edge in the graph (boxed)
61    GraphEdge(Box<GraphEdgeKind>),
62    /// A vector in a collection
63    Vector { collection: String },
64    /// A time-series data point (boxed)
65    TimeSeriesPoint(Box<TimeSeriesPointKind>),
66    /// A queue message
67    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    /// Get the storage type as a string
92    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    /// Get the collection/table name
104    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/// The actual data content of an entity
117#[derive(Debug, Clone)]
118pub enum EntityData {
119    /// Table row data
120    Row(RowData),
121    /// Graph node data
122    Node(NodeData),
123    /// Graph edge data
124    Edge(EdgeData),
125    /// Vector data
126    Vector(VectorData),
127    /// Time-series data point
128    TimeSeries(TimeSeriesData),
129    /// Queue message data
130    QueueMessage(QueueMessageData),
131}
132
133impl EntityData {
134    /// Check if this is row data
135    pub fn is_row(&self) -> bool {
136        matches!(self, Self::Row(_))
137    }
138
139    /// Check if this is node data
140    pub fn is_node(&self) -> bool {
141        matches!(self, Self::Node(_))
142    }
143
144    /// Check if this is edge data
145    pub fn is_edge(&self) -> bool {
146        matches!(self, Self::Edge(_))
147    }
148
149    /// Check if this is vector data
150    pub fn is_vector(&self) -> bool {
151        matches!(self, Self::Vector(_))
152    }
153
154    /// Get as row data
155    pub fn as_row(&self) -> Option<&RowData> {
156        match self {
157            Self::Row(r) => Some(r),
158            _ => None,
159        }
160    }
161
162    /// Get as node data
163    pub fn as_node(&self) -> Option<&NodeData> {
164        match self {
165            Self::Node(n) => Some(n),
166            _ => None,
167        }
168    }
169
170    /// Get as edge data
171    pub fn as_edge(&self) -> Option<&EdgeData> {
172        match self {
173            Self::Edge(e) => Some(e),
174            _ => None,
175        }
176    }
177
178    /// Get as vector data
179    pub fn as_vector(&self) -> Option<&VectorData> {
180        match self {
181            Self::Vector(v) => Some(v),
182            _ => None,
183        }
184    }
185}
186
187/// Data for a table row
188#[derive(Debug, Clone)]
189pub struct RowData {
190    /// Column values in schema order
191    pub columns: Vec<Value>,
192    /// Named column access (optional, for convenience)
193    pub named: Option<HashMap<String, Value>>,
194    /// Shared column schema: column names in order (maps index → name).
195    /// When set, `columns` holds the values and `named` is None.
196    /// This saves ~60% memory vs per-row HashMap.
197    pub schema: Option<std::sync::Arc<Vec<String>>>,
198}
199
200impl RowData {
201    /// Create new row data from column values
202    pub fn new(columns: Vec<Value>) -> Self {
203        Self {
204            columns,
205            named: None,
206            schema: None,
207        }
208    }
209
210    /// Create row data with named columns
211    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    /// Get a named field value — checks named HashMap first, then schema+columns.
222    pub fn get_field(&self, name: &str) -> Option<&Value> {
223        // Fast path: named HashMap
224        if let Some(ref named) = self.named {
225            return named.get(name);
226        }
227        // Columnar path: use schema to find index
228        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    /// Iterate over all (name, value) pairs — works for both named and columnar.
237    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    /// Get column by index
253    pub fn get(&self, index: usize) -> Option<&Value> {
254        self.columns.get(index)
255    }
256
257    /// Get column by name
258    pub fn get_by_name(&self, name: &str) -> Option<&Value> {
259        self.named.as_ref()?.get(name)
260    }
261
262    /// Number of columns
263    pub fn len(&self) -> usize {
264        self.columns.len()
265    }
266
267    /// Check if empty
268    pub fn is_empty(&self) -> bool {
269        self.columns.is_empty()
270    }
271}
272
273/// Data for a graph node
274#[derive(Debug, Clone)]
275pub struct NodeData {
276    /// Node properties
277    pub properties: HashMap<String, Value>,
278}
279
280impl NodeData {
281    /// Create new node data
282    pub fn new() -> Self {
283        Self {
284            properties: HashMap::new(),
285        }
286    }
287
288    /// Create with properties
289    pub fn with_properties(properties: HashMap<String, Value>) -> Self {
290        Self { properties }
291    }
292
293    /// Set a property
294    pub fn set(&mut self, key: impl Into<String>, value: Value) {
295        self.properties.insert(key.into(), value);
296    }
297
298    /// Get a property
299    pub fn get(&self, key: &str) -> Option<&Value> {
300        self.properties.get(key)
301    }
302
303    /// Check if property exists
304    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/// Data for a graph edge
316#[derive(Debug, Clone)]
317pub struct EdgeData {
318    /// Edge properties
319    pub properties: HashMap<String, Value>,
320    /// Edge weight (for weighted graphs)
321    pub weight: f32,
322}
323
324impl EdgeData {
325    /// Create new edge data
326    pub fn new(weight: f32) -> Self {
327        Self {
328            properties: HashMap::new(),
329            weight,
330        }
331    }
332
333    /// Create with properties
334    pub fn with_properties(weight: f32, properties: HashMap<String, Value>) -> Self {
335        Self { properties, weight }
336    }
337
338    /// Set a property
339    pub fn set(&mut self, key: impl Into<String>, value: Value) {
340        self.properties.insert(key.into(), value);
341    }
342
343    /// Get a property
344    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/// Data for a vector
356#[derive(Debug, Clone)]
357pub struct VectorData {
358    /// Dense vector (primary embedding)
359    pub dense: Vec<f32>,
360    /// Optional sparse vector
361    pub sparse: Option<SparseVector>,
362    /// Original content (if applicable)
363    pub content: Option<String>,
364}
365
366impl VectorData {
367    /// Create new vector data from dense vector
368    pub fn new(dense: Vec<f32>) -> Self {
369        Self {
370            dense,
371            sparse: None,
372            content: None,
373        }
374    }
375
376    /// Create with sparse vector
377    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    /// Set content
386    pub fn with_content(mut self, content: impl Into<String>) -> Self {
387        self.content = Some(content.into());
388        self
389    }
390
391    /// Get dimension
392    pub fn dimension(&self) -> usize {
393        self.dense.len()
394    }
395
396    /// Check if has sparse component
397    pub fn is_hybrid(&self) -> bool {
398        self.sparse.is_some()
399    }
400}
401
402/// Time-series data point
403#[derive(Debug, Clone)]
404pub struct TimeSeriesData {
405    /// Metric name (e.g., "cpu.idle")
406    pub metric: String,
407    /// Interned `(metric, canonical tags)` id for native time-series points.
408    pub series_id: Option<u64>,
409    /// Timestamp in nanoseconds since epoch
410    pub timestamp_ns: u64,
411    /// Metric value
412    pub value: f64,
413    /// Legacy/read-through dimensional tags (e.g., {"host": "srv1"}).
414    pub tags: std::collections::HashMap<String, String>,
415    /// Per-point fields that are not part of the immutable series identity.
416    pub fields: std::collections::HashMap<String, Value>,
417}
418
419/// Queue message data
420#[derive(Debug, Clone)]
421pub struct QueueMessageData {
422    /// Message payload
423    pub payload: Value,
424    /// Optional priority (higher = more urgent)
425    pub priority: Option<i32>,
426    /// Enqueue timestamp (nanoseconds)
427    pub enqueued_at_ns: u64,
428    /// Number of delivery attempts
429    pub attempts: u32,
430    /// Maximum delivery attempts before DLQ
431    pub max_attempts: u32,
432    /// Whether the message has been acknowledged
433    pub acked: bool,
434}
435
436/// Sparse vector representation
437#[derive(Debug, Clone)]
438pub struct SparseVector {
439    /// Indices of non-zero elements
440    pub indices: Vec<u32>,
441    /// Values at those indices
442    pub values: Vec<f32>,
443    /// Total dimension (may be larger than indices.len())
444    pub dimension: usize,
445}
446
447impl SparseVector {
448    /// Create new sparse vector
449    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    /// Number of non-zero elements
459    pub fn nnz(&self) -> usize {
460        self.indices.len()
461    }
462
463    /// Sparsity ratio
464    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    /// Get value at index (0 if not present)
473    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/// A slot for embedding a specific aspect of an entity
483#[derive(Debug, Clone)]
484pub struct EmbeddingSlot {
485    /// Slot name (e.g., "content", "summary", "title", "code")
486    pub name: String,
487    /// The embedding vector
488    pub vector: Vec<f32>,
489    /// Model used to generate embedding
490    pub model: String,
491    /// Vector dimension
492    pub dimension: usize,
493    /// Generation timestamp
494    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    /// Create a new embedding slot
506    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/// A unified entity that can represent any storage type
519#[derive(Debug, Clone)]
520pub struct UnifiedEntity {
521    /// Unique entity identifier
522    pub id: EntityId,
523    /// Stable user-visible identity shared by all physical versions.
524    ///
525    /// `None` is the legacy encoding and resolves to `id`.
526    logical_id: Option<EntityId>,
527    /// What kind of entity this is
528    pub kind: EntityKind,
529    /// Creation timestamp
530    pub created_at: u64,
531    /// Last update timestamp
532    pub updated_at: u64,
533    /// The actual data content
534    pub data: EntityData,
535    /// Sequence ID for ordering/versioning
536    pub sequence_id: u64,
537    /// Field-name bloom filter (u64, zero-allocation).
538    ///
539    /// Each bit encodes one possible mid-character value: for field name `n`
540    /// the bit position is `n.as_bytes()[n.len()/2] & 63`. OR of all user
541    /// field names present in this entity. Cleared for schema-based bulk rows
542    /// (all rows share the same schema so bloom is segment-level).
543    ///
544    /// The compiled filter computes `required_bloom` from predicate field names
545    /// at compile time. If `entity.field_bloom & required_bloom != required_bloom`,
546    /// the entity cannot match and is skipped before any HashMap probe.
547    pub field_bloom: u64,
548    /// MVCC creation transaction ID (Phase 2.3 PG parity).
549    ///
550    /// `0` means "pre-MVCC" / auto-commit — visible to every snapshot. When
551    /// a BEGIN-wrapped INSERT runs, it stamps `xmin` with the transaction's
552    /// snapshot id so other concurrent transactions only see the row after
553    /// the writer commits (snapshot isolation semantics).
554    ///
555    /// Visibility rule: `xmin <= snapshot.xid && (xmax == 0 || xmax > snapshot.xid)`.
556    pub xmin: u64,
557    /// MVCC deletion transaction ID (Phase 2.3 PG parity).
558    ///
559    /// `0` means "live". Set to the deleting transaction's snapshot id on
560    /// DELETE/UPDATE (row is kept until VACUUM reclaims it). Snapshots with
561    /// `xid < xmax` still see the row; newer snapshots skip it.
562    pub xmax: u64,
563    /// Optional auxiliary data (embeddings, cross-refs).
564    /// None for most table rows — saves 40 bytes/entity.
565    aux: Option<Box<EntityAux>>,
566}
567
568/// Auxiliary entity data — only allocated when needed.
569#[derive(Debug, Clone, Default)]
570pub struct EntityAux {
571    /// Embedding slots (for multi-vector support)
572    pub embeddings: Vec<EmbeddingSlot>,
573    /// Cross-references to other entities
574    pub cross_refs: Vec<CrossRef>,
575}
576
577impl UnifiedEntity {
578    /// Access embeddings (returns empty slice if no aux data).
579    pub fn embeddings(&self) -> &[EmbeddingSlot] {
580        self.aux
581            .as_ref()
582            .map(|a| a.embeddings.as_slice())
583            .unwrap_or(&[])
584    }
585
586    /// Access cross-references (returns empty slice if no aux data).
587    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    /// Get mutable embeddings (allocates aux if needed).
595    pub fn embeddings_mut(&mut self) -> &mut Vec<EmbeddingSlot> {
596        &mut self.aux.get_or_insert_with(Default::default).embeddings
597    }
598
599    /// Get mutable cross-refs (allocates aux if needed).
600    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    /// Check if entity has any auxiliary data.
605    pub fn has_aux(&self) -> bool {
606        self.aux.is_some()
607    }
608}
609
610/// Compute one bit of a field-name bloom filter.
611///
612/// Uses the mid-character trick from MongoDB's field-name bloom:
613/// the bit position is the mid-byte value clamped to 0..63. Zero-allocation,
614/// ~1.5% false-positive rate for ≤5 distinct field names.
615#[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
624/// Compute the combined field-name bloom for all user-level fields in `data`.
625/// Returns 0 for schema-based rows (all rows share the same schema, so the
626/// per-entity bloom would be identical — caller uses a segment-level bloom).
627pub fn compute_entity_field_bloom(data: &EntityData) -> u64 {
628    match data {
629        EntityData::Row(row) => {
630            if row.schema.is_some() {
631                // Schema path: bloom is identical for every row in this table.
632                // Don't store per-entity — use segment-level bloom instead.
633                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                // Single-source documents no longer materialise promoted
638                // columns, so the body's top-level keys are the only place a
639                // `WHERE`/projection field lives. Fold them into the bloom so
640                // the field-bloom gate doesn't reject a row before the body
641                // read fallback ever runs. Inert for non-document rows (no
642                // binary-container `body` field) — `container_field_names`
643                // self-gates on the RDOC magic.
644                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        // Vectors, time-series, queue: no user-named fields worth blooming.
665        _ => 0,
666    }
667}
668
669impl UnifiedEntity {
670    /// Create a new unified entity
671    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            // Pre-MVCC default: xmin/xmax = 0 means visible to every snapshot.
685            // Transactional writers stamp real snapshot IDs after allocation.
686            xmin: 0,
687            xmax: 0,
688            aux: None,
689        }
690    }
691
692    /// MVCC visibility check (Phase 2.3 PG parity).
693    ///
694    /// Returns `true` when this tuple is visible under the provided
695    /// snapshot xid. Pre-MVCC rows (`xmin == 0`, `xmax == 0`) are visible
696    /// to every snapshot — preserves full compatibility with existing
697    /// data inserted before the MVCC headers existed.
698    ///
699    /// Snapshot isolation rule:
700    ///   - `xmin == 0 || xmin <= snapshot_xid`  (creator committed before snapshot)
701    ///   - `xmax == 0 || xmax > snapshot_xid`   (deleter committed after snapshot)
702    #[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    /// Stamp `xmin` (creation transaction ID). Called by the runtime on
714    /// INSERT inside an active transaction.
715    #[inline]
716    pub fn set_xmin(&mut self, xid: u64) {
717        self.xmin = xid;
718    }
719
720    /// Stamp `xmax` (deletion transaction ID). Called by the runtime on
721    /// DELETE/UPDATE inside an active transaction — the tuple survives
722    /// until VACUUM reclaims it.
723    #[inline]
724    pub fn set_xmax(&mut self, xid: u64) {
725        self.xmax = xid;
726    }
727
728    /// Stable user-visible identity. Legacy rows without an explicit
729    /// logical id map to their physical entity id.
730    #[inline]
731    pub fn logical_id(&self) -> EntityId {
732        self.logical_id.unwrap_or(self.id)
733    }
734
735    /// Returns true when the entity carries an explicit logical id on disk.
736    #[inline]
737    pub fn has_explicit_logical_id(&self) -> bool {
738        self.logical_id.is_some()
739    }
740
741    /// Set the stable user-visible identity for this physical version.
742    #[inline]
743    pub fn set_logical_id(&mut self, logical_id: EntityId) {
744        self.logical_id = Some(logical_id);
745    }
746
747    /// Ensure entities written by the current engine carry explicit
748    /// logical identity. Table rows + documents (Phase 1/2) and graph
749    /// nodes/edges (Phase 3) participate in the multi-model MVCC
750    /// versioning rollout and so need a stable logical id for
751    /// version-chain selection. Stamping the logical id is inert for
752    /// non-versioned collections: history only accrues through
753    /// `install_versioned_table_row_update`, which is gated on the
754    /// collection `versioned` flag, so a stamped-but-never-superseded
755    /// entity keeps `logical_id == id` and behaves exactly as before.
756    /// Vectors are intentionally excluded pending their read-path follow
757    /// up (no snapshot-honoring `VECTOR SEARCH`).
758    #[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    /// Create a table row entity
771    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    /// Create a graph node entity
788    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    /// Create a graph edge entity
805    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    /// Create a vector entity
826    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    /// Add an embedding to this entity
837    pub fn add_embedding(&mut self, slot: EmbeddingSlot) {
838        self.embeddings_mut().push(slot);
839        self.touch();
840    }
841
842    /// Add a cross-reference
843    pub fn add_cross_ref(&mut self, cross_ref: CrossRef) {
844        self.cross_refs_mut().push(cross_ref);
845        self.touch();
846    }
847
848    /// Get embedding by slot name
849    pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingSlot> {
850        self.embeddings().iter().find(|e| e.name == name)
851    }
852
853    /// Update timestamp
854    fn touch(&mut self) {
855        self.updated_at = current_unix_secs();
856    }
857
858    /// Check if entity is stale (not updated in given seconds)
859    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/// A cross-reference between entities
866#[derive(Debug, Clone, PartialEq)]
867pub struct CrossRef {
868    /// Source entity ID (the entity that holds this reference)
869    pub source: EntityId,
870    /// Target entity ID
871    pub target: EntityId,
872    /// Target collection name
873    pub target_collection: String,
874    /// Type of reference
875    pub ref_type: RefType,
876    /// Reference weight/strength (0.0-1.0)
877    pub weight: f32,
878    /// When this reference was created
879    pub created_at: u64,
880}
881
882impl CrossRef {
883    /// Create a new cross-reference
884    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    /// Create with weight
901    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/// Types of cross-references between entities
915#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
916pub enum RefType {
917    // Table ↔ Graph
918    RowToNode, // Table row represents a graph node
919    RowToEdge, // Table row represents a graph edge
920    NodeToRow, // Node links back to source row
921
922    // Table ↔ Vector
923    RowToVector, // Table row has embeddings
924    VectorToRow, // Vector search → source row
925
926    // Graph ↔ Vector
927    NodeToVector, // Node has embeddings
928    EdgeToVector, // Edge has embeddings
929    VectorToNode, // Vector search → source node
930
931    // Semantic links (discovered)
932    SimilarTo,   // Discovered by vector similarity
933    RelatedTo,   // Domain-specific relationship
934    DerivesFrom, // Data lineage
935    Mentions,    // Text mentions another entity
936    Contains,    // Structural containment
937    DependsOn,   // Dependency relationship
938}
939
940impl RefType {
941    /// Get the inverse reference type (for bidirectional tracking)
942    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), // Symmetric
951            Self::RelatedTo => Some(Self::RelatedTo), // Symmetric
952            _ => None,                                // One-directional references
953        }
954    }
955
956    /// Check if this is a symmetric reference type
957    pub fn is_symmetric(&self) -> bool {
958        matches!(self, Self::SimilarTo | Self::RelatedTo)
959    }
960
961    /// Convert RefType to byte for binary serialization
962    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    /// Create RefType from byte (binary deserialization)
982    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, // Default fallback
999        }
1000    }
1001}
1002
1003/// Convert Vec<Value> to RowData
1004impl From<Vec<Value>> for RowData {
1005    fn from(columns: Vec<Value>) -> Self {
1006        RowData::new(columns)
1007    }
1008}
1009
1010/// Convert HashMap to NodeData
1011impl From<HashMap<String, Value>> for NodeData {
1012    fn from(properties: HashMap<String, Value>) -> Self {
1013        NodeData::with_properties(properties)
1014    }
1015}
1016
1017/// Convert dense vector to VectorData
1018impl From<Vec<f32>> for VectorData {
1019    fn from(dense: Vec<f32>) -> Self {
1020        VectorData::new(dense)
1021    }
1022}
1023
1024/// Convert tuple (dense, sparse) to VectorData
1025impl From<(Vec<f32>, SparseVector)> for VectorData {
1026    fn from((dense, sparse): (Vec<f32>, SparseVector)) -> Self {
1027        VectorData::with_sparse(dense, sparse)
1028    }
1029}
1030
1031// Helper trait for uniform entity creation
1032impl UnifiedEntity {
1033    /// Create a graph node entity from properties map
1034    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    /// Convert entity to row data if applicable
1046    pub fn into_row(self) -> Option<RowData> {
1047        match self.data {
1048            EntityData::Row(r) => Some(r),
1049            _ => None,
1050        }
1051    }
1052
1053    /// Convert entity to node data if applicable
1054    pub fn into_node(self) -> Option<NodeData> {
1055        match self.data {
1056            EntityData::Node(n) => Some(n),
1057            _ => None,
1058        }
1059    }
1060
1061    /// Convert entity to edge data if applicable
1062    pub fn into_edge(self) -> Option<EdgeData> {
1063        match self.data {
1064            EntityData::Edge(e) => Some(e),
1065            _ => None,
1066        }
1067    }
1068
1069    /// Convert entity to vector data if applicable
1070    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}