Skip to main content

haematite_cli/serve/storage/
merge_candidate.rs

1//! Private candidate pages share the server-owned affected-owner walk.
2
3use std::collections::BTreeMap;
4use std::num::NonZeroUsize;
5
6use haematite::branch::history::Key as HistoryKey;
7use haematite::branch::{
8    CommitDurability, CommitRequest, ForkWithChanges, ForkWrite, current_timestamp,
9    open as open_branch, remove as remove_branch,
10};
11use haematite::tree::Hash;
12use serde::{Deserialize, Serialize};
13
14use super::{HeldInputs, MergeChoice, MergeOwner, MissingChoice, Storage, Triple, coverage};
15use crate::serve::storage::{
16    Captured, DocumentRevision, Failure, MergePlan, SERVICE, counts, edges, identifier,
17    merge_holders::holder_name, reads, review::choice, revision_key, version,
18};
19
20const MARKER: &[u8] = b"b\0merge-candidate-progress";
21
22#[derive(Debug, Clone, Copy, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum CandidateAction {
25    Provisioned,
26    Confirmed,
27    Page,
28    End,
29}
30
31#[derive(Debug, Serialize)]
32pub struct CandidateAdvance {
33    pub action: CandidateAction,
34    pub page: u64,
35    pub end: bool,
36}
37
38#[derive(Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40struct Marker {
41    plan: MergePlan,
42    revision: u64,
43    page: u64,
44    end: bool,
45    position: Option<coverage::Position>,
46}
47
48#[derive(Clone, Copy)]
49enum Side {
50    Source,
51    Target,
52    Removed,
53}
54
55struct Resolved {
56    side: Side,
57    value: Option<Vec<u8>>,
58}
59
60type Writes = BTreeMap<Vec<u8>, Option<Vec<u8>>>;
61
62impl Storage {
63    /// Release a private candidate without changing its inputs or choice journal.
64    ///
65    /// A later page call provisions again at the journal's current revision.
66    ///
67    /// # Errors
68    /// Refuses invalid identities and reports uncertain reference removal explicitly.
69    pub fn merge_candidate_restart(&mut self, id: &str) -> Result<bool, Failure> {
70        identifier(id)?;
71        let name = holder_name(id, "candidate");
72        self.confirmed_candidates.remove(&name);
73        remove_branch(&name, &mut self.refs)
74            .map(|removed| removed.is_some())
75            .map_err(|error| Failure::new("candidate_restart_unknown", error.to_string()))
76    }
77
78    /// Advance a private candidate from its durable marker, never a caller cursor.
79    ///
80    /// End means the affected-owner walk ended; it is not a seal or a graph proof.
81    /// Supplied vector changes require the separate field-wide rebuild and refuse here.
82    ///
83    /// # Errors
84    /// Refuses moved inputs or choices, missing choices, invalid markers and uncertain writes.
85    pub fn merge_candidate_page(&mut self, id: &str) -> Result<CandidateAdvance, Failure> {
86        let plan = self.open_merge_plan(id)?;
87        let inputs = self.choice_inputs(&plan)?;
88        let service = self.merge_service_selection()?;
89        let revision = self.choice_revision(&service, id)?;
90        let name = holder_name(id, "candidate");
91        let Some(at) = self.candidate_selection(&name) else {
92            return self.provision_candidate(&name, plan, revision);
93        };
94        let mut marker = load_marker(self, &at)?;
95        validate_marker(&marker, &plan, revision, &at, &inputs)?;
96        if self.confirmed_candidates.get(&name) != Some(&at.version) {
97            self.refs
98                .confirm_version(&name, at.version.generation, at.version.sequence)
99                .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
100            self.confirmed_candidates.insert(name, at.version);
101            return Ok(answer(CandidateAction::Confirmed, &marker));
102        }
103        if marker.end {
104            return Ok(answer(CandidateAction::End, &marker));
105        }
106        let step = self.coverage_step(
107            id,
108            marker.position.take(),
109            NonZeroUsize::MIN.saturating_add(63),
110        )?;
111        if let Some(missing) = step.missing.first() {
112            return Err(missing_choice(missing)?);
113        }
114        if step.keys.len() > coverage::PAGE_OWNER_LIMIT {
115            return Err(Failure::new(
116                "candidate_page_budget",
117                "affected-owner batch exceeds its bound",
118            ));
119        }
120        let rows = materialise(self, id, &inputs, &service, &at, &step.keys)?;
121        marker.position = step.next;
122        marker.end = marker.position.is_none();
123        marker.page = marker.page.checked_add(1).ok_or_else(|| {
124            Failure::new("candidate_page_exhausted", "page counter cannot advance")
125        })?;
126        self.commit_candidate(&name, &at, &marker, rows)?;
127        Ok(answer(CandidateAction::Page, &marker))
128    }
129
130    fn candidate_selection(&self, name: &str) -> Option<Captured> {
131        self.refs.get(name).map(|record| Captured {
132            branch: name.to_owned(),
133            version: version(record),
134            shards: record.shards.clone(),
135        })
136    }
137
138    fn provision_candidate(
139        &mut self,
140        name: &str,
141        plan: MergePlan,
142        revision: u64,
143    ) -> Result<CandidateAdvance, Failure> {
144        let marker = Marker {
145            plan,
146            revision,
147            page: 0,
148            end: false,
149            position: None,
150        };
151        let service =
152            open_branch(SERVICE, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
153        let writes = [ForkWrite {
154            shard_id: self.writer.shard_for(MARKER),
155            key: MARKER.to_vec(),
156            value: Some(serde_json::to_vec(&marker).map_err(Failure::storage)?),
157        }];
158        self.writer
159            .fork_kept(
160                &service,
161                HistoryKey {
162                    branch: Hash::of(marker.plan.target.branch.as_bytes()),
163                    generation: marker.plan.target.version.generation,
164                    sequence: marker.plan.target.version.sequence,
165                },
166                &mut self.refs,
167                &self.registry,
168                ForkWithChanges {
169                    name,
170                    timestamp: current_timestamp(),
171                    writes: &writes,
172                },
173            )
174            .map_err(|error| {
175                Failure::new(
176                    if self.refs.get(name).is_some() {
177                        "durability_unknown"
178                    } else {
179                        "candidate_provision_refused"
180                    },
181                    error.to_string(),
182                )
183            })?;
184        let at = self.candidate_selection(name).ok_or_else(|| {
185            Failure::new(
186                "candidate_reference_missing",
187                "fork did not install its reference",
188            )
189        })?;
190        self.confirmed_candidates
191            .insert(name.to_owned(), at.version);
192        Ok(answer(CandidateAction::Provisioned, &marker))
193    }
194
195    fn commit_candidate(
196        &mut self,
197        name: &str,
198        before: &Captured,
199        marker: &Marker,
200        mut rows: Writes,
201    ) -> Result<(), Failure> {
202        rows.insert(
203            MARKER.to_vec(),
204            Some(serde_json::to_vec(marker).map_err(Failure::storage)?),
205        );
206        let branch =
207            open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
208        for (key, value) in rows {
209            let shard = self.writer.shard_for(&key);
210            match value {
211                Some(value) => branch.put(shard, key, value).map_err(Failure::branch)?,
212                None => branch.delete(shard, key).map_err(Failure::branch)?,
213            }
214        }
215        self.confirmed_candidates.remove(name);
216        self.writer
217            .commit(
218                &branch,
219                &self.registry,
220                CommitRequest {
221                    durability: CommitDurability::Durable {
222                        refs: &mut self.refs,
223                    },
224                    extra_parents: &[],
225                    timestamp: current_timestamp(),
226                },
227            )
228            .map_err(|error| {
229                Failure::new(
230                    if self
231                        .refs
232                        .get(name)
233                        .is_some_and(|record| version(record) != before.version)
234                    {
235                        "durability_unknown"
236                    } else {
237                        "candidate_write_refused"
238                    },
239                    error.to_string(),
240                )
241            })?;
242        let at = self.candidate_selection(name).ok_or_else(|| {
243            Failure::new(
244                "candidate_reference_missing",
245                "committed reference disappeared",
246            )
247        })?;
248        self.confirmed_candidates
249            .insert(name.to_owned(), at.version);
250        Ok(())
251    }
252}
253
254const fn answer(action: CandidateAction, marker: &Marker) -> CandidateAdvance {
255    CandidateAdvance {
256        action,
257        page: marker.page,
258        end: marker.end,
259    }
260}
261
262fn load_marker(storage: &Storage, at: &Captured) -> Result<Marker, Failure> {
263    let bytes = reads::get_bytes(&storage.reader, at, MARKER)?.ok_or_else(|| {
264        Failure::new(
265            "candidate_marker_missing",
266            "candidate has no progress marker",
267        )
268    })?;
269    serde_json::from_slice(&bytes)
270        .map_err(|error| Failure::new("candidate_marker_corrupt", error.to_string()))
271}
272
273fn validate_marker(
274    marker: &Marker,
275    plan: &MergePlan,
276    revision: u64,
277    at: &Captured,
278    inputs: &HeldInputs,
279) -> Result<(), Failure> {
280    if marker.plan != *plan
281        || marker.page != at.version.sequence
282        || (marker.end && marker.position.is_some())
283        || (marker.page == 0 && (marker.end || marker.position.is_some()))
284        || (!marker.end && marker.page > 0 && marker.position.is_none())
285        || !at
286            .shards
287            .iter()
288            .map(|shard| (shard.shard_id, shard.fork_anchor))
289            .eq(inputs
290                .target
291                .shards
292                .iter()
293                .map(|shard| (shard.shard_id, shard.head)))
294    {
295        return Err(Failure::new(
296            "candidate_marker_corrupt",
297            "candidate identity, anchors or progress differs",
298        ));
299    }
300    if marker.revision != revision {
301        return Err(Failure::new(
302            "choice_journal_moved",
303            "candidate is bound to another choice revision",
304        ));
305    }
306    Ok(())
307}
308
309fn missing_choice(missing: &MissingChoice) -> Result<Failure, Failure> {
310    Ok(Failure::new(
311        "choice_missing",
312        serde_json::to_string(missing).map_err(Failure::storage)?,
313    ))
314}
315
316fn embedded_choice(
317    storage: &Storage,
318    id: &str,
319    inputs: &HeldInputs,
320    service: &Captured,
321    key: &[u8],
322    values: &Triple,
323) -> Result<choice::EmbeddedSide, Failure> {
324    let Some(side) = choice::embedded_field(
325        key,
326        values.ancestor.as_deref(),
327        values.target.as_deref(),
328        values.source.as_deref(),
329    )?
330    else {
331        return Err(Failure::new(
332            "candidate_vector_rebuild_required",
333            "supplied vector fields require field-wide validation and dimensions",
334        ));
335    };
336    let token = values.token(&inputs.pins, key)?;
337    if storage.read_choice_row(service, id, &token)?.is_some() {
338        return Err(Failure::new(
339            "choice_owner_unsupported",
340            "embedded declarations do not admit a choice",
341        ));
342    }
343    Ok(side)
344}
345
346fn materialise(
347    storage: &Storage,
348    id: &str,
349    inputs: &HeldInputs,
350    service: &Captured,
351    candidate: &Captured,
352    keys: &[Vec<u8>],
353) -> Result<Writes, Failure> {
354    let mut rows = BTreeMap::new();
355    let mut kept_counts = counts::CandidateCounts::new(&storage.reader, candidate)?;
356    for key in keys {
357        if key.starts_with(b"k\0") {
358            continue;
359        }
360        // Supplied vectors require field-wide validation before they can be carried.
361        if key.starts_with(b"v\0") {
362            return Err(Failure::new(
363                "candidate_vector_rebuild_required",
364                "vector changes require field-wide validation and dimensions",
365            ));
366        }
367        let values = storage.choice_values(inputs, key)?;
368        if key.starts_with(b"w\0") {
369            let side = embedded_choice(storage, id, inputs, service, key, &values)?;
370            if side == choice::EmbeddedSide::Source {
371                rows.insert(key.clone(), values.source);
372            }
373            continue;
374        }
375        let owner = coverage::owner_key(key)?;
376        let token = values.token(&inputs.pins, key)?;
377        let row = if owner.is_some() {
378            storage.read_choice_row(service, id, &token)?
379        } else {
380            None
381        };
382        if row
383            .as_ref()
384            .is_some_and(|row| Some(&row.owner) != owner.as_ref())
385        {
386            return Err(Failure::new(
387                "choice_row_corrupt",
388                "candidate choice names another owner",
389            ));
390        }
391        let side = selected(&values, row.as_ref().map(|row| row.choice)).ok_or_else(|| {
392            Failure::new(
393                "choice_missing",
394                format!(
395                    "owner {} token {token}",
396                    String::from_utf8_lossy(key).replace('\0', "/")
397                ),
398            )
399        })?;
400        let value = match side {
401            Side::Source => values.source,
402            Side::Target => values.target,
403            Side::Removed => None,
404        };
405        let resolved = Resolved { side, value };
406        match owner {
407            Some(MergeOwner::Document {
408                collection,
409                document_id,
410            }) => {
411                let (revision, bytes) =
412                    revision_row(storage, inputs, &resolved, &collection, &document_id)?;
413                if !matches!(resolved.side, Side::Target) {
414                    kept_counts.record(
415                        &storage.reader,
416                        candidate,
417                        key,
418                        &revision,
419                        bytes.is_some(),
420                    )?;
421                }
422                rows.insert(revision, bytes);
423            }
424            Some(MergeOwner::Edge { relation, from, to }) => {
425                if let Some(value) = &resolved.value {
426                    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
427                    edges::decode_edge(b"", text, value)?;
428                }
429                let incoming = edges::keys(&from, &relation, &to)?.1;
430                if !matches!(resolved.side, Side::Target) {
431                    kept_counts.record(
432                        &storage.reader,
433                        candidate,
434                        key,
435                        key,
436                        resolved.value.is_some(),
437                    )?;
438                }
439                rows.insert(incoming, resolved.value.clone());
440            }
441            None => {}
442        }
443        rows.insert(key.clone(), resolved.value);
444    }
445    kept_counts.append(&storage.reader, candidate, &mut rows)?;
446    Ok(rows)
447}
448
449fn revision_row(
450    storage: &Storage,
451    inputs: &HeldInputs,
452    resolved: &Resolved,
453    collection: &str,
454    document_id: &str,
455) -> Result<(Vec<u8>, Option<Vec<u8>>), Failure> {
456    let at = match resolved.side {
457        Side::Source => &inputs.source,
458        Side::Target => &inputs.target,
459        Side::Removed => {
460            return Err(Failure::new(
461                "choice_row_corrupt",
462                "an edge removal is recorded for a document",
463            ));
464        }
465    };
466    let revision = revision_key(collection, document_id);
467    let bytes = if resolved.value.is_some() {
468        let bytes = reads::get_bytes(&storage.reader, at, &revision)?.ok_or_else(|| {
469            Failure::new(
470                "document_revision_missing",
471                "resolved document has no revision",
472            )
473        })?;
474        serde_json::from_slice::<DocumentRevision>(&bytes).map_err(Failure::storage)?;
475        Some(bytes)
476    } else {
477        None
478    };
479    Ok((revision, bytes))
480}
481
482fn selected(values: &Triple, choice: Option<MergeChoice>) -> Option<Side> {
483    match choice {
484        Some(MergeChoice::TakeSource) => Some(Side::Source),
485        Some(MergeChoice::TakeTarget) => Some(Side::Target),
486        Some(MergeChoice::RemoveEdge) => Some(Side::Removed),
487        None if values.ancestor == values.target => Some(Side::Source),
488        None if values.ancestor == values.source || values.target == values.source => {
489            Some(Side::Target)
490        }
491        None => None,
492    }
493}
494
495#[cfg(test)]
496#[path = "merge_candidate_tests.rs"]
497mod tests;