Skip to main content

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}