Skip to main content

nodedb_types/
calvin.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Primitive Calvin scheduling types shared between `nodedb-physical`
4//! (the physical-plan IR layer) and `nodedb-cluster` (the distributed
5//! Calvin sequencer / scheduler).
6//!
7//! Provides [`SortedVec`], [`EngineKeySet`], and [`PassiveReadKey`] —
8//! the building blocks of Calvin read/write sets. `DependentReadSpec`
9//! and other scheduler-internal aggregates stay in `nodedb-cluster`.
10
11use serde::{Deserialize, Serialize};
12
13use crate::{KeyRepr, WriteVersion};
14
15pub use crate::calvin_passive::{PassiveKey, PassiveReadKeyId};
16
17/// A newtype over `Vec<T>` that guarantees sorted, deduplicated contents.
18///
19/// Constructed via [`SortedVec::new`], which sorts and deduplicates at
20/// construction time. This property is load-bearing for byte-determinism:
21/// two `SortedVec`s built from the same logical set (in any insertion order)
22/// produce identical serialized bytes.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct SortedVec<T>(Vec<T>);
25
26impl<T: zerompk::ToMessagePack> zerompk::ToMessagePack for SortedVec<T> {
27    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
28        self.0.write(writer)
29    }
30}
31
32impl<'de, T> zerompk::FromMessagePack<'de> for SortedVec<T>
33where
34    T: zerompk::FromMessagePack<'de> + Ord + Clone,
35{
36    fn read<R: zerompk::Read<'de>>(reader: &mut R) -> zerompk::Result<Self> {
37        let v = Vec::<T>::read(reader)?;
38        Ok(Self::new(v))
39    }
40}
41
42impl<T: Ord + Clone> SortedVec<T> {
43    /// Build from any slice. Sorts and deduplicates in place.
44    pub fn new(mut items: Vec<T>) -> Self {
45        items.sort();
46        items.dedup();
47        Self(items)
48    }
49
50    pub fn as_slice(&self) -> &[T] {
51        &self.0
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.0.is_empty()
56    }
57
58    pub fn len(&self) -> usize {
59        self.0.len()
60    }
61
62    pub fn iter(&self) -> std::slice::Iter<'_, T> {
63        self.0.iter()
64    }
65}
66
67impl<T: Ord + Clone> From<Vec<T>> for SortedVec<T> {
68    fn from(v: Vec<T>) -> Self {
69        Self::new(v)
70    }
71}
72
73/// A typed key set for one engine within a read or write set.
74///
75/// Keys are normalized to surrogates (or byte keys for KV) at admission, so
76/// all engine-specific naming is resolved upstream of the sequencer.
77#[derive(
78    Debug,
79    Clone,
80    PartialEq,
81    Eq,
82    Serialize,
83    Deserialize,
84    zerompk::ToMessagePack,
85    zerompk::FromMessagePack,
86)]
87pub enum EngineKeySet {
88    /// Document engine (schemaless or strict): identified by surrogate.
89    Document {
90        collection: String,
91        surrogates: SortedVec<u32>,
92    },
93    /// Vector engine: identified by surrogate.
94    Vector {
95        collection: String,
96        surrogates: SortedVec<u32>,
97    },
98    /// Key-Value engine: identified by raw byte keys.
99    Kv {
100        collection: String,
101        keys: SortedVec<Vec<u8>>,
102    },
103    /// Graph edge engine: identified by (src_surrogate, dst_surrogate) pairs.
104    ///
105    /// `edges` carries the surrogate-pair IDENTITY used for Calvin locking and
106    /// conflict detection. `home_vshards` carries the from_key ROUTING homes —
107    /// the set of `VShardId::from_key(src_str)` / `from_key(dst_str)` u32 ids
108    /// for every edge in this set. Routing (which vShards participate) is driven
109    /// by `home_vshards`, NOT by the collection name, because a graph edge is
110    /// dual-homed across its two endpoint key-hashed vShards.
111    Edge {
112        collection: String,
113        edges: SortedVec<(u32, u32)>,
114        home_vshards: SortedVec<u32>,
115    },
116    /// Array engine: the array's cells on each vShard in `vshards`.
117    ///
118    /// An array is tile-partitioned: its cells live on the vShards their
119    /// tiles hash to, not on the array's collection home. Each vShard in
120    /// `vshards` participates, and the transaction locks the whole array on
121    /// it. Appended last: the encoding is positional.
122    Array {
123        collection: String,
124        vshards: SortedVec<u32>,
125    },
126    /// A whole collection on each vShard in `vshards`, or on the
127    /// collection's own vShard when `vshards` is empty.
128    ///
129    /// In a write set the transaction locks the collection exclusively: a
130    /// truncate, bulk, predicate, or identity-less write. In a read set it
131    /// locks the collection shared: a predicate, scan, or index read. A read
132    /// that observed the collection on named vShards, such as a cross-shard
133    /// graph read, names them in `vshards`.
134    Collection {
135        collection: String,
136        vshards: SortedVec<u32>,
137    },
138    /// UNIQUE values a write claims in one index of `collection`.
139    ///
140    /// Each value is the canonical index value the Data Plane judges. Two
141    /// transactions that claim one value lock one key, so they serialize.
142    Unique {
143        collection: String,
144        index: String,
145        values: SortedVec<Vec<u8>>,
146    },
147}
148
149impl EngineKeySet {
150    /// O(1) estimate of the serialized byte size of this key set.
151    ///
152    /// Used by the dependent-read cap check at sequencer admission to bound
153    /// the total bytes that would be Raft-replicated in a `CalvinReadResult`
154    /// entry.  This is an estimate, not an exact count; do NOT use it as a
155    /// correctness check — only as a pre-flight guard.
156    pub fn serialized_size_hint(&self) -> usize {
157        match self {
158            // u32 surrogates: 4 bytes each.
159            Self::Document { surrogates, .. } | Self::Vector { surrogates, .. } => {
160                surrogates.len() * 4
161            }
162            // KV keys: sum of key byte lengths.
163            Self::Kv { keys, .. } => keys.iter().map(|k| k.len()).sum(),
164            // Edge: two u32 per edge = 8 bytes each.
165            Self::Edge { edges, .. } => edges.len() * 8,
166            // Array: one u32 vShard each.
167            Self::Array { vshards, .. } => vshards.len() * 4,
168            // Collection: the name plus one u32 vShard each.
169            Self::Collection {
170                collection,
171                vshards,
172            } => collection.len() + vshards.len() * 4,
173            // Unique: the index name plus each value's bytes.
174            Self::Unique { index, values, .. } => {
175                index.len() + values.iter().map(|v| v.len()).sum::<usize>()
176            }
177        }
178    }
179
180    /// The collection this key set belongs to.
181    pub fn collection(&self) -> &str {
182        match self {
183            Self::Document { collection, .. }
184            | Self::Vector { collection, .. }
185            | Self::Kv { collection, .. }
186            | Self::Edge { collection, .. }
187            | Self::Array { collection, .. }
188            | Self::Collection { collection, .. }
189            | Self::Unique { collection, .. } => collection,
190        }
191    }
192
193    /// Returns `true` if this key set contains no keys.
194    pub fn is_empty(&self) -> bool {
195        match self {
196            Self::Document { surrogates, .. } => surrogates.is_empty(),
197            Self::Vector { surrogates, .. } => surrogates.is_empty(),
198            Self::Kv { keys, .. } => keys.is_empty(),
199            Self::Edge { edges, .. } => edges.is_empty(),
200            Self::Array { vshards, .. } => vshards.is_empty(),
201            Self::Collection { .. } => false,
202            Self::Unique { values, .. } => values.is_empty(),
203        }
204    }
205}
206
207/// A key set a passive participant reads and broadcasts.
208///
209/// Wraps an [`EngineKeySet`]. A passive vShard reads document rows by
210/// surrogate and key-value rows by key. It refuses every other key set. The
211/// sequencer bounds the bytes a transaction reads passively through
212/// `DependentReadSpec::total_bytes()` in `nodedb-cluster`.
213#[derive(
214    Debug,
215    Clone,
216    PartialEq,
217    Eq,
218    Serialize,
219    Deserialize,
220    zerompk::ToMessagePack,
221    zerompk::FromMessagePack,
222)]
223pub struct PassiveReadKey {
224    /// The engine key set to read on the passive vshard.
225    pub engine_key: EngineKeySet,
226}
227
228/// Which peer engine served a read. Mirrors the top-level physical-plan
229/// engine variants one-to-one so the classifier is total and a new engine
230/// forces a decision at compile time.
231///
232/// Encoded as a plain integer discriminant (`#[msgpack(c_enum)]`): stable,
233/// compact, and comparable across the wire. The discriminant assignment is
234/// load-bearing for on-wire stability — do NOT reorder variants.
235#[derive(
236    Debug,
237    Clone,
238    Copy,
239    PartialEq,
240    Eq,
241    Serialize,
242    Deserialize,
243    zerompk::ToMessagePack,
244    zerompk::FromMessagePack,
245)]
246#[msgpack(c_enum)]
247pub enum EngineTag {
248    Vector,
249    Graph,
250    Document,
251    Kv,
252    Text,
253    Columnar,
254    Timeseries,
255    Spatial,
256    Crdt,
257    Query,
258    Meta,
259    Array,
260    ClusterArray,
261}
262
263/// The identity a read observed within a collection, carried on the
264/// replicated Calvin `TxClass`.
265///
266/// `Point` carries the exact row identity ([`KeyRepr`]) for a keyed lookup
267/// (per-key optimistic-concurrency validation). `Predicate` is the coarse,
268/// collection-scoped observation for scans / searches / aggregates — safe
269/// against phantoms, never under-approximating. `IndexEq` / `IndexRange` carry
270/// the indexed dimension of a secondary-index equality / range read (canonical
271/// stringified index value, identical to the index-key segment) for narrower
272/// per-value validation.
273///
274/// New variants are APPENDED only — the on-wire encoding is positional, so
275/// reordering would break cross-version decode. Do NOT reorder variants.
276#[derive(
277    Debug,
278    Clone,
279    PartialEq,
280    Eq,
281    Serialize,
282    Deserialize,
283    zerompk::ToMessagePack,
284    zerompk::FromMessagePack,
285)]
286pub enum ReadKeyIdent {
287    /// A single-row keyed observation.
288    Point(KeyRepr),
289    /// A collection-scoped predicate observation.
290    Predicate,
291    /// A secondary-index equality observation on one indexed field.
292    IndexEq { field: String, value: String },
293    /// A secondary-index range observation on one indexed field. `lo`/`hi` are
294    /// optional so a one-sided native range is representable; both `None` is
295    /// never emitted.
296    IndexRange {
297        field: String,
298        lo: Option<String>,
299        hi: Option<String>,
300    },
301}
302
303/// One versioned, predicate-aware read observed by a transaction, carried on
304/// the replicated Calvin `TxClass` so participants can validate it at the
305/// commit serialization point.
306///
307/// `read_version` is a data-group log position, so any replica of the read's
308/// vShard validates it. The enclosing `TxClass` scopes the tenant;
309/// per-database scoping is carried by the transaction as a whole.
310#[derive(
311    Debug,
312    Clone,
313    PartialEq,
314    Eq,
315    Serialize,
316    Deserialize,
317    zerompk::ToMessagePack,
318    zerompk::FromMessagePack,
319)]
320pub struct VersionedReadEntry {
321    /// Which engine served the read.
322    pub engine: EngineTag,
323    /// The collection the read observed.
324    pub collection: String,
325    /// Point-key or collection-scoped-predicate identity of the observation.
326    pub key: ReadKeyIdent,
327    /// The version of what the read observed on its home vShard.
328    pub read_version: WriteVersion,
329    /// The vShard whose write versions validate this read. `None` homes the
330    /// read to its collection's vShard. A graph read names the key vShard it
331    /// read edges on, because edges live on their endpoints' vShards. A homed
332    /// read with an empty `collection` observed every collection there, so it
333    /// validates against the vShard's latest version.
334    pub home_vshard: Option<u32>,
335}
336
337/// The versioned read-set of a Calvin transaction.
338///
339/// Empty for pure-write transactions and for autocommit statements (which
340/// accumulate no session read-set). Populated at commit time from the neutral
341/// session read-set, and validated per-participant against the local
342/// write-version index at Calvin stage time (the commit vote on
343/// a stage response).
344#[derive(
345    Debug,
346    Clone,
347    Default,
348    PartialEq,
349    Eq,
350    Serialize,
351    Deserialize,
352    zerompk::ToMessagePack,
353    zerompk::FromMessagePack,
354)]
355pub struct VersionedReadSet(pub Vec<VersionedReadEntry>);
356
357impl VersionedReadSet {
358    /// Build from a vector of entries.
359    pub fn new(entries: Vec<VersionedReadEntry>) -> Self {
360        Self(entries)
361    }
362
363    /// Returns `true` if no reads were recorded.
364    pub fn is_empty(&self) -> bool {
365        self.0.is_empty()
366    }
367
368    /// Number of recorded read entries.
369    pub fn len(&self) -> usize {
370        self.0.len()
371    }
372
373    /// Iterate the recorded read entries.
374    pub fn iter(&self) -> std::slice::Iter<'_, VersionedReadEntry> {
375        self.0.iter()
376    }
377
378    /// Borrow the entries as a slice.
379    pub fn as_slice(&self) -> &[VersionedReadEntry] {
380        &self.0
381    }
382}