nodedb_physical/physical_plan/vector/resolved_mutation.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! The decided mutation set a governed vector-primary write resolved to.
4//!
5//! A vector-primary `DELETE` / `UPDATE` / conflict-patching `UPSERT` on a
6//! collection with a row-level write policy cannot cross the Raft wire
7//! carrying the live predicate: a follower has no writing identity to decide
8//! it against. The Control Plane resolves the write against the rows the
9//! Data Plane holds, decides the policy there, and ships the row mutations
10//! themselves.
11
12use nodedb_types::Surrogate;
13
14/// One row mutation a resolved vector-primary write applies.
15///
16/// Every variant carries the sidecar bytes the resolve read for its row —
17/// the apply's drift check. A row's sidecar is its `zerompk` TAGGED payload
18/// map and is never empty once written, so `Vec::new()` on `Delete` /
19/// `Update` names a bound node with no sidecar row behind it, and the apply
20/// compares it against an absent sidecar.
21#[derive(
22 Debug,
23 Clone,
24 PartialEq,
25 serde::Serialize,
26 serde::Deserialize,
27 zerompk::ToMessagePack,
28 zerompk::FromMessagePack,
29)]
30pub enum VectorResolvedMutation {
31 /// Remove `surrogate`'s HNSW node, bitmap entries, and sidecar.
32 Delete {
33 surrogate: Surrogate,
34 /// Sidecar bytes the resolve read; the node must still be bound and
35 /// hold exactly these bytes.
36 old_payload: Vec<u8>,
37 },
38 /// Rewrite `surrogate`'s row: a `new_vector` rebuilds the HNSW node
39 /// under the same surrogate, `merged_payload` replaces the sidecar and
40 /// moves the bitmap entries.
41 Update {
42 surrogate: Surrogate,
43 new_vector: Option<Vec<f32>>,
44 /// The full post-image sidecar, already merged and lower-cased,
45 /// in the sidecar's own `zerompk` TAGGED encoding.
46 merged_payload: Vec<u8>,
47 /// Sidecar bytes the resolve read; see `Delete::old_payload`.
48 old_payload: Vec<u8>,
49 },
50 /// Store a whole row under `surrogate`, replacing any existing one.
51 Upsert {
52 surrogate: Surrogate,
53 /// UTF-8 of the declared primary-key value. Followers bind the
54 /// leader-assigned surrogate to this exact key.
55 pk_bytes: Vec<u8>,
56 vector: Vec<f32>,
57 /// The sidecar to store, in its own `zerompk` TAGGED encoding.
58 payload: Vec<u8>,
59 /// `None` requires the surrogate to still be unbound; `Some(bytes)`
60 /// requires a bound node whose sidecar holds exactly `bytes`.
61 old_payload: Option<Vec<u8>>,
62 },
63}
64
65impl VectorResolvedMutation {
66 /// The surrogate this one mutation targets.
67 pub fn surrogate(&self) -> Surrogate {
68 match self {
69 VectorResolvedMutation::Delete { surrogate, .. }
70 | VectorResolvedMutation::Update { surrogate, .. }
71 | VectorResolvedMutation::Upsert { surrogate, .. } => *surrogate,
72 }
73 }
74
75 /// The vector this mutation stores, when it stores one.
76 pub fn stored_vector(&self) -> Option<&[f32]> {
77 match self {
78 VectorResolvedMutation::Delete { .. } => None,
79 VectorResolvedMutation::Update { new_vector, .. } => new_vector.as_deref(),
80 VectorResolvedMutation::Upsert { vector, .. } => Some(vector.as_slice()),
81 }
82 }
83}
84
85/// What `VectorOp::ResolveDirectWrite` reports back: every row mutation the
86/// intercepted write applies, plus the exact response payload the statement
87/// returns once they all apply cleanly.
88///
89/// An empty `mutations` list is a legitimate outcome — a predicate that
90/// matches no row writes nothing and still owes its `{"affected": 0}` reply.
91#[derive(
92 Debug,
93 Clone,
94 PartialEq,
95 serde::Serialize,
96 serde::Deserialize,
97 zerompk::ToMessagePack,
98 zerompk::FromMessagePack,
99)]
100pub struct VectorResolveOutcome {
101 pub mutations: Vec<VectorResolvedMutation>,
102 pub response_payload: Vec<u8>,
103}