Skip to main content

nodedb_lite/engine/vector/
row.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The CRDT row a vector is attached to.
4//!
5//! A vector is an attachment to the row with the same collection and id. A
6//! vector write merges its own fields into that row and never replaces the
7//! row's other fields. A vector delete removes those fields again, and
8//! removes the whole row only when nothing but the vector lives in it.
9
10use loro::LoroValue;
11use nodedb_types::collection_config::PrimaryEngine;
12use nodedb_types::sync::wire::CollectionDescriptor;
13
14use crate::engine::crdt::CrdtEngine;
15use crate::engine::vector::AttachedVectors;
16use crate::error::LiteError;
17use crate::nodedb::collection::CollectionMeta;
18use crate::storage::engine::StorageEngine;
19
20/// The vector's dimension, written on every vector row.
21pub(crate) const EMBEDDING_DIM_FIELD: &str = "embedding_dim";
22
23/// The named vector a row carries, written by named-vector inserts.
24pub(crate) const VECTOR_FIELD_TAG: &str = "__field";
25
26/// Every row field a vector write owns. None of them is document data.
27pub(crate) const VECTOR_ROW_FIELDS: &[&str] = &[EMBEDDING_DIM_FIELD, VECTOR_FIELD_TAG];
28
29/// Whether `collection` is declared vector-primary: its rows exist for their
30/// vectors, so deleting the vector deletes the row.
31///
32/// Reads the persisted collection descriptor. A collection with no persisted
33/// descriptor is not declared vector-primary. Fails when reading storage
34/// fails.
35pub(crate) async fn is_vector_primary<S: StorageEngine>(
36    storage: &S,
37    collection: &str,
38) -> Result<bool, LiteError> {
39    let key = format!("collection:{collection}");
40    let Some(bytes) = storage
41        .get(nodedb_types::Namespace::Meta, key.as_bytes())
42        .await?
43    else {
44        return Ok(false);
45    };
46    let Ok(meta) = sonic_rs::from_slice::<CollectionMeta>(&bytes) else {
47        return Ok(false);
48    };
49    let Some(descriptor) = meta
50        .descriptor_json
51        .as_deref()
52        .and_then(|json| sonic_rs::from_str::<CollectionDescriptor>(json).ok())
53    else {
54        return Ok(false);
55    };
56    Ok(matches!(descriptor.primary, PrimaryEngine::Vector) || descriptor.vector_primary.is_some())
57}
58
59/// Which of a row's vectors a detach removes.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) enum VectorSlot<'a> {
62    /// The base vector, in index `"{collection}"`.
63    Base,
64    /// A named vector, in index `"{collection}:{field}"`.
65    Named(&'a str),
66}
67
68/// Detach one vector from `doc_id`'s row in `collection`, after its index
69/// binding is gone. `remaining` lists the vectors still attached to the row.
70///
71/// Only fields the detached vector owns alone go; document fields stay:
72/// - No vector remains: `embedding_dim` and `__field` go. The whole row goes
73///   when `vector_primary` is set or no document field remains.
74/// - A vector remains: `embedding_dim` stays with it. Detaching the last
75///   named vector removes `__field`; detaching the named vector `__field`
76///   names retags it to a remaining named vector. Detaching the base vector
77///   changes nothing.
78///
79/// Emits at most one CRDT delta. Returns its mutation ID, or 0 when nothing
80/// changed.
81pub(crate) fn detach_vector_row(
82    crdt: &mut CrdtEngine,
83    collection: &str,
84    doc_id: &str,
85    detached: VectorSlot<'_>,
86    remaining: &AttachedVectors,
87    vector_primary: bool,
88) -> Result<u64, LiteError> {
89    let Some(row) = crdt.read(collection, doc_id) else {
90        return Ok(0);
91    };
92    if !remaining.any() {
93        if vector_primary || !has_document_fields(&row) {
94            return crdt.delete(collection, doc_id);
95        }
96        let (_, mutation_id) = crdt.remove_fields(collection, doc_id, VECTOR_ROW_FIELDS)?;
97        return Ok(mutation_id);
98    }
99    let VectorSlot::Named(field) = detached else {
100        return Ok(0);
101    };
102    match remaining.named.first() {
103        None => {
104            let (_, mutation_id) = crdt.remove_fields(collection, doc_id, &[VECTOR_FIELD_TAG])?;
105            Ok(mutation_id)
106        }
107        Some(other) if field_tag(&row) == Some(field) => crdt.set_fields(
108            collection,
109            doc_id,
110            &[(VECTOR_FIELD_TAG, LoroValue::String(other.as_str().into()))],
111        ),
112        Some(_) => Ok(0),
113    }
114}
115
116/// The named vector `row`'s `__field` records.
117fn field_tag(row: &LoroValue) -> Option<&str> {
118    match row {
119        LoroValue::Map(map) => match map.get(VECTOR_FIELD_TAG) {
120            Some(LoroValue::String(tag)) => Some(tag.as_str()),
121            _ => None,
122        },
123        _ => None,
124    }
125}
126
127/// Whether `row` holds any field a vector write does not own.
128fn has_document_fields(row: &LoroValue) -> bool {
129    match row {
130        LoroValue::Map(map) => map.keys().any(|k| !VECTOR_ROW_FIELDS.contains(&k.as_str())),
131        _ => false,
132    }
133}