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