Skip to main content

rushdb_types/
occurrence.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ids::OwnerKind;
4use crate::model::{Cardinality, LogicalType, Nullability, TypeState};
5
6/// `ExplicitNull` is a payload-free occurrence state, never a domain type.
7#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize)]
8pub enum OccurrenceValue {
9    /// A property explicitly set to null.
10    ExplicitNull,
11    /// Marker occurrence for an array property with zero elements. Arrays are
12    /// stored element-wise, so an empty array has no elements to persist; this
13    /// payload-free marker (stored at elem 0) lets reconstruction emit a
14    /// genuine `[]` — matching prod, where empty arrays are preserved rather
15    /// than collapsed to null.
16    EmptyArray,
17    /// A stored scalar value of a logical type; its order-preserving payload
18    /// is stored inline in the occurrence row.
19    Stored {
20        /// Logical type of the stored scalar.
21        ty: LogicalType,
22    },
23}
24
25impl OccurrenceValue {
26    /// Report whether this occurrence is an explicit null.
27    #[must_use]
28    pub fn is_null(&self) -> bool {
29        matches!(self, OccurrenceValue::ExplicitNull)
30    }
31}
32
33/// One property occurrence of a record or relationship. Its identity is
34/// `(owner_kind, owner_ord, property_id, elem)`; the canonical row is keyed by
35/// exactly that (`keys::occ_key`, DC11).
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct OccurrenceRow {
38    /// Whether the owning row is a record or a relationship.
39    pub owner_kind: OwnerKind,
40    /// Dense ordinal of the owning row.
41    pub owner_ord: u64,
42    /// Property id this occurrence belongs to.
43    pub property_id: u64,
44    /// Property family this occurrence belongs to.
45    pub family_id: u64,
46    /// Element index within an array property, or `None` for scalars.
47    pub elem_ordinal: Option<u64>,
48    /// The occurrence's value state. The value hash is not carried: it is a
49    /// function of this state and the canonical bytes
50    /// ([`occurrence_value_hash`]).
51    pub occurrence_value: OccurrenceValue,
52}
53
54/// Canonical value hash of an occurrence (null and the empty-array marker
55/// share the null hash, as the planner assigns them).
56#[must_use]
57pub fn occurrence_value_hash(value: OccurrenceValue, payload: &[u8]) -> u64 {
58    match value {
59        OccurrenceValue::Stored { ty } => {
60            crate::hash::value_hash(crate::encoding::logical_tag(ty), payload)
61        }
62        OccurrenceValue::ExplicitNull | OccurrenceValue::EmptyArray => {
63            crate::hash::null_value_hash()
64        }
65    }
66}
67
68/// Durable header for a record.
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct RecordHeader {
71    /// Stable public record id.
72    pub record_id: [u8; 16],
73    /// Dense ordinal of the record.
74    pub ordinal: u64,
75    /// Primary label of the record.
76    pub label: String,
77}
78
79/// Durable header for a relationship.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct RelationshipHeader {
82    /// Stable public relationship id.
83    pub relationship_id: [u8; 16],
84    /// Dense ordinal of the relationship.
85    pub ordinal: u64,
86    /// Relationship type.
87    pub rel_type: String,
88    /// Ordinal of the source record.
89    pub source_ord: u64,
90    /// Ordinal of the target record.
91    pub target_ord: u64,
92    /// Public id of the source record.
93    pub source_id: [u8; 16],
94    /// Public id of the target record.
95    pub target_id: [u8; 16],
96}
97
98/// A normalized property family, scoped to records or relationships.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct PropertyFamily {
101    /// Dense id of the family.
102    pub family_id: u64,
103    /// Namespace the property belongs to.
104    pub namespace: String,
105    /// Whether the family is owned by records or relationships.
106    pub owner_scope: OwnerKind,
107    /// Normalized name used for lookup.
108    pub normalized_name: String,
109    /// Human-readable display name.
110    pub display_name: String,
111}
112
113/// A typed property definition within a family.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct PropertyDefinition {
116    /// Dense id of the property.
117    pub property_id: u64,
118    /// Family this definition belongs to.
119    pub family_id: u64,
120    /// Current type resolution state.
121    pub type_state: TypeState,
122    /// Allowed value cardinality.
123    pub cardinality: Cardinality,
124    /// Nullability constraint.
125    pub nullability: Nullability,
126}
127
128/// Immutable vector-space contract bound through stable aliases.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
130#[serde(rename_all = "lowercase")]
131pub enum Similarity {
132    /// Cosine similarity (dot product of L2-normalized vectors).
133    #[default]
134    Cosine,
135    /// Dot product (raw; for best results use normalized vectors).
136    Dot,
137    /// Euclidean similarity = `1 / (1 + euclidean_distance)` (Neo4j-style).
138    Euclidean,
139}
140
141impl Similarity {
142    /// Lowercase name of the similarity metric.
143    #[must_use]
144    pub fn as_str(&self) -> &'static str {
145        match self {
146            Similarity::Cosine => "cosine",
147            Similarity::Dot => "dot",
148            Similarity::Euclidean => "euclidean",
149        }
150    }
151}
152
153/// Immutable metadata for an embedding space.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct EmbeddingSpaceInfo {
156    /// Dense id of the space.
157    pub space_id: u64,
158    /// Stable, opaque index name. The engine carries vectors without any notion
159    /// of embedding source (managed/external/model) — those are control-plane
160    /// concepts. Legacy spaces (persisted without this field) default to empty.
161    #[serde(default)]
162    pub name: String,
163    /// Dimensionality of vectors in the space.
164    pub dims: u32,
165    /// Similarity metric for the space. Older persisted spaces without this
166    /// field default to cosine.
167    #[serde(default)]
168    pub similarity: Similarity,
169}
170
171/// Lifecycle status of an embedding binding.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum BindingStatus {
174    /// Binding created but not yet populated.
175    Pending,
176    /// Binding is populated and eligible for search.
177    Ready,
178    /// Source value changed, so the binding must be rebuilt.
179    Stale,
180    /// Binding permanently retired.
181    Retired,
182}
183
184/// One canonical source -> Space association carrying the exact value hash
185/// at bind time. Eligible only while that hash remains current.
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct EmbeddingBinding {
188    /// Space this binding targets.
189    pub space_id: u64,
190    /// Whether the owner is a record or a relationship.
191    pub owner_kind: OwnerKind,
192    /// Dense ordinal of the owning row.
193    pub owner_ord: u64,
194    /// Property id of the bound value.
195    pub pid: u64,
196    /// Element index within an array property.
197    pub elem: u64,
198    /// Slot of the stored vector within the space.
199    pub vector_slot: u64,
200    /// Value hash captured when the binding was created.
201    pub source_hash: u64,
202    /// `true` for a vector derived from a canonical occurrence. Such a binding
203    /// becomes stale when the occurrence value changes. External vectors use a
204    /// payload-derived hash and remain eligible while their owner exists.
205    #[serde(default = "default_source_hash_tracked")]
206    pub source_hash_tracked: bool,
207    /// Current lifecycle status of the binding.
208    pub status: BindingStatus,
209}
210
211fn default_source_hash_tracked() -> bool {
212    true
213}
214
215/// Bytes of a stored binding value ([`EmbeddingBinding::encode_value`]).
216pub const BINDING_VALUE_LEN: usize = 17;
217
218/// The part of a stored binding its key does not hold: `vector_slot`
219/// (u64 LE) `||` `source_hash` (u64 LE) `||` flags (status in bits 0-1,
220/// `source_hash_tracked` in bit 2). The key carries the space and source
221/// ([`crate::keys::parse_emb_binding_key`]).
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct BindingValue {
224    /// Slot of the stored vector within the space.
225    pub vector_slot: u64,
226    /// Value hash captured when the binding was created.
227    pub source_hash: u64,
228    /// See [`EmbeddingBinding::source_hash_tracked`].
229    pub source_hash_tracked: bool,
230    /// Current lifecycle status of the binding.
231    pub status: BindingStatus,
232}
233
234impl BindingValue {
235    /// Decode a stored binding value; `None` when it is not one.
236    #[must_use]
237    pub fn decode(raw: &[u8]) -> Option<Self> {
238        let raw: &[u8; BINDING_VALUE_LEN] = raw.try_into().ok()?;
239        let flags = raw[16];
240        if flags & !0b111 != 0 {
241            return None;
242        }
243        let status = match flags & 0b11 {
244            0 => BindingStatus::Pending,
245            1 => BindingStatus::Ready,
246            2 => BindingStatus::Stale,
247            _ => BindingStatus::Retired,
248        };
249        Some(Self {
250            vector_slot: u64::from_le_bytes(raw[0..8].try_into().ok()?),
251            source_hash: u64::from_le_bytes(raw[8..16].try_into().ok()?),
252            source_hash_tracked: flags & 0b100 != 0,
253            status,
254        })
255    }
256}
257
258impl EmbeddingBinding {
259    /// The stored value of this binding (see [`BindingValue`]).
260    #[must_use]
261    pub fn encode_value(&self) -> [u8; BINDING_VALUE_LEN] {
262        let mut out = [0u8; BINDING_VALUE_LEN];
263        out[0..8].copy_from_slice(&self.vector_slot.to_le_bytes());
264        out[8..16].copy_from_slice(&self.source_hash.to_le_bytes());
265        out[16] = match self.status {
266            BindingStatus::Pending => 0,
267            BindingStatus::Ready => 1,
268            BindingStatus::Stale => 2,
269            BindingStatus::Retired => 3,
270        } | if self.source_hash_tracked { 0b100 } else { 0 };
271        out
272    }
273
274    /// Rebuild a binding from its stored key and value; `None` when either
275    /// is not a binding's.
276    #[must_use]
277    pub fn decode(key: &[u8], value: &[u8]) -> Option<Self> {
278        let (space_id, owner_kind, owner_ord, pid, elem) = crate::keys::parse_emb_binding_key(key)?;
279        let value = BindingValue::decode(value)?;
280        Some(Self {
281            space_id,
282            owner_kind,
283            owner_ord,
284            pid,
285            elem,
286            vector_slot: value.vector_slot,
287            source_hash: value.source_hash,
288            source_hash_tracked: value.source_hash_tracked,
289            status: value.status,
290        })
291    }
292}
293
294/// Occurrence row flag: the occurrence is an element of an array property.
295pub const OCC_FLAG_ARRAY: u8 = 0x01;
296/// Occurrence row state: explicit null.
297pub const OCC_STATE_NULL: u8 = 0x00;
298/// Occurrence row state: the zero-element array marker.
299pub const OCC_STATE_EMPTY_ARRAY: u8 = 0xFE;
300
301/// Encode an occurrence row value: `flags || state || payload`, where state is
302/// [`OCC_STATE_NULL`], [`OCC_STATE_EMPTY_ARRAY`], or the logical type tag of a
303/// stored scalar followed by its order-preserving payload.
304#[must_use]
305pub fn encode_occ_value(array: bool, value: OccurrenceValue, payload: Option<&[u8]>) -> Vec<u8> {
306    let payload = payload.unwrap_or_default();
307    let mut out = Vec::with_capacity(2 + payload.len());
308    out.push(if array { OCC_FLAG_ARRAY } else { 0 });
309    match value {
310        OccurrenceValue::ExplicitNull => out.push(OCC_STATE_NULL),
311        OccurrenceValue::EmptyArray => out.push(OCC_STATE_EMPTY_ARRAY),
312        OccurrenceValue::Stored { ty } => {
313            out.push(crate::encoding::logical_tag(ty));
314            out.extend_from_slice(payload);
315        }
316    }
317    out
318}
319
320/// A decoded occurrence row value.
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub struct OccValue<'a> {
323    /// Whether the occurrence is an array element.
324    pub array: bool,
325    /// Value state.
326    pub value: OccurrenceValue,
327    /// Order-preserving payload of a stored scalar (empty otherwise).
328    pub payload: &'a [u8],
329    /// `tag || payload` of a stored scalar: the value-dictionary entry.
330    pub entry: &'a [u8],
331}
332
333impl OccValue<'_> {
334    /// Canonical value hash ([`occurrence_value_hash`]).
335    #[must_use]
336    pub fn value_hash(&self) -> u64 {
337        occurrence_value_hash(self.value, self.payload)
338    }
339}
340
341/// Decode an occurrence row value written by [`encode_occ_value`].
342///
343/// # Errors
344/// Returns [`crate::error::TypesError::MalformedKey`] for a malformed value.
345pub fn decode_occ_value(raw: &[u8]) -> Result<OccValue<'_>, crate::error::TypesError> {
346    let malformed = crate::error::TypesError::MalformedKey;
347    let (&flags, rest) = raw.split_first().ok_or(malformed.clone())?;
348    let (&state, payload) = rest.split_first().ok_or(malformed.clone())?;
349    let value = match state {
350        OCC_STATE_NULL => OccurrenceValue::ExplicitNull,
351        OCC_STATE_EMPTY_ARRAY => OccurrenceValue::EmptyArray,
352        tag => OccurrenceValue::Stored {
353            ty: crate::encoding::logical_from_tag(tag)?,
354        },
355    };
356    if !matches!(value, OccurrenceValue::Stored { .. }) && !payload.is_empty() {
357        return Err(malformed);
358    }
359    Ok(OccValue {
360        array: flags & OCC_FLAG_ARRAY != 0,
361        value,
362        payload,
363        entry: rest,
364    })
365}
366
367#[cfg(test)]
368mod codec_tests {
369    use super::*;
370    use crate::encoding::{decode_order_preserving, encode_order_preserving, logical_tag};
371    use crate::model::LogicalType;
372    use crate::value::Scalar;
373
374    #[test]
375    fn a_binding_round_trips_through_its_key_and_compact_value() {
376        for (status, tracked) in [
377            (BindingStatus::Pending, false),
378            (BindingStatus::Ready, true),
379            (BindingStatus::Stale, true),
380            (BindingStatus::Retired, false),
381        ] {
382            let binding = EmbeddingBinding {
383                space_id: 7,
384                owner_kind: OwnerKind::Relationship,
385                owner_ord: 1 << 40,
386                pid: 3,
387                elem: 2,
388                vector_slot: u64::MAX - 1,
389                source_hash: 0xdead_beef_cafe_f00d,
390                source_hash_tracked: tracked,
391                status,
392            };
393            let key = crate::keys::emb_binding_key(
394                crate::ids::TenantId(9),
395                binding.space_id,
396                binding.owner_kind,
397                binding.owner_ord,
398                binding.pid,
399                binding.elem,
400            );
401            let value = binding.encode_value();
402            assert_eq!(value.len(), BINDING_VALUE_LEN);
403            assert_eq!(EmbeddingBinding::decode(&key, &value), Some(binding));
404        }
405        // A wrong length or an unknown flag bit is not a binding value.
406        assert_eq!(BindingValue::decode(&[0; 16]), None);
407        let mut bad = [0u8; BINDING_VALUE_LEN];
408        bad[16] = 0b1000;
409        assert_eq!(BindingValue::decode(&bad), None);
410    }
411
412    /// Seeded xorshift: a reproducible property test without a dependency.
413    struct Rng(u64);
414    impl Rng {
415        fn next(&mut self) -> u64 {
416            self.0 ^= self.0 << 13;
417            self.0 ^= self.0 >> 7;
418            self.0 ^= self.0 << 17;
419            self.0
420        }
421    }
422
423    fn scalar(rng: &mut Rng) -> (LogicalType, Scalar) {
424        let bits = rng.next();
425        match bits % 5 {
426            0 => (LogicalType::Boolean, Scalar::Boolean(bits & 8 != 0)),
427            1 => (LogicalType::Int64, Scalar::Int64(bits.cast_signed())),
428            2 => {
429                let f = f64::from_bits(rng.next());
430                let f = if f.is_finite() { f } else { -0.0 };
431                (LogicalType::Float64, Scalar::Float64(f))
432            }
433            3 => (
434                LogicalType::DateTime,
435                Scalar::DateTime((bits >> 2).cast_signed()),
436            ),
437            _ => {
438                // Up to 1 MiB, including multi-byte UTF-8.
439                let len = usize::try_from(rng.next() % (1 << 20)).unwrap();
440                let text: String = "aé€".chars().cycle().take(len / 3).collect();
441                (LogicalType::Utf8, Scalar::Utf8(text))
442            }
443        }
444    }
445
446    #[test]
447    fn occurrence_values_round_trip_for_every_type_null_and_array_state() {
448        let mut rng = Rng(0x2545_f491_4f6c_dd1d);
449        for _ in 0..200 {
450            let array = rng.next() & 1 == 1;
451            let (ty, value) = scalar(&mut rng);
452            let payload = encode_order_preserving(ty, &value).unwrap();
453            let raw = encode_occ_value(array, OccurrenceValue::Stored { ty }, Some(&payload));
454            let decoded = decode_occ_value(&raw).unwrap();
455            assert_eq!(decoded.array, array);
456            assert_eq!(decoded.value, OccurrenceValue::Stored { ty });
457            assert_eq!(decoded.payload, payload.as_slice());
458            assert_eq!(decoded.entry[0], logical_tag(ty));
459            assert_eq!(&decoded.entry[1..], payload.as_slice());
460            let back = decode_order_preserving(ty, decoded.payload).unwrap();
461            assert_eq!(
462                encode_order_preserving(ty, &back).unwrap(),
463                payload,
464                "{ty:?}"
465            );
466        }
467        for state in [OccurrenceValue::ExplicitNull, OccurrenceValue::EmptyArray] {
468            for array in [false, true] {
469                let raw = encode_occ_value(array, state, None);
470                let decoded = decode_occ_value(&raw).unwrap();
471                assert_eq!((decoded.array, decoded.value), (array, state));
472                assert!(decoded.payload.is_empty());
473            }
474        }
475    }
476
477    #[test]
478    fn malformed_occurrence_values_are_errors_never_panics() {
479        assert!(decode_occ_value(&[]).is_err());
480        assert!(decode_occ_value(&[0]).is_err());
481        // A null or empty-array state never carries a payload.
482        assert!(decode_occ_value(&[0, OCC_STATE_NULL, 1]).is_err());
483        assert!(decode_occ_value(&[0, OCC_STATE_EMPTY_ARRAY, 1]).is_err());
484        // Unknown type tags are rejected.
485        assert!(decode_occ_value(&[0, 0x7F]).is_err());
486        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
487        for _ in 0..10_000 {
488            let len = usize::try_from(rng.next() % 24).unwrap();
489            let raw: Vec<u8> = (0..len).map(|_| rng.next().to_le_bytes()[0]).collect();
490            // Must return, Ok or Err, for any input.
491            let _ = decode_occ_value(&raw);
492        }
493    }
494}