Skip to main content

prolly/prolly/
patch.rs

1//! Portable logical patches and format-bound structural patch envelopes.
2
3use serde::{Deserialize, Serialize};
4
5use super::cid::Cid;
6use super::error::{Error, Mutation};
7use super::store::Store;
8use super::{Prolly, Tree};
9
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11pub enum LogicalPatch {
12    Upsert {
13        key: Vec<u8>,
14        old: Option<Vec<u8>>,
15        new: Vec<u8>,
16    },
17    Delete {
18        key: Vec<u8>,
19        old: Vec<u8>,
20    },
21}
22
23impl LogicalPatch {
24    pub fn key(&self) -> &[u8] {
25        match self {
26            Self::Upsert { key, .. } | Self::Delete { key, .. } => key,
27        }
28    }
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32pub enum StructuralEdit {
33    Point(LogicalPatch),
34    Subtree {
35        start_exclusive: Option<Vec<u8>>,
36        end_inclusive: Vec<u8>,
37        level: u16,
38        cid: Option<Cid>,
39        logical_count: u64,
40    },
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
44pub struct StructuralPatch {
45    pub base_root: Option<Cid>,
46    pub format_digest: Cid,
47    pub edits: Vec<StructuralEdit>,
48}
49
50impl StructuralPatch {
51    pub fn validate(&self) -> Result<(), Error> {
52        if let [StructuralEdit::Subtree {
53            start_exclusive: None,
54            end_inclusive,
55            level,
56            cid,
57            logical_count,
58        }] = self.edits.as_slice()
59        {
60            if end_inclusive.is_empty() {
61                let valid_root = match cid {
62                    Some(_) => *logical_count == 0 && *level == 0,
63                    None => *logical_count == 0 && *level == 0,
64                };
65                if !valid_root {
66                    return Err(Error::InvalidStructuralPatch(
67                        "invalid root subtree replacement".to_string(),
68                    ));
69                }
70                return Ok(());
71            }
72        }
73
74        let mut previous_end: Option<&[u8]> = None;
75        for edit in &self.edits {
76            let (start, end) = match edit {
77                StructuralEdit::Point(point) => (None, point.key()),
78                StructuralEdit::Subtree {
79                    start_exclusive,
80                    end_inclusive,
81                    level,
82                    cid,
83                    logical_count,
84                } => {
85                    if *level == 0
86                        || *logical_count == 0
87                        || cid.is_none()
88                        || start_exclusive
89                            .as_ref()
90                            .map(|start| start >= end_inclusive)
91                            .unwrap_or(false)
92                    {
93                        return Err(Error::InvalidStructuralPatch(
94                            "invalid subtree replacement".to_string(),
95                        ));
96                    }
97                    (start_exclusive.as_deref(), end_inclusive.as_slice())
98                }
99            };
100            if previous_end
101                .map(|previous| previous >= end)
102                .unwrap_or(false)
103                || start
104                    .zip(previous_end)
105                    .map(|(start, previous)| start < previous)
106                    .unwrap_or(false)
107            {
108                return Err(Error::InvalidStructuralPatch(
109                    "patch edits must be strictly ordered and non-overlapping".to_string(),
110                ));
111            }
112            previous_end = Some(end);
113        }
114        Ok(())
115    }
116}
117
118impl<S: Store> Prolly<S> {
119    /// Produce a format-bound patch that reuses the target's content-addressed
120    /// root subtree.
121    ///
122    /// The referenced target subtree must already exist in the destination
123    /// store before application. Snapshot synchronization can transfer missing
124    /// nodes independently, while same-store version operations only need this
125    /// compact root envelope.
126    pub fn diff_patch(&self, base: &Tree, target: &Tree) -> Result<StructuralPatch, Error> {
127        if base.config.format != target.config.format {
128            return Err(Error::PatchBaseMismatch);
129        }
130        let edits = if base.root == target.root {
131            Vec::new()
132        } else if let Some(cid) = &target.root {
133            vec![StructuralEdit::Subtree {
134                start_exclusive: None,
135                end_inclusive: Vec::new(),
136                level: 0,
137                cid: Some(cid.clone()),
138                logical_count: 0,
139            }]
140        } else {
141            vec![StructuralEdit::Subtree {
142                start_exclusive: None,
143                end_inclusive: Vec::new(),
144                level: 0,
145                cid: None,
146                logical_count: 0,
147            }]
148        };
149        Ok(StructuralPatch {
150            base_root: base.root.clone(),
151            format_digest: self.format_digest()?,
152            edits,
153        })
154    }
155
156    /// Validate and apply a patch through the canonical writer.
157    pub fn apply_patch(&self, base: &Tree, patch: &StructuralPatch) -> Result<Tree, Error> {
158        patch.validate()?;
159        if patch.base_root != base.root || patch.format_digest != self.format_digest()? {
160            return Err(Error::PatchBaseMismatch);
161        }
162
163        if patch.edits.is_empty() {
164            return Ok(base.clone());
165        }
166        if let [StructuralEdit::Subtree {
167            start_exclusive: None,
168            end_inclusive,
169            level: _,
170            cid,
171            logical_count,
172        }] = patch.edits.as_slice()
173        {
174            if end_inclusive.is_empty() {
175                let target = Tree {
176                    root: cid.clone(),
177                    config: base.config.clone(),
178                };
179                match cid {
180                    Some(cid) => {
181                        let _ = self.load_arc_ready(cid)?;
182                    }
183                    None if *logical_count == 0 => {}
184                    None => {
185                        return Err(Error::InvalidStructuralPatch(
186                            "empty root subtree metadata is inconsistent".to_string(),
187                        ));
188                    }
189                }
190                return Ok(target);
191            }
192        }
193
194        let keys = patch
195            .edits
196            .iter()
197            .filter_map(|edit| match edit {
198                StructuralEdit::Point(point) => Some(point.key()),
199                StructuralEdit::Subtree { .. } => None,
200            })
201            .collect::<Vec<_>>();
202        if keys.len() != patch.edits.len() {
203            return Err(Error::InvalidStructuralPatch(
204                "partial subtree replacement requires an imported subtree plan".to_string(),
205            ));
206        }
207        let existing = self.get_many(base, &keys)?;
208        let mut mutations = Vec::with_capacity(patch.edits.len());
209        for (edit, existing) in patch.edits.iter().zip(existing) {
210            let point = match edit {
211                StructuralEdit::Point(point) => point,
212                StructuralEdit::Subtree { .. } => {
213                    return Err(Error::InvalidStructuralPatch(
214                        "subtree replacement requires a verified imported subtree".to_string(),
215                    ));
216                }
217            };
218            match point {
219                LogicalPatch::Upsert { key, old, new } => {
220                    if existing != *old {
221                        return Err(Error::PatchBaseMismatch);
222                    }
223                    mutations.push(Mutation::Upsert {
224                        key: key.clone(),
225                        val: new.clone(),
226                    });
227                }
228                LogicalPatch::Delete { key, old } => {
229                    if existing.as_ref() != Some(old) {
230                        return Err(Error::PatchBaseMismatch);
231                    }
232                    mutations.push(Mutation::Delete { key: key.clone() });
233                }
234            }
235        }
236        self.batch(base, mutations)
237    }
238}