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, PartialEq)]
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, PartialEq)]
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    #[cfg(feature = "full")]
197    pub(crate) fn compact_with_schema(&mut self, schema: &std::sync::Arc<Vec<String>>) -> bool {
198        let Some(named) = &self.named else {
199            return false;
200        };
201        if named.len() != schema.len() || schema.iter().any(|name| !named.contains_key(name)) {
202            return false;
203        }
204        // Existing positional values are independently observable. Only drop
205        // their duplicate copy when the declared order agrees with them.
206        if !self.columns.is_empty()
207            && (self.columns.len() != schema.len()
208                || schema
209                    .iter()
210                    .zip(&self.columns)
211                    .any(|(name, value)| named.get(name) != Some(value)))
212        {
213            return false;
214        }
215        let mut named = self.named.take().expect("validated named row");
216        self.columns = schema
217            .iter()
218            .map(|name| named.remove(name).expect("validated schema field"))
219            .collect();
220        self.schema = Some(std::sync::Arc::clone(schema));
221        true
222    }
223
224    /// Get a named field value — checks named HashMap first, then schema+columns.
225    pub fn get_field(&self, name: &str) -> Option<&Value> {
226        // Fast path: named HashMap
227        if let Some(ref named) = self.named {
228            return named.get(name);
229        }
230        // Columnar path: use schema to find index
231        if let Some(ref schema) = self.schema {
232            if let Some(idx) = schema.iter().position(|s| s == name) {
233                return self.columns.get(idx);
234            }
235        }
236        None
237    }
238
239    /// Iterate over all (name, value) pairs — works for both named and columnar.
240    pub fn iter_fields(&self) -> Box<dyn Iterator<Item = (&str, &Value)> + '_> {
241        if let Some(ref named) = self.named {
242            Box::new(named.iter().map(|(k, v)| (k.as_str(), v)))
243        } else if let Some(ref schema) = self.schema {
244            Box::new(
245                schema
246                    .iter()
247                    .zip(self.columns.iter())
248                    .map(|(k, v)| (k.as_str(), v)),
249            )
250        } else {
251            Box::new(std::iter::empty())
252        }
253    }
254
255    /// Get column by index
256    pub fn get(&self, index: usize) -> Option<&Value> {
257        self.columns.get(index)
258    }
259
260    /// Get column by name
261    pub fn get_by_name(&self, name: &str) -> Option<&Value> {
262        self.named.as_ref()?.get(name)
263    }
264
265    /// Number of columns
266    pub fn len(&self) -> usize {
267        self.columns.len()
268    }
269
270    /// Check if empty
271    pub fn is_empty(&self) -> bool {
272        self.columns.is_empty()
273    }
274}
275
276/// Data for a graph node
277#[derive(Debug, Clone, PartialEq)]
278pub struct NodeData {
279    /// Node properties
280    pub properties: HashMap<String, Value>,
281}
282
283impl NodeData {
284    /// Create new node data
285    pub fn new() -> Self {
286        Self {
287            properties: HashMap::new(),
288        }
289    }
290
291    /// Create with properties
292    pub fn with_properties(properties: HashMap<String, Value>) -> Self {
293        Self { properties }
294    }
295
296    /// Set a property
297    pub fn set(&mut self, key: impl Into<String>, value: Value) {
298        self.properties.insert(key.into(), value);
299    }
300
301    /// Get a property
302    pub fn get(&self, key: &str) -> Option<&Value> {
303        self.properties.get(key)
304    }
305
306    /// Check if property exists
307    pub fn has(&self, key: &str) -> bool {
308        self.properties.contains_key(key)
309    }
310}
311
312impl Default for NodeData {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317
318/// Data for a graph edge
319#[derive(Debug, Clone, PartialEq)]
320pub struct EdgeData {
321    /// Edge properties
322    pub properties: HashMap<String, Value>,
323    /// Edge weight (for weighted graphs)
324    pub weight: f32,
325}
326
327impl EdgeData {
328    /// Create new edge data
329    pub fn new(weight: f32) -> Self {
330        Self {
331            properties: HashMap::new(),
332            weight,
333        }
334    }
335
336    /// Create with properties
337    pub fn with_properties(weight: f32, properties: HashMap<String, Value>) -> Self {
338        Self { properties, weight }
339    }
340
341    /// Set a property
342    pub fn set(&mut self, key: impl Into<String>, value: Value) {
343        self.properties.insert(key.into(), value);
344    }
345
346    /// Get a property
347    pub fn get(&self, key: &str) -> Option<&Value> {
348        self.properties.get(key)
349    }
350}
351
352impl Default for EdgeData {
353    fn default() -> Self {
354        Self::new(1.0)
355    }
356}
357
358/// Data for a vector
359#[derive(Debug, Clone, PartialEq)]
360pub struct VectorData {
361    /// Dense vector (primary embedding)
362    pub dense: Vec<f32>,
363    /// Optional sparse vector
364    pub sparse: Option<SparseVector>,
365    /// Original content (if applicable)
366    pub content: Option<String>,
367}
368
369impl VectorData {
370    /// Create new vector data from dense vector
371    pub fn new(dense: Vec<f32>) -> Self {
372        Self {
373            dense,
374            sparse: None,
375            content: None,
376        }
377    }
378
379    /// Create with sparse vector
380    pub fn with_sparse(dense: Vec<f32>, sparse: SparseVector) -> Self {
381        Self {
382            dense,
383            sparse: Some(sparse),
384            content: None,
385        }
386    }
387
388    /// Set content
389    pub fn with_content(mut self, content: impl Into<String>) -> Self {
390        self.content = Some(content.into());
391        self
392    }
393
394    /// Get dimension
395    pub fn dimension(&self) -> usize {
396        self.dense.len()
397    }
398
399    /// Check if has sparse component
400    pub fn is_hybrid(&self) -> bool {
401        self.sparse.is_some()
402    }
403}
404
405/// Time-series data point
406#[derive(Debug, Clone, PartialEq)]
407pub struct TimeSeriesData {
408    /// Metric name (e.g., "cpu.idle")
409    pub metric: String,
410    /// Interned `(metric, canonical tags)` id for native time-series points.
411    pub series_id: Option<u64>,
412    /// Timestamp in nanoseconds since epoch
413    pub timestamp_ns: u64,
414    /// Metric value
415    pub value: f64,
416    /// Legacy/read-through dimensional tags (e.g., {"host": "srv1"}).
417    pub tags: std::collections::HashMap<String, String>,
418    /// Per-point fields that are not part of the immutable series identity.
419    pub fields: std::collections::HashMap<String, Value>,
420}
421
422/// Queue message data
423#[derive(Debug, Clone, PartialEq)]
424pub struct QueueMessageData {
425    /// Message payload
426    pub payload: Value,
427    /// Optional priority (higher = more urgent)
428    pub priority: Option<i32>,
429    /// Enqueue timestamp (nanoseconds)
430    pub enqueued_at_ns: u64,
431    /// Number of delivery attempts
432    pub attempts: u32,
433    /// Maximum delivery attempts before DLQ
434    pub max_attempts: u32,
435    /// Whether the message has been acknowledged
436    pub acked: bool,
437}
438
439/// Sparse vector representation
440#[derive(Debug, Clone, PartialEq)]
441pub struct SparseVector {
442    /// Indices of non-zero elements
443    pub indices: Vec<u32>,
444    /// Values at those indices
445    pub values: Vec<f32>,
446    /// Total dimension (may be larger than indices.len())
447    pub dimension: usize,
448}
449
450impl SparseVector {
451    /// Create new sparse vector
452    pub fn new(indices: Vec<u32>, values: Vec<f32>, dimension: usize) -> Self {
453        debug_assert_eq!(indices.len(), values.len());
454        Self {
455            indices,
456            values,
457            dimension,
458        }
459    }
460
461    /// Number of non-zero elements
462    pub fn nnz(&self) -> usize {
463        self.indices.len()
464    }
465
466    /// Sparsity ratio
467    pub fn sparsity(&self) -> f32 {
468        if self.dimension == 0 {
469            1.0
470        } else {
471            1.0 - (self.nnz() as f32 / self.dimension as f32)
472        }
473    }
474
475    /// Get value at index (0 if not present)
476    pub fn get(&self, index: u32) -> f32 {
477        self.indices
478            .iter()
479            .position(|&i| i == index)
480            .map(|pos| self.values[pos])
481            .unwrap_or(0.0)
482    }
483}
484
485/// A slot for embedding a specific aspect of an entity
486#[derive(Debug, Clone, PartialEq)]
487pub struct EmbeddingSlot {
488    /// Slot name (e.g., "content", "summary", "title", "code")
489    pub name: String,
490    /// The embedding vector
491    pub vector: Vec<f32>,
492    /// Model used to generate embedding
493    pub model: String,
494    /// Vector dimension
495    pub dimension: usize,
496    /// Generation timestamp
497    pub generated_at: u64,
498}
499
500use crate::clock::current_unix_secs;
501
502impl EmbeddingSlot {
503    /// Create a new embedding slot
504    pub fn new(name: impl Into<String>, vector: Vec<f32>, model: impl Into<String>) -> Self {
505        let dimension = vector.len();
506        Self {
507            name: name.into(),
508            vector,
509            model: model.into(),
510            dimension,
511            generated_at: current_unix_secs(),
512        }
513    }
514}
515
516/// A unified entity that can represent any storage type
517#[derive(Debug, Clone)]
518pub struct UnifiedEntity {
519    /// Unique entity identifier
520    pub id: EntityId,
521    /// Stable user-visible identity shared by all physical versions.
522    ///
523    /// `None` is the legacy encoding and resolves to `id`.
524    logical_id: Option<EntityId>,
525    /// What kind of entity this is
526    pub kind: EntityKind,
527    /// Creation timestamp
528    pub created_at: u64,
529    /// Last update timestamp
530    pub updated_at: u64,
531    /// The actual data content
532    pub data: EntityData,
533    /// Sequence ID for ordering/versioning
534    pub sequence_id: u64,
535    /// Field-name bloom filter (u64, zero-allocation).
536    ///
537    /// Each bit encodes one possible mid-character value: for field name `n`
538    /// the bit position is `n.as_bytes()[n.len()/2] & 63`. OR of all user
539    /// field names present in this entity. Cleared for schema-based bulk rows
540    /// (all rows share the same schema so bloom is segment-level).
541    ///
542    /// The compiled filter computes `required_bloom` from predicate field names
543    /// at compile time. If `entity.field_bloom & required_bloom != required_bloom`,
544    /// the entity cannot match and is skipped before any HashMap probe.
545    pub field_bloom: u64,
546    /// MVCC creation transaction ID (Phase 2.3 PG parity).
547    ///
548    /// `0` means "pre-MVCC" / auto-commit — visible to every snapshot. When
549    /// a BEGIN-wrapped INSERT runs, it stamps `xmin` with the transaction's
550    /// snapshot id so other concurrent transactions only see the row after
551    /// the writer commits (snapshot isolation semantics).
552    ///
553    /// Visibility rule: `xmin <= snapshot.xid && (xmax == 0 || xmax > snapshot.xid)`.
554    pub xmin: u64,
555    /// MVCC deletion transaction ID (Phase 2.3 PG parity).
556    ///
557    /// `0` means "live". Set to the deleting transaction's snapshot id on
558    /// DELETE/UPDATE (row is kept until VACUUM reclaims it). Snapshots with
559    /// `xid < xmax` still see the row; newer snapshots skip it.
560    pub xmax: u64,
561    /// Optional auxiliary data (embeddings, cross-refs).
562    /// None for most table rows — saves 40 bytes/entity.
563    aux: Option<Box<EntityAux>>,
564}
565
566/// Auxiliary entity data — only allocated when needed.
567#[derive(Debug, Clone, Default)]
568pub struct EntityAux {
569    /// Embedding slots (for multi-vector support)
570    pub embeddings: Vec<EmbeddingSlot>,
571    /// Cross-references to other entities
572    pub cross_refs: Vec<CrossRef>,
573    /// VCS provenance is allocated only for opted-in entities. Epochs are
574    /// immutable branch incarnations; recreating a branch uses a new epoch.
575    pub branch: Option<BranchVersion>,
576}
577
578#[derive(Debug, Clone, Default, PartialEq, Eq)]
579pub struct BranchVersion {
580    pub origin: u64,
581    /// Deletions by other epochs must not overwrite the creator's xmax.
582    pub deletions: Vec<(u64, u64)>,
583}
584
585impl UnifiedEntity {
586    /// Normalize persisted runtime-owned collection identities while retaining
587    /// exact user collection names. This is idempotent and intentionally does
588    /// not rewrite graph labels, which are data rather than collection keys.
589    pub fn canonicalize_system_collection_names(&mut self) {
590        match &mut self.kind {
591            EntityKind::TableRow { table, .. } => {
592                if let Some(canonical) = super::system_collection_names::canonical_name(table) {
593                    *table = Arc::from(canonical);
594                }
595            }
596            EntityKind::Vector { collection } => {
597                if let Some(canonical) = super::system_collection_names::canonical_name(collection)
598                {
599                    *collection = canonical.to_owned();
600                }
601            }
602            EntityKind::TimeSeriesPoint(point) => {
603                if let Some(canonical) =
604                    super::system_collection_names::canonical_name(&point.series)
605                {
606                    point.series = canonical.to_owned();
607                }
608            }
609            EntityKind::QueueMessage { queue, .. } => {
610                if let Some(canonical) = super::system_collection_names::canonical_name(queue) {
611                    *queue = canonical.to_owned();
612                }
613            }
614            EntityKind::GraphNode(_) | EntityKind::GraphEdge(_) => {}
615        }
616        if let Some(aux) = &mut self.aux {
617            for reference in &mut aux.cross_refs {
618                if let Some(canonical) =
619                    super::system_collection_names::canonical_name(&reference.target_collection)
620                {
621                    reference.target_collection = canonical.to_owned();
622                }
623            }
624        }
625    }
626
627    /// Access embeddings (returns empty slice if no aux data).
628    pub fn embeddings(&self) -> &[EmbeddingSlot] {
629        self.aux
630            .as_ref()
631            .map(|a| a.embeddings.as_slice())
632            .unwrap_or(&[])
633    }
634
635    /// Access cross-references (returns empty slice if no aux data).
636    pub fn cross_refs(&self) -> &[CrossRef] {
637        self.aux
638            .as_ref()
639            .map(|a| a.cross_refs.as_slice())
640            .unwrap_or(&[])
641    }
642
643    /// Get mutable embeddings (allocates aux if needed).
644    pub fn embeddings_mut(&mut self) -> &mut Vec<EmbeddingSlot> {
645        &mut self.aux.get_or_insert_with(Default::default).embeddings
646    }
647
648    /// Get mutable cross-refs (allocates aux if needed).
649    pub fn cross_refs_mut(&mut self) -> &mut Vec<CrossRef> {
650        &mut self.aux.get_or_insert_with(Default::default).cross_refs
651    }
652
653    /// Check if entity has any auxiliary data.
654    pub fn has_aux(&self) -> bool {
655        self.aux.is_some()
656    }
657}
658
659/// Compute one bit of a field-name bloom filter.
660///
661/// Uses the mid-character trick from MongoDB's field-name bloom:
662/// the bit position is the mid-byte value clamped to 0..63. Zero-allocation,
663/// ~1.5% false-positive rate for ≤5 distinct field names.
664#[inline]
665pub fn field_name_bloom(name: &str) -> u64 {
666    let b = name.as_bytes();
667    if b.is_empty() {
668        return 0;
669    }
670    1u64 << (b[b.len() / 2] & 63)
671}
672
673/// Compute the combined field-name bloom for all user-level fields in `data`.
674/// Returns 0 for schema-based rows (all rows share the same schema, so the
675/// per-entity bloom would be identical — caller uses a segment-level bloom).
676pub fn compute_entity_field_bloom(data: &EntityData) -> u64 {
677    match data {
678        EntityData::Row(row) => {
679            if row.schema.is_some() {
680                // Schema path: bloom is identical for every row in this table.
681                // Don't store per-entity — use segment-level bloom instead.
682                return 0;
683            }
684            if let Some(named) = &row.named {
685                let mut bloom = named.keys().fold(0u64, |acc, k| acc | field_name_bloom(k));
686                // Single-source documents no longer materialise promoted
687                // columns, so the body's top-level keys are the only place a
688                // `WHERE`/projection field lives. Fold them into the bloom so
689                // the field-bloom gate doesn't reject a row before the body
690                // read fallback ever runs. Inert for non-document rows (no
691                // binary-container `body` field) — `container_field_names`
692                // self-gates on the RDOC magic.
693                if let Some(Value::Json(bytes)) = named.get("body") {
694                    if let Ok(names) = reddb_types::document_body_codec::field_names(bytes) {
695                        for name in names {
696                            bloom |= field_name_bloom(&name);
697                        }
698                    }
699                }
700                bloom
701            } else {
702                0
703            }
704        }
705        EntityData::Node(node) => node
706            .properties
707            .keys()
708            .fold(0u64, |acc, k| acc | field_name_bloom(k)),
709        EntityData::Edge(edge) => edge
710            .properties
711            .keys()
712            .fold(0u64, |acc, k| acc | field_name_bloom(k)),
713        // Vectors, time-series, queue: no user-named fields worth blooming.
714        _ => 0,
715    }
716}
717
718impl UnifiedEntity {
719    /// Create a new unified entity
720    pub fn new(id: EntityId, kind: EntityKind, data: EntityData) -> Self {
721        let now = current_unix_secs();
722        let field_bloom = compute_entity_field_bloom(&data);
723
724        Self {
725            id,
726            logical_id: None,
727            kind,
728            created_at: now,
729            updated_at: now,
730            data,
731            sequence_id: 0,
732            field_bloom,
733            // Pre-MVCC default: xmin/xmax = 0 means visible to every snapshot.
734            // Transactional writers stamp real snapshot IDs after allocation.
735            xmin: 0,
736            xmax: 0,
737            aux: None,
738        }
739    }
740
741    /// MVCC visibility check (Phase 2.3 PG parity).
742    ///
743    /// Returns `true` when this tuple is visible under the provided
744    /// snapshot xid. Pre-MVCC rows (`xmin == 0`, `xmax == 0`) are visible
745    /// to every snapshot — preserves full compatibility with existing
746    /// data inserted before the MVCC headers existed.
747    ///
748    /// Snapshot isolation rule:
749    ///   - `xmin == 0 || xmin <= snapshot_xid`  (creator committed before snapshot)
750    ///   - `xmax == 0 || xmax > snapshot_xid`   (deleter committed after snapshot)
751    #[inline]
752    pub fn is_visible(&self, snapshot_xid: u64) -> bool {
753        if self.xmin != 0 && self.xmin > snapshot_xid {
754            return false;
755        }
756        if self.xmax != 0 && self.xmax <= snapshot_xid {
757            return false;
758        }
759        true
760    }
761
762    /// Stamp `xmin` (creation transaction ID). Called by the runtime on
763    /// INSERT inside an active transaction.
764    #[inline]
765    pub fn set_xmin(&mut self, xid: u64) {
766        self.xmin = xid;
767    }
768
769    /// Stamp `xmax` (deletion transaction ID). Called by the runtime on
770    /// DELETE/UPDATE inside an active transaction — the tuple survives
771    /// until VACUUM reclaims it.
772    #[inline]
773    pub fn set_xmax(&mut self, xid: u64) {
774        self.xmax = xid;
775    }
776
777    pub fn branch_version(&self) -> Option<&BranchVersion> {
778        self.aux.as_deref()?.branch.as_ref()
779    }
780
781    pub fn set_branch_origin(&mut self, epoch: u64) {
782        self.aux.get_or_insert_with(Default::default).branch = Some(BranchVersion {
783            origin: epoch,
784            deletions: Vec::new(),
785        });
786    }
787
788    pub fn clear_branch_version(&mut self) {
789        if let Some(aux) = self.aux.as_deref_mut() {
790            aux.branch = None;
791        }
792    }
793
794    pub fn branch_xmax(&self, epoch: u64) -> u64 {
795        match self.branch_version() {
796            Some(branch) if branch.origin != epoch => branch
797                .deletions
798                .binary_search_by_key(&epoch, |(owner, _)| *owner)
799                .map_or(0, |index| branch.deletions[index].1),
800            _ => self.xmax,
801        }
802    }
803
804    /// The caller holds the publication fence and starts from the current
805    /// stored entity, so another branch's deletion cannot be lost on replace.
806    pub fn set_branch_xmax(&mut self, epoch: u64, xid: u64) {
807        let Some(branch) = self.aux.as_deref_mut().and_then(|aux| aux.branch.as_mut()) else {
808            self.xmax = xid;
809            return;
810        };
811        if branch.origin == epoch {
812            self.xmax = xid;
813            return;
814        }
815        match branch
816            .deletions
817            .binary_search_by_key(&epoch, |(owner, _)| *owner)
818        {
819            Ok(index) if xid == 0 => {
820                branch.deletions.remove(index);
821            }
822            Ok(index) => branch.deletions[index].1 = xid,
823            Err(index) if xid != 0 => branch.deletions.insert(index, (epoch, xid)),
824            Err(_) => {}
825        }
826    }
827
828    pub fn retain_branch_deletions(&mut self, mut keep: impl FnMut(u64) -> bool) {
829        if let Some(branch) = self.aux.as_deref_mut().and_then(|aux| aux.branch.as_mut()) {
830            branch.deletions.retain(|(_, xid)| keep(*xid));
831        }
832    }
833
834    pub fn max_transaction_id(&self) -> u64 {
835        self.branch_version()
836            .map_or(self.xmin.max(self.xmax), |branch| {
837                branch
838                    .deletions
839                    .iter()
840                    .fold(self.xmin.max(self.xmax), |high, (_, xid)| high.max(*xid))
841            })
842    }
843
844    /// Stable user-visible identity. Legacy rows without an explicit
845    /// logical id map to their physical entity id.
846    #[inline]
847    pub fn logical_id(&self) -> EntityId {
848        self.logical_id.unwrap_or(self.id)
849    }
850
851    /// Returns true when the entity carries an explicit logical id on disk.
852    #[inline]
853    pub fn has_explicit_logical_id(&self) -> bool {
854        self.logical_id.is_some()
855    }
856
857    /// Set the stable user-visible identity for this physical version.
858    #[inline]
859    pub fn set_logical_id(&mut self, logical_id: EntityId) {
860        self.logical_id = Some(logical_id);
861    }
862
863    /// Ensure entities written by the current engine carry explicit
864    /// logical identity. Table rows + documents (Phase 1/2) and graph
865    /// nodes/edges (Phase 3) participate in the multi-model MVCC
866    /// versioning rollout and so need a stable logical id for
867    /// version-chain selection. Stamping the logical id is inert for
868    /// non-versioned collections: history only accrues through
869    /// `install_versioned_table_row_update`, which is gated on the
870    /// collection `versioned` flag, so a stamped-but-never-superseded
871    /// entity keeps `logical_id == id` and behaves exactly as before.
872    /// Vectors are intentionally excluded pending their read-path follow
873    /// up (no snapshot-honoring `VECTOR SEARCH`).
874    #[inline]
875    pub fn ensure_table_logical_id(&mut self) {
876        if self.logical_id.is_none()
877            && matches!(
878                self.kind,
879                EntityKind::TableRow { .. } | EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
880            )
881        {
882            self.logical_id = Some(self.id);
883        }
884    }
885
886    /// Create a table row entity
887    pub fn table_row(
888        id: EntityId,
889        table: impl Into<Arc<str>>,
890        row_id: u64,
891        columns: Vec<Value>,
892    ) -> Self {
893        Self::new(
894            id,
895            EntityKind::TableRow {
896                table: table.into(),
897                row_id,
898            },
899            EntityData::Row(RowData::new(columns)),
900        )
901    }
902
903    /// Create a graph node entity
904    pub fn graph_node(
905        id: EntityId,
906        label: impl Into<String>,
907        node_type: impl Into<String>,
908        properties: HashMap<String, Value>,
909    ) -> Self {
910        Self::new(
911            id,
912            EntityKind::GraphNode(Box::new(GraphNodeKind {
913                label: label.into(),
914                node_type: node_type.into(),
915            })),
916            EntityData::Node(NodeData::with_properties(properties)),
917        )
918    }
919
920    /// Create a graph edge entity
921    pub fn graph_edge(
922        id: EntityId,
923        label: impl Into<String>,
924        from: impl Into<String>,
925        to: impl Into<String>,
926        weight: f32,
927        properties: HashMap<String, Value>,
928    ) -> Self {
929        Self::new(
930            id,
931            EntityKind::GraphEdge(Box::new(GraphEdgeKind {
932                label: label.into(),
933                from_node: from.into(),
934                to_node: to.into(),
935                weight: (weight * 1000.0) as u32,
936            })),
937            EntityData::Edge(EdgeData::with_properties(weight, properties)),
938        )
939    }
940
941    /// Create a vector entity
942    pub fn vector(id: EntityId, collection: impl Into<String>, vector: Vec<f32>) -> Self {
943        Self::new(
944            id,
945            EntityKind::Vector {
946                collection: collection.into(),
947            },
948            EntityData::Vector(VectorData::new(vector)),
949        )
950    }
951
952    /// Add an embedding to this entity
953    pub fn add_embedding(&mut self, slot: EmbeddingSlot) {
954        self.embeddings_mut().push(slot);
955        self.touch();
956    }
957
958    /// Add a cross-reference
959    pub fn add_cross_ref(&mut self, cross_ref: CrossRef) {
960        self.cross_refs_mut().push(cross_ref);
961        self.touch();
962    }
963
964    /// Get embedding by slot name
965    pub fn get_embedding(&self, name: &str) -> Option<&EmbeddingSlot> {
966        self.embeddings().iter().find(|e| e.name == name)
967    }
968
969    /// Update timestamp
970    fn touch(&mut self) {
971        self.updated_at = current_unix_secs();
972    }
973
974    /// Check if entity is stale (not updated in given seconds)
975    pub fn is_stale(&self, max_age_secs: u64) -> bool {
976        let now = current_unix_secs();
977        now.saturating_sub(self.updated_at) > max_age_secs
978    }
979}
980
981/// A cross-reference between entities
982#[derive(Debug, Clone, PartialEq)]
983pub struct CrossRef {
984    /// Source entity ID (the entity that holds this reference)
985    pub source: EntityId,
986    /// Target entity ID
987    pub target: EntityId,
988    /// Target collection name
989    pub target_collection: String,
990    /// Type of reference
991    pub ref_type: RefType,
992    /// Reference weight/strength (0.0-1.0)
993    pub weight: f32,
994    /// When this reference was created
995    pub created_at: u64,
996}
997
998impl CrossRef {
999    /// Create a new cross-reference
1000    pub fn new(
1001        source: EntityId,
1002        target: EntityId,
1003        target_collection: impl Into<String>,
1004        ref_type: RefType,
1005    ) -> Self {
1006        Self {
1007            source,
1008            target,
1009            target_collection: target_collection.into(),
1010            ref_type,
1011            weight: 1.0,
1012            created_at: current_unix_secs(),
1013        }
1014    }
1015
1016    /// Create with weight
1017    pub fn with_weight(
1018        source: EntityId,
1019        target: EntityId,
1020        target_collection: impl Into<String>,
1021        ref_type: RefType,
1022        weight: f32,
1023    ) -> Self {
1024        let mut cr = Self::new(source, target, target_collection, ref_type);
1025        cr.weight = weight;
1026        cr
1027    }
1028}
1029
1030/// Types of cross-references between entities
1031#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1032pub enum RefType {
1033    // Table ↔ Graph
1034    RowToNode, // Table row represents a graph node
1035    RowToEdge, // Table row represents a graph edge
1036    NodeToRow, // Node links back to source row
1037
1038    // Table ↔ Vector
1039    RowToVector, // Table row has embeddings
1040    VectorToRow, // Vector search → source row
1041
1042    // Graph ↔ Vector
1043    NodeToVector, // Node has embeddings
1044    EdgeToVector, // Edge has embeddings
1045    VectorToNode, // Vector search → source node
1046
1047    // Semantic links (discovered)
1048    SimilarTo,   // Discovered by vector similarity
1049    RelatedTo,   // Domain-specific relationship
1050    DerivesFrom, // Data lineage
1051    Mentions,    // Text mentions another entity
1052    Contains,    // Structural containment
1053    DependsOn,   // Dependency relationship
1054}
1055
1056impl RefType {
1057    /// Get the inverse reference type (for bidirectional tracking)
1058    pub fn inverse(&self) -> Option<Self> {
1059        match self {
1060            Self::RowToNode => Some(Self::NodeToRow),
1061            Self::NodeToRow => Some(Self::RowToNode),
1062            Self::RowToVector => Some(Self::VectorToRow),
1063            Self::VectorToRow => Some(Self::RowToVector),
1064            Self::NodeToVector => Some(Self::VectorToNode),
1065            Self::VectorToNode => Some(Self::NodeToVector),
1066            Self::SimilarTo => Some(Self::SimilarTo), // Symmetric
1067            Self::RelatedTo => Some(Self::RelatedTo), // Symmetric
1068            _ => None,                                // One-directional references
1069        }
1070    }
1071
1072    /// Check if this is a symmetric reference type
1073    pub fn is_symmetric(&self) -> bool {
1074        matches!(self, Self::SimilarTo | Self::RelatedTo)
1075    }
1076
1077    /// Convert RefType to byte for binary serialization
1078    pub fn to_byte(&self) -> u8 {
1079        match self {
1080            Self::RowToNode => 0,
1081            Self::RowToEdge => 1,
1082            Self::NodeToRow => 2,
1083            Self::RowToVector => 3,
1084            Self::VectorToRow => 4,
1085            Self::NodeToVector => 5,
1086            Self::EdgeToVector => 6,
1087            Self::VectorToNode => 7,
1088            Self::SimilarTo => 8,
1089            Self::RelatedTo => 9,
1090            Self::DerivesFrom => 10,
1091            Self::Mentions => 11,
1092            Self::Contains => 12,
1093            Self::DependsOn => 13,
1094        }
1095    }
1096
1097    /// Create RefType from byte (binary deserialization)
1098    pub fn from_byte(byte: u8) -> Self {
1099        match byte {
1100            0 => Self::RowToNode,
1101            1 => Self::RowToEdge,
1102            2 => Self::NodeToRow,
1103            3 => Self::RowToVector,
1104            4 => Self::VectorToRow,
1105            5 => Self::NodeToVector,
1106            6 => Self::EdgeToVector,
1107            7 => Self::VectorToNode,
1108            8 => Self::SimilarTo,
1109            9 => Self::RelatedTo,
1110            10 => Self::DerivesFrom,
1111            11 => Self::Mentions,
1112            12 => Self::Contains,
1113            13 => Self::DependsOn,
1114            _ => Self::RelatedTo, // Default fallback
1115        }
1116    }
1117}
1118
1119/// Convert Vec<Value> to RowData
1120impl From<Vec<Value>> for RowData {
1121    fn from(columns: Vec<Value>) -> Self {
1122        RowData::new(columns)
1123    }
1124}
1125
1126/// Convert HashMap to NodeData
1127impl From<HashMap<String, Value>> for NodeData {
1128    fn from(properties: HashMap<String, Value>) -> Self {
1129        NodeData::with_properties(properties)
1130    }
1131}
1132
1133/// Convert dense vector to VectorData
1134impl From<Vec<f32>> for VectorData {
1135    fn from(dense: Vec<f32>) -> Self {
1136        VectorData::new(dense)
1137    }
1138}
1139
1140/// Convert tuple (dense, sparse) to VectorData
1141impl From<(Vec<f32>, SparseVector)> for VectorData {
1142    fn from((dense, sparse): (Vec<f32>, SparseVector)) -> Self {
1143        VectorData::with_sparse(dense, sparse)
1144    }
1145}
1146
1147// Helper trait for uniform entity creation
1148impl UnifiedEntity {
1149    /// Create a graph node entity from properties map
1150    pub fn from_properties(
1151        id: EntityId,
1152        label: impl Into<String>,
1153        node_type: impl Into<String>,
1154        properties: impl IntoIterator<Item = (impl Into<String>, Value)>,
1155    ) -> Self {
1156        let props: HashMap<String, Value> =
1157            properties.into_iter().map(|(k, v)| (k.into(), v)).collect();
1158        Self::graph_node(id, label, node_type, props)
1159    }
1160
1161    /// Convert entity to row data if applicable
1162    pub fn into_row(self) -> Option<RowData> {
1163        match self.data {
1164            EntityData::Row(r) => Some(r),
1165            _ => None,
1166        }
1167    }
1168
1169    /// Convert entity to node data if applicable
1170    pub fn into_node(self) -> Option<NodeData> {
1171        match self.data {
1172            EntityData::Node(n) => Some(n),
1173            _ => None,
1174        }
1175    }
1176
1177    /// Convert entity to edge data if applicable
1178    pub fn into_edge(self) -> Option<EdgeData> {
1179        match self.data {
1180            EntityData::Edge(e) => Some(e),
1181            _ => None,
1182        }
1183    }
1184
1185    /// Convert entity to vector data if applicable
1186    pub fn into_vector(self) -> Option<VectorData> {
1187        match self.data {
1188            EntityData::Vector(v) => Some(v),
1189            _ => None,
1190        }
1191    }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196    use super::*;
1197
1198    #[test]
1199    fn test_entity_creation() {
1200        let id = EntityId::new(1);
1201        let entity = UnifiedEntity::table_row(
1202            id,
1203            "users",
1204            100,
1205            vec![Value::text("alice".to_string()), Value::Integer(25)],
1206        );
1207
1208        assert!(entity.data.is_row());
1209        assert_eq!(entity.kind.storage_type(), "table");
1210        assert_eq!(entity.kind.collection(), "users");
1211    }
1212
1213    #[test]
1214    fn test_cross_refs() {
1215        let id1 = EntityId::new(1);
1216        let id2 = EntityId::new(2);
1217
1218        let cross_ref = CrossRef::new(id1, id2, "nodes", RefType::RowToNode);
1219        assert_eq!(cross_ref.source, id1);
1220        assert_eq!(cross_ref.target, id2);
1221        assert_eq!(cross_ref.ref_type.inverse(), Some(RefType::NodeToRow));
1222    }
1223
1224    #[test]
1225    fn test_sparse_vector() {
1226        let sparse = SparseVector::new(vec![0, 5, 10], vec![1.0, 2.0, 3.0], 100);
1227
1228        assert_eq!(sparse.nnz(), 3);
1229        assert_eq!(sparse.get(5), 2.0);
1230        assert_eq!(sparse.get(3), 0.0);
1231        assert!(sparse.sparsity() > 0.9);
1232    }
1233
1234    #[test]
1235    fn test_embedding_slots() {
1236        let mut entity = UnifiedEntity::table_row(
1237            EntityId::new(1),
1238            "documents",
1239            1,
1240            vec![Value::text("Hello world".to_string())],
1241        );
1242
1243        entity.add_embedding(EmbeddingSlot::new(
1244            "content",
1245            vec![0.1, 0.2, 0.3],
1246            "text-embedding-3-small",
1247        ));
1248
1249        assert_eq!(entity.embeddings().len(), 1);
1250        assert!(entity.get_embedding("content").is_some());
1251        assert!(entity.get_embedding("summary").is_none());
1252    }
1253}