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/// Occurrence row flag: the occurrence is an element of an array property.
216pub const OCC_FLAG_ARRAY: u8 = 0x01;
217/// Occurrence row state: explicit null.
218pub const OCC_STATE_NULL: u8 = 0x00;
219/// Occurrence row state: the zero-element array marker.
220pub const OCC_STATE_EMPTY_ARRAY: u8 = 0xFE;
221
222/// Encode an occurrence row value: `flags || state || payload`, where state is
223/// [`OCC_STATE_NULL`], [`OCC_STATE_EMPTY_ARRAY`], or the logical type tag of a
224/// stored scalar followed by its order-preserving payload.
225#[must_use]
226pub fn encode_occ_value(array: bool, value: OccurrenceValue, payload: Option<&[u8]>) -> Vec<u8> {
227    let payload = payload.unwrap_or_default();
228    let mut out = Vec::with_capacity(2 + payload.len());
229    out.push(if array { OCC_FLAG_ARRAY } else { 0 });
230    match value {
231        OccurrenceValue::ExplicitNull => out.push(OCC_STATE_NULL),
232        OccurrenceValue::EmptyArray => out.push(OCC_STATE_EMPTY_ARRAY),
233        OccurrenceValue::Stored { ty } => {
234            out.push(crate::encoding::logical_tag(ty));
235            out.extend_from_slice(payload);
236        }
237    }
238    out
239}
240
241/// A decoded occurrence row value.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub struct OccValue<'a> {
244    /// Whether the occurrence is an array element.
245    pub array: bool,
246    /// Value state.
247    pub value: OccurrenceValue,
248    /// Order-preserving payload of a stored scalar (empty otherwise).
249    pub payload: &'a [u8],
250    /// `tag || payload` of a stored scalar: the value-dictionary entry.
251    pub entry: &'a [u8],
252}
253
254impl OccValue<'_> {
255    /// Canonical value hash ([`occurrence_value_hash`]).
256    #[must_use]
257    pub fn value_hash(&self) -> u64 {
258        occurrence_value_hash(self.value, self.payload)
259    }
260}
261
262/// Decode an occurrence row value written by [`encode_occ_value`].
263///
264/// # Errors
265/// Returns [`crate::error::TypesError::MalformedKey`] for a malformed value.
266pub fn decode_occ_value(raw: &[u8]) -> Result<OccValue<'_>, crate::error::TypesError> {
267    let malformed = crate::error::TypesError::MalformedKey;
268    let (&flags, rest) = raw.split_first().ok_or(malformed.clone())?;
269    let (&state, payload) = rest.split_first().ok_or(malformed.clone())?;
270    let value = match state {
271        OCC_STATE_NULL => OccurrenceValue::ExplicitNull,
272        OCC_STATE_EMPTY_ARRAY => OccurrenceValue::EmptyArray,
273        tag => OccurrenceValue::Stored {
274            ty: crate::encoding::logical_from_tag(tag)?,
275        },
276    };
277    if !matches!(value, OccurrenceValue::Stored { .. }) && !payload.is_empty() {
278        return Err(malformed);
279    }
280    Ok(OccValue {
281        array: flags & OCC_FLAG_ARRAY != 0,
282        value,
283        payload,
284        entry: rest,
285    })
286}
287
288#[cfg(test)]
289mod codec_tests {
290    use super::*;
291    use crate::encoding::{decode_order_preserving, encode_order_preserving, logical_tag};
292    use crate::model::LogicalType;
293    use crate::value::Scalar;
294
295    /// Seeded xorshift: a reproducible property test without a dependency.
296    struct Rng(u64);
297    impl Rng {
298        fn next(&mut self) -> u64 {
299            self.0 ^= self.0 << 13;
300            self.0 ^= self.0 >> 7;
301            self.0 ^= self.0 << 17;
302            self.0
303        }
304    }
305
306    fn scalar(rng: &mut Rng) -> (LogicalType, Scalar) {
307        let bits = rng.next();
308        match bits % 5 {
309            0 => (LogicalType::Boolean, Scalar::Boolean(bits & 8 != 0)),
310            1 => (LogicalType::Int64, Scalar::Int64(bits.cast_signed())),
311            2 => {
312                let f = f64::from_bits(rng.next());
313                let f = if f.is_finite() { f } else { -0.0 };
314                (LogicalType::Float64, Scalar::Float64(f))
315            }
316            3 => (
317                LogicalType::DateTime,
318                Scalar::DateTime((bits >> 2).cast_signed()),
319            ),
320            _ => {
321                // Up to 1 MiB, including multi-byte UTF-8.
322                let len = usize::try_from(rng.next() % (1 << 20)).unwrap();
323                let text: String = "aé€".chars().cycle().take(len / 3).collect();
324                (LogicalType::Utf8, Scalar::Utf8(text))
325            }
326        }
327    }
328
329    #[test]
330    fn occurrence_values_round_trip_for_every_type_null_and_array_state() {
331        let mut rng = Rng(0x2545_f491_4f6c_dd1d);
332        for _ in 0..200 {
333            let array = rng.next() & 1 == 1;
334            let (ty, value) = scalar(&mut rng);
335            let payload = encode_order_preserving(ty, &value).unwrap();
336            let raw = encode_occ_value(array, OccurrenceValue::Stored { ty }, Some(&payload));
337            let decoded = decode_occ_value(&raw).unwrap();
338            assert_eq!(decoded.array, array);
339            assert_eq!(decoded.value, OccurrenceValue::Stored { ty });
340            assert_eq!(decoded.payload, payload.as_slice());
341            assert_eq!(decoded.entry[0], logical_tag(ty));
342            assert_eq!(&decoded.entry[1..], payload.as_slice());
343            let back = decode_order_preserving(ty, decoded.payload).unwrap();
344            assert_eq!(
345                encode_order_preserving(ty, &back).unwrap(),
346                payload,
347                "{ty:?}"
348            );
349        }
350        for state in [OccurrenceValue::ExplicitNull, OccurrenceValue::EmptyArray] {
351            for array in [false, true] {
352                let raw = encode_occ_value(array, state, None);
353                let decoded = decode_occ_value(&raw).unwrap();
354                assert_eq!((decoded.array, decoded.value), (array, state));
355                assert!(decoded.payload.is_empty());
356            }
357        }
358    }
359
360    #[test]
361    fn malformed_occurrence_values_are_errors_never_panics() {
362        assert!(decode_occ_value(&[]).is_err());
363        assert!(decode_occ_value(&[0]).is_err());
364        // A null or empty-array state never carries a payload.
365        assert!(decode_occ_value(&[0, OCC_STATE_NULL, 1]).is_err());
366        assert!(decode_occ_value(&[0, OCC_STATE_EMPTY_ARRAY, 1]).is_err());
367        // Unknown type tags are rejected.
368        assert!(decode_occ_value(&[0, 0x7F]).is_err());
369        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
370        for _ in 0..10_000 {
371            let len = usize::try_from(rng.next() % 24).unwrap();
372            let raw: Vec<u8> = (0..len).map(|_| rng.next().to_le_bytes()[0]).collect();
373            // Must return, Ok or Err, for any input.
374            let _ = decode_occ_value(&raw);
375        }
376    }
377}