1use 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 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 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 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;