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}