nodedb_physical/physical_plan/document/resolved_mutation.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! The decided mutation set a governed document write resolved to.
4//!
5//! An RLS-governed write can't cross Raft carrying the live predicate — a
6//! follower has no writing identity to judge it against. The Control Plane
7//! resolves the write and policy against the rows the Data Plane holds, then
8//! ships the mutations themselves. `value` is always pre-encode MessagePack;
9//! `precondition` is always raw stored bytes (Binary Tuple or MessagePack).
10//! Swapping the two forms fails silently or never matches — never mix them.
11
12use nodedb_types::{QualifiedCollection, Surrogate};
13
14use super::sum_target::ResolvedSumTarget;
15
16/// One row mutation a resolved document write applies.
17///
18/// `precondition` is the resolve→apply drift check, not business logic:
19/// `None` requires the row absent, `Some(bytes)` requires it hold exactly
20/// `bytes` (`==`). A surrogate-existence check would miss a concurrent write
21/// that changed content without deleting the row — the lost update this
22/// precondition prevents.
23#[derive(
24 Debug,
25 Clone,
26 PartialEq,
27 serde::Serialize,
28 serde::Deserialize,
29 zerompk::ToMessagePack,
30 zerompk::FromMessagePack,
31)]
32pub enum DocumentResolvedMutation {
33 /// Store `value` for `document_id`, replacing whatever is there.
34 Put {
35 collection: QualifiedCollection,
36 /// User-facing primary key, as `RETURNING` and CDC name the row.
37 document_id: String,
38 /// Catalog-bound identity; the handler hex-encodes it for the row key.
39 surrogate: Surrogate,
40 /// Raw primary-key bytes, for follower-side WAL decode rebind.
41 pk_bytes: Vec<u8>,
42 /// Pre-encode MessagePack body — see this module's doc.
43 value: Vec<u8>,
44 /// Raw stored bytes the resolve read, or `None` for an absent row.
45 precondition: Option<Vec<u8>>,
46 /// `(target collection, join value)` → target row surrogate,
47 /// resolved by the Control Plane (Data Plane has no PK→surrogate map).
48 resolved_sum_targets: Vec<ResolvedSumTarget>,
49 },
50 /// Remove `document_id`.
51 Delete {
52 collection: QualifiedCollection,
53 document_id: String,
54 surrogate: Surrogate,
55 pk_bytes: Vec<u8>,
56 /// Raw stored pre-image the resolve read and decided the policy against.
57 precondition: Option<Vec<u8>>,
58 /// See [`DocumentResolvedMutation::Put::resolved_sum_targets`].
59 resolved_sum_targets: Vec<ResolvedSumTarget>,
60 },
61}
62
63impl DocumentResolvedMutation {
64 /// The collection this one mutation targets.
65 pub fn collection(&self) -> &QualifiedCollection {
66 match self {
67 DocumentResolvedMutation::Put { collection, .. }
68 | DocumentResolvedMutation::Delete { collection, .. } => collection,
69 }
70 }
71
72 /// The user-facing primary key this one mutation targets.
73 pub fn document_id(&self) -> &str {
74 match self {
75 DocumentResolvedMutation::Put { document_id, .. }
76 | DocumentResolvedMutation::Delete { document_id, .. } => document_id.as_str(),
77 }
78 }
79
80 /// The row identity this one mutation targets.
81 pub fn surrogate(&self) -> Surrogate {
82 match self {
83 DocumentResolvedMutation::Put { surrogate, .. }
84 | DocumentResolvedMutation::Delete { surrogate, .. } => *surrogate,
85 }
86 }
87
88 /// The state this mutation requires the row to be in before it applies.
89 pub fn precondition(&self) -> Option<&[u8]> {
90 match self {
91 DocumentResolvedMutation::Put { precondition, .. }
92 | DocumentResolvedMutation::Delete { precondition, .. } => precondition.as_deref(),
93 }
94 }
95}
96
97/// What `DocumentOp::ResolveWrite` reports back for a point/bulk write: every
98/// mutation the intercepted write applies, plus the exact response payload the
99/// statement returns once they all apply cleanly.
100///
101/// An empty `mutations` list is a legitimate outcome — a `PointUpdate` whose
102/// row is already gone writes nothing and still owes the caller its
103/// `{"affected": 0}` reply.
104#[derive(
105 Debug,
106 Clone,
107 PartialEq,
108 serde::Serialize,
109 serde::Deserialize,
110 zerompk::ToMessagePack,
111 zerompk::FromMessagePack,
112)]
113pub struct DocumentResolveOutcome {
114 pub mutations: Vec<DocumentResolvedMutation>,
115 pub response_payload: Vec<u8>,
116}