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                    note: None,
173                },
174            )
175            .map_err(|error| {
176                Failure::new(
177                    if self.refs.get(name).is_some() {
178                        "durability_unknown"
179                    } else {
180                        "candidate_provision_refused"
181                    },
182                    error.to_string(),
183                )
184            })?;
185        let at = self.candidate_selection(name).ok_or_else(|| {
186            Failure::new(
187                "candidate_reference_missing",
188                "fork did not install its reference",
189            )
190        })?;
191        self.confirmed_candidates
192            .insert(name.to_owned(), at.version);
193        Ok(answer(CandidateAction::Provisioned, &marker))
194    }
195
196    fn commit_candidate(
197        &mut self,
198        name: &str,
199        before: &Captured,
200        marker: &Marker,
201        mut rows: Writes,
202    ) -> Result<(), Failure> {
203        rows.insert(
204            MARKER.to_vec(),
205            Some(serde_json::to_vec(marker).map_err(Failure::storage)?),
206        );
207        let branch =
208            open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
209        for (key, value) in rows {
210            let shard = self.writer.shard_for(&key);
211            match value {
212                Some(value) => branch.put(shard, key, value).map_err(Failure::branch)?,
213                None => branch.delete(shard, key).map_err(Failure::branch)?,
214            }
215        }
216        self.confirmed_candidates.remove(name);
217        self.writer
218            .commit(
219                &branch,
220                &self.registry,
221                CommitRequest {
222                    durability: CommitDurability::Durable {
223                        refs: &mut self.refs,
224                    },
225                    extra_parents: &[],
226                    timestamp: current_timestamp(),
227                    note: None,
228                },
229            )
230            .map_err(|error| {
231                Failure::new(
232                    if self
233                        .refs
234                        .get(name)
235                        .is_some_and(|record| version(record) != before.version)
236                    {
237                        "durability_unknown"
238                    } else {
239                        "candidate_write_refused"
240                    },
241                    error.to_string(),
242                )
243            })?;
244        let at = self.candidate_selection(name).ok_or_else(|| {
245            Failure::new(
246                "candidate_reference_missing",
247                "committed reference disappeared",
248            )
249        })?;
250        self.confirmed_candidates
251            .insert(name.to_owned(), at.version);
252        Ok(())
253    }
254}
255
256const fn answer(action: CandidateAction, marker: &Marker) -> CandidateAdvance {
257    CandidateAdvance {
258        action,
259        page: marker.page,
260        end: marker.end,
261    }
262}
263
264fn load_marker(storage: &Storage, at: &Captured) -> Result<Marker, Failure> {
265    let bytes = reads::get_bytes(&storage.reader, at, MARKER)?.ok_or_else(|| {
266        Failure::new(
267            "candidate_marker_missing",
268            "candidate has no progress marker",
269        )
270    })?;
271    serde_json::from_slice(&bytes)
272        .map_err(|error| Failure::new("candidate_marker_corrupt", error.to_string()))
273}
274
275fn validate_marker(
276    marker: &Marker,
277    plan: &MergePlan,
278    revision: u64,
279    at: &Captured,
280    inputs: &HeldInputs,
281) -> Result<(), Failure> {
282    if marker.plan != *plan
283        || marker.page != at.version.sequence
284        || (marker.end && marker.position.is_some())
285        || (marker.page == 0 && (marker.end || marker.position.is_some()))
286        || (!marker.end && marker.page > 0 && marker.position.is_none())
287        || !at
288            .shards
289            .iter()
290            .map(|shard| (shard.shard_id, shard.fork_anchor))
291            .eq(inputs
292                .target
293                .shards
294                .iter()
295                .map(|shard| (shard.shard_id, shard.head)))
296    {
297        return Err(Failure::new(
298            "candidate_marker_corrupt",
299            "candidate identity, anchors or progress differs",
300        ));
301    }
302    if marker.revision != revision {
303        return Err(Failure::new(
304            "choice_journal_moved",
305            "candidate is bound to another choice revision",
306        ));
307    }
308    Ok(())
309}
310
311fn missing_choice(missing: &MissingChoice) -> Result<Failure, Failure> {
312    Ok(Failure::new(
313        "choice_missing",
314        serde_json::to_string(missing).map_err(Failure::storage)?,
315    ))
316}
317
318fn embedded_choice(
319    storage: &Storage,
320    id: &str,
321    inputs: &HeldInputs,
322    service: &Captured,
323    key: &[u8],
324    values: &Triple,
325) -> Result<choice::EmbeddedSide, Failure> {
326    let Some(side) = choice::embedded_field(
327        key,
328        values.ancestor.as_deref(),
329        values.target.as_deref(),
330        values.source.as_deref(),
331    )?
332    else {
333        return Err(Failure::new(
334            "candidate_vector_rebuild_required",
335            "supplied vector fields require field-wide validation and dimensions",
336        ));
337    };
338    let token = values.token(&inputs.pins, key)?;
339    if storage.read_choice_row(service, id, &token)?.is_some() {
340        return Err(Failure::new(
341            "choice_owner_unsupported",
342            "embedded declarations do not admit a choice",
343        ));
344    }
345    Ok(side)
346}
347
348fn materialise(
349    storage: &Storage,
350    id: &str,
351    inputs: &HeldInputs,
352    service: &Captured,
353    candidate: &Captured,
354    keys: &[Vec<u8>],
355) -> Result<Writes, Failure> {
356    let mut rows = BTreeMap::new();
357    let mut kept_counts = counts::CandidateCounts::new(&storage.reader, candidate)?;
358    for key in keys {
359        if key.starts_with(b"k\0") {
360            continue;
361        }
362        // Supplied vectors require field-wide validation before they can be carried.
363        if key.starts_with(b"v\0") {
364            return Err(Failure::new(
365                "candidate_vector_rebuild_required",
366                "vector changes require field-wide validation and dimensions",
367            ));
368        }
369        let values = storage.choice_values(inputs, key)?;
370        if key.starts_with(b"w\0") {
371            let side = embedded_choice(storage, id, inputs, service, key, &values)?;
372            if side == choice::EmbeddedSide::Source {
373                rows.insert(key.clone(), values.source);
374            }
375            continue;
376        }
377        let owner = coverage::owner_key(key)?;
378        let token = values.token(&inputs.pins, key)?;
379        let row = if owner.is_some() {
380            storage.read_choice_row(service, id, &token)?
381        } else {
382            None
383        };
384        if row
385            .as_ref()
386            .is_some_and(|row| Some(&row.owner) != owner.as_ref())
387        {
388            return Err(Failure::new(
389                "choice_row_corrupt",
390                "candidate choice names another owner",
391            ));
392        }
393        let side = selected(&values, row.as_ref().map(|row| row.choice)).ok_or_else(|| {
394            Failure::new(
395                "choice_missing",
396                format!(
397                    "owner {} token {token}",
398                    String::from_utf8_lossy(key).replace('\0', "/")
399                ),
400            )
401        })?;
402        let value = match side {
403            Side::Source => values.source,
404            Side::Target => values.target,
405            Side::Removed => None,
406        };
407        let resolved = Resolved { side, value };
408        match owner {
409            Some(MergeOwner::Document {
410                collection,
411                document_id,
412            }) => {
413                let (revision, bytes) =
414                    revision_row(storage, inputs, &resolved, &collection, &document_id)?;
415                if !matches!(resolved.side, Side::Target) {
416                    kept_counts.record(
417                        &storage.reader,
418                        candidate,
419                        key,
420                        &revision,
421                        bytes.is_some(),
422                    )?;
423                }
424                rows.insert(revision, bytes);
425            }
426            Some(MergeOwner::Edge { relation, from, to }) => {
427                if let Some(value) = &resolved.value {
428                    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
429                    edges::decode_edge(b"", text, value)?;
430                }
431                let incoming = edges::keys(&from, &relation, &to)?.1;
432                if !matches!(resolved.side, Side::Target) {
433                    kept_counts.record(
434                        &storage.reader,
435                        candidate,
436                        key,
437                        key,
438                        resolved.value.is_some(),
439                    )?;
440                }
441                rows.insert(incoming, resolved.value.clone());
442            }
443            None => {}
444        }
445        rows.insert(key.clone(), resolved.value);
446    }
447    kept_counts.append(&storage.reader, candidate, &mut rows)?;
448    Ok(rows)
449}
450
451fn revision_row(
452    storage: &Storage,
453    inputs: &HeldInputs,
454    resolved: &Resolved,
455    collection: &str,
456    document_id: &str,
457) -> Result<(Vec<u8>, Option<Vec<u8>>), Failure> {
458    let at = match resolved.side {
459        Side::Source => &inputs.source,
460        Side::Target => &inputs.target,
461        Side::Removed => {
462            return Err(Failure::new(
463                "choice_row_corrupt",
464                "an edge removal is recorded for a document",
465            ));
466        }
467    };
468    let revision = revision_key(collection, document_id);
469    let bytes = if resolved.value.is_some() {
470        let bytes = reads::get_bytes(&storage.reader, at, &revision)?.ok_or_else(|| {
471            Failure::new(
472                "document_revision_missing",
473                "resolved document has no revision",
474            )
475        })?;
476        serde_json::from_slice::<DocumentRevision>(&bytes).map_err(Failure::storage)?;
477        Some(bytes)
478    } else {
479        None
480    };
481    Ok((revision, bytes))
482}
483
484fn selected(values: &Triple, choice: Option<MergeChoice>) -> Option<Side> {
485    match choice {
486        Some(MergeChoice::TakeSource) => Some(Side::Source),
487        Some(MergeChoice::TakeTarget) => Some(Side::Target),
488        Some(MergeChoice::RemoveEdge) => Some(Side::Removed),
489        None if values.ancestor == values.target => Some(Side::Source),
490        None if values.ancestor == values.source || values.target == values.source => {
491            Some(Side::Target)
492        }
493        None => None,
494    }
495}
496
497#[cfg(test)]
498#[path = "merge_candidate_tests.rs"]
499mod tests;