1use haematite::branch::history::Key as HistoryKey;
2use haematite::branch::{
3 CommitDurability, CommitRequest, ForkWithChanges, ForkWrite, RefRecord, current_timestamp,
4 open as open_branch,
5};
6use haematite::db::branches::ForkKeptError;
7use haematite::tree::Hash;
8
9use super::{
10 Captured, Collection, DocumentRevision, Failure, Receipt, Storage, branch_name, collection_key,
11 counts, document_key, identifier, reads, receipt_key, revision_key, version,
12};
13use crate::serve::protocol::{Change, DocumentImport, Mutation, ReceiptQuery, Selection, Version};
14use crate::serve::schema::Collection as CollectionSchema;
15
16pub(super) type PreparedWrite = (Vec<u8>, Option<Vec<u8>>);
17
18impl Storage {
19 pub fn receipt(&mut self, query: &ReceiptQuery) -> Result<Option<Receipt>, Failure> {
25 identifier(&query.request_id)?;
26 identifier(&query.branch)?;
27 if let Some(receipt) = self.discard_receipt(query)? {
28 return Ok(Some(receipt));
29 }
30 if self.pending_discards.contains(&branch_name(&query.branch)) {
31 return Err(Failure::new(
32 "discard_pending",
33 "branch removal requires receipt reconciliation",
34 ));
35 }
36 let record = self
39 .refs
40 .get(&branch_name(&query.branch))
41 .ok_or_else(|| Failure::new("branch_missing", &query.branch))?;
42 let at = Captured {
43 branch: query.branch.clone(),
44 version: version(record),
45 shards: record.shards.clone(),
46 };
47 let Some(bytes) = reads::get_bytes(
48 &self.reader,
49 &at,
50 &receipt_key(&query.branch, at.version.generation, &query.request_id),
51 )?
52 else {
53 return Ok(None);
54 };
55 let receipt: Receipt = serde_json::from_slice(&bytes).map_err(Failure::storage)?;
56 if receipt.request_id != query.request_id
57 || receipt.branch != query.branch
58 || receipt.version.generation != at.version.generation
59 {
60 return Err(Failure::new(
61 "receipt_corrupt",
62 "receipt identity does not match its stored key",
63 ));
64 }
65 self.refs
66 .confirm_version(
67 &branch_name(&query.branch),
68 at.version.generation,
69 at.version.sequence,
70 )
71 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
72 self.publish_record(&branch_name(&query.branch))?;
73 Ok(Some(receipt))
74 }
75
76 pub fn mutate(&mut self, request: &Mutation) -> Result<Receipt, Failure> {
82 self.mutate_with_import_bound(request, None, super::AppendPruneLimits::default())
83 }
84
85 pub fn mutate_bounded(
90 &mut self,
91 request: &Mutation,
92 max_page: std::num::NonZeroU32,
93 ) -> Result<Receipt, Failure> {
94 self.mutate_with_import_bound(request, Some(max_page), super::AppendPruneLimits::default())
95 }
96
97 pub fn mutate_with_append_prune_limits(
102 &mut self,
103 request: &Mutation,
104 max_page: std::num::NonZeroU32,
105 limits: super::AppendPruneLimits,
106 ) -> Result<Receipt, Failure> {
107 self.mutate_with_import_bound(request, Some(max_page), limits)
108 }
109
110 fn expected_record(&self, request: &Mutation) -> Result<RefRecord, Failure> {
112 let record = self
113 .refs
114 .get(&branch_name(&request.branch))
115 .ok_or_else(|| Failure::new("branch_missing", &request.branch))?;
116 if version(record) != request.expected {
117 return Err(Failure::new(
118 "version_conflict",
119 "expected branch generation or sequence differs",
120 ));
121 }
122 if self.pending_discards.contains(&record.name) {
123 return Err(Failure::new(
124 "discard_pending",
125 "branch removal requires receipt reconciliation",
126 ));
127 }
128 Ok(record.clone())
129 }
130
131 fn mutate_with_import_bound(
132 &mut self,
133 request: &Mutation,
134 import_max_page: Option<std::num::NonZeroU32>,
135 retention_limits: super::AppendPruneLimits,
136 ) -> Result<Receipt, Failure> {
137 identifier(&request.request_id)?;
138 identifier(&request.branch)?;
139 let destination = match &request.change {
140 Change::Fork(fork) => &fork.name,
141 _ => &request.branch,
142 };
143 identifier(destination)?;
144 let fingerprint =
145 Hash::of(&serde_json::to_vec(request).map_err(Failure::storage)?).to_string();
146 if let Some(receipt) = self.replay(request, destination, &fingerprint)? {
147 return Ok(receipt);
148 }
149 validate_import_bound(&request.change, import_max_page)?;
150 let record = self.expected_record(request)?;
151 let at = Captured {
152 branch: request.branch.clone(),
153 version: request.expected,
154 shards: record.shards.clone(),
155 };
156 if let Change::Fork(fork) = &request.change {
157 return self.fork_request(request, &fingerprint, &fork.name, fork.from);
158 }
159 if let Change::Discard(discard) = &request.change {
160 return self.discard_request(request, &fingerprint, &discard.name, &record);
161 }
162 let next = Version {
163 generation: record.created,
164 sequence: record.seq.checked_add(1).ok_or_else(|| {
165 Failure::new("sequence_exhausted", "branch sequence cannot advance")
166 })?,
167 };
168 let (mut writes, retention) = match &request.change {
169 Change::AppendPrune(write) => {
170 let (writes, result) =
171 self.prepare_append_prune(&at, write, next, retention_limits)?;
172 (writes, Some(result))
173 }
174 change => (self.prepare_change(&at, change, next)?, None),
175 };
176 counts::append(&self.reader, &at, &request.change, &mut writes)?;
177 self.rows_before_advance(&request.branch, &record)?;
180 #[cfg(test)]
181 super::history::after_rows::reached()?;
182 let branch = open_branch(&record.name, &self.refs, &self.registry)
183 .map_err(Failure::branch_commit)?;
184 for (key, value) in writes {
185 let shard_id = self.writer.shard_for(&key);
186 match value {
187 Some(value) => branch.put(shard_id, key, value),
188 None => branch.delete(shard_id, key),
189 }
190 .map_err(Failure::branch)?;
191 }
192 let receipt = Receipt {
193 request_id: request.request_id.clone(),
194 fingerprint,
195 branch: request.branch.clone(),
196 version: next,
197 retention,
198 };
199 let key = receipt_key(&request.branch, next.generation, &request.request_id);
200 branch
201 .put(
202 self.writer.shard_for(&key),
203 key,
204 serde_json::to_vec(&receipt).map_err(Failure::storage)?,
205 )
206 .map_err(Failure::branch)?;
207 let result = self.writer.commit(
208 &branch,
209 &self.registry,
210 CommitRequest {
211 durability: CommitDurability::Durable {
212 refs: &mut self.refs,
213 },
214 extra_parents: &[],
215 timestamp: current_timestamp(),
216 },
217 );
218 if let Err(error) = result {
219 let installed = self
220 .refs
221 .get(&record.name)
222 .is_some_and(|now| version(now) != request.expected);
223 let mut failure = Failure::branch_commit(error);
224 let code = if installed {
225 "durability_unknown"
226 } else {
227 "write_refused"
228 };
229 code.clone_into(&mut failure.code);
230 return Err(failure);
231 }
232 #[cfg(test)]
233 lose_mutation_answer()?;
234 #[cfg(test)]
235 publish_pause_point();
236 self.publish_record(&record.name)?;
237 Ok(receipt)
238 }
239
240 fn replay(
241 &mut self,
242 request: &Mutation,
243 destination: &str,
244 fingerprint: &str,
245 ) -> Result<Option<Receipt>, Failure> {
246 if matches!(&request.change, Change::Discard(_))
248 || self.refs.get(&branch_name(destination)).is_some()
249 {
250 if let Some(receipt) = self.receipt(&ReceiptQuery {
251 branch: destination.to_owned(),
252 request_id: request.request_id.clone(),
253 })? {
254 return if receipt.fingerprint == fingerprint {
255 Ok(Some(receipt))
256 } else {
257 Err(Failure::new(
258 "request_conflict",
259 "request identity was used with different content",
260 ))
261 };
262 }
263 if matches!(&request.change, Change::Fork(_)) {
264 return Err(Failure::new(
265 "branch_exists",
266 "destination exists without this fork receipt",
267 ));
268 }
269 }
270 Ok(None)
271 }
272
273 pub(super) fn prepare_change(
274 &self,
275 at: &Captured,
276 change: &Change,
277 next: Version,
278 ) -> Result<Vec<PreparedWrite>, Failure> {
279 match change {
280 Change::Collection(write) => {
281 identifier(&write.collection)?;
282 if let Some(schema) = &write.schema {
283 CollectionSchema::compile(schema).map_err(schema_failure)?;
284 }
285 let collection = Collection {
286 name: write.collection.clone(),
287 schema: write.schema.clone(),
288 changed_in: at.branch.clone(),
289 changed_at: next,
290 };
291 Ok(vec![(
292 collection_key(&write.collection),
293 Some(serde_json::to_vec(&collection).map_err(Failure::storage)?),
294 )])
295 }
296 Change::Put(write) => {
297 identifier(&write.document_id)?;
298 let collection = reads::collection(&self.reader, at, &write.collection)?
299 .ok_or_else(|| Failure::new("collection_missing", &write.collection))?;
300 if let Some(schema) = &collection.schema {
301 CollectionSchema::compile(schema)
302 .map_err(schema_failure)?
303 .validate(
304 &serde_json::from_str(write.document.get())
305 .map_err(Failure::storage)?,
306 )
307 .map_err(schema_failure)?;
308 }
309 let revision = DocumentRevision {
310 collection_branch: collection.changed_in,
311 collection_version: collection.changed_at,
312 written_in: at.branch.clone(),
313 written_at: next,
314 };
315 Ok(vec![
316 (
317 document_key(&write.collection, &write.document_id),
318 Some(write.document.get().as_bytes().to_vec()),
319 ),
320 (
321 revision_key(&write.collection, &write.document_id),
322 Some(serde_json::to_vec(&revision).map_err(Failure::storage)?),
323 ),
324 ])
325 }
326 Change::AppendPrune(_) => Err(Failure::new(
327 "append_prune_preparation_required",
328 "retention uses bounded mutation preparation",
329 )),
330 Change::Delete(write) => {
331 identifier(&write.collection)?;
332 identifier(&write.document_id)?;
333 let mut writes = vec![
336 (document_key(&write.collection, &write.document_id), None),
337 (revision_key(&write.collection, &write.document_id), None),
338 ];
339 writes.extend(super::edges::removals_for_document(
340 &self.reader,
341 at,
342 &write.collection,
343 &write.document_id,
344 )?);
345 writes.extend(super::vectors::removals_for_document(
346 &self.reader,
347 at,
348 &write.collection,
349 &write.document_id,
350 )?);
351 Ok(writes)
352 }
353 Change::DeleteMany(delete) => self.prepare_delete_many(at, delete, next),
354 Change::Merge(merge) => self.merge_writes(at, merge),
355 Change::Fork(_) | Change::Discard(_) => Err(Failure::new(
356 "invalid_operation",
357 "branch control uses the publication path",
358 )),
359 Change::EdgePut(edge) => super::edges::put(&self.reader, at, edge),
360 Change::EdgeDelete(edge) => super::edges::delete(edge),
361 Change::EdgePutMany(import) => super::edges::put_many(&self.reader, at, import),
362 Change::IndexDeclare(field) => {
363 super::field_index::declare(&self.reader, at, field, next)
364 }
365 Change::VectorDeclare(field) => super::vectors::declare(&self.reader, at, field),
366 Change::VectorPut(vector) => super::vectors::put(&self.reader, at, vector),
367 Change::VectorPutMany(import) => {
368 super::vector_import::prepare(&self.reader, at, import)
369 }
370 Change::VectorDelete(vector) => super::vectors::delete(vector),
371 Change::PutMany(import) => self.prepare_import(at, import, next),
372 Change::RelationPut(relation) => super::relations::put(&self.reader, at, relation),
373 Change::RelationDelete(relation) => super::relations::delete(relation),
374 }
375 }
376
377 fn prepare_import(
378 &self,
379 at: &Captured,
380 import: &DocumentImport,
381 next: Version,
382 ) -> Result<Vec<PreparedWrite>, Failure> {
383 if import.documents.is_empty() {
384 return Err(Failure {
385 code: "import_empty".to_owned(),
386 message: "an import must contain at least one document".to_owned(),
387 path: Some("/documents".to_owned()),
388 cause: None,
389 });
390 }
391 let collection = reads::collection(&self.reader, at, &import.collection)
392 .map_err(|mut failure| {
393 failure.path = Some("/collection".to_owned());
394 failure
395 })?
396 .ok_or_else(|| Failure {
397 code: "collection_missing".to_owned(),
398 message: import.collection.clone(),
399 path: Some("/collection".to_owned()),
400 cause: None,
401 })?;
402 let schema = collection
403 .schema
404 .as_ref()
405 .map(CollectionSchema::compile)
406 .transpose()
407 .map_err(schema_failure)?;
408 validate_import_rows(import, schema.as_ref())?;
409 let revision = serde_json::to_vec(&DocumentRevision {
410 collection_branch: collection.changed_in,
411 collection_version: collection.changed_at,
412 written_in: at.branch.clone(),
413 written_at: next,
414 })
415 .map_err(Failure::storage)?;
416 let mut writes = Vec::new();
417 for document in &import.documents {
418 writes.push((
419 document_key(&import.collection, &document.document_id),
420 Some(document.document.get().as_bytes().to_vec()),
421 ));
422 writes.push((
423 revision_key(&import.collection, &document.document_id),
424 Some(revision.clone()),
425 ));
426 }
427 Ok(writes)
428 }
429
430 fn fork_request(
431 &mut self,
432 request: &Mutation,
433 fingerprint: &str,
434 destination: &str,
435 from: Version,
436 ) -> Result<Receipt, Failure> {
437 if self.pending_discards.contains(&branch_name(destination)) {
438 return Err(Failure::new(
439 "discard_pending",
440 "destination removal requires receipt reconciliation",
441 ));
442 }
443 self.capture(&Selection {
444 branch: request.branch.clone(),
445 version: Some(from),
446 })?;
447 let current = self
448 .refs
449 .get(&branch_name(&request.branch))
450 .ok_or_else(|| Failure::new("branch_missing", &request.branch))?
451 .clone();
452 if from == version(¤t) {
455 self.retain_current(&request.branch, ¤t)?;
456 }
457 self.row_for_kept(&request.branch, ¤t, from)?;
460 #[cfg(test)]
461 super::history::after_rows::reached()?;
462 let service =
463 open_branch(super::SERVICE, &self.refs, &self.registry).map_err(|source| {
464 fork_kept_failure(&ForkKeptError::ServiceBindingStale { source }, false)
465 })?;
466 let history_key = HistoryKey {
467 branch: Hash::of(request.branch.as_bytes()),
468 generation: from.generation,
469 sequence: from.sequence,
470 };
471 let created = current_timestamp();
472 let receipt = Receipt {
473 request_id: request.request_id.clone(),
474 fingerprint: fingerprint.to_owned(),
475 branch: destination.to_owned(),
476 version: Version {
477 generation: created,
478 sequence: 0,
479 },
480 retention: None,
481 };
482 let key = receipt_key(destination, created, &request.request_id);
483 let writes = [
484 ForkWrite {
485 shard_id: self.writer.shard_for(&key),
486 key,
487 value: Some(serde_json::to_vec(&receipt).map_err(Failure::storage)?),
488 },
489 super::branches::provenance_write(&self.writer, &request.branch, from)?,
490 ];
491 let name = branch_name(destination);
492 self.writer
493 .fork_kept(
494 &service,
495 history_key,
496 &mut self.refs,
497 &self.registry,
498 ForkWithChanges {
499 name: &name,
500 timestamp: created,
501 writes: &writes,
502 },
503 )
504 .map_err(|error| fork_kept_failure(&error, self.refs.get(&name).is_some()))?;
505 #[cfg(test)]
506 lose_mutation_answer()?;
507 self.publish_record(&name)?;
508 Ok(receipt)
509 }
510
511 #[cfg(test)]
516 pub fn arm_publish_pause(
517 paused: std::sync::mpsc::Sender<()>,
518 release: std::sync::mpsc::Receiver<()>,
519 ) {
520 PUBLISH_PAUSE.with(|slot| {
521 *slot.borrow_mut() = Some(PublishPause { paused, release });
522 });
523 }
524
525 #[cfg(test)]
526 pub(crate) fn lose_next_mutation_answer() {
527 Self::lose_next_mutation_answer_with_message(
528 "injected lost answer after reference commit".to_owned(),
529 );
530 }
531
532 #[cfg(test)]
533 pub(crate) fn lose_next_mutation_answer_with_message(message: String) {
534 LOST_MUTATION_ANSWER.with(|slot| {
535 *slot.borrow_mut() = Some(message);
536 });
537 }
538}
539
540#[cfg(test)]
541thread_local! {
542 static LOST_MUTATION_ANSWER: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
543}
544
545#[cfg(test)]
546fn lose_mutation_answer() -> Result<(), Failure> {
547 if let Some(message) = LOST_MUTATION_ANSWER.with(|slot| slot.borrow_mut().take()) {
548 return Err(Failure::new("durability_unknown", message));
549 }
550 Ok(())
551}
552
553fn fork_kept_failure(error: &ForkKeptError, installed: bool) -> Failure {
554 let code = match error {
555 ForkKeptError::ServiceBindingStale { .. } => "kept_service_binding_stale",
556 ForkKeptError::ServiceInvalid { .. } => "kept_service_invalid",
557 ForkKeptError::ShardSetIncomplete { .. } => "kept_shard_set_incomplete",
558 ForkKeptError::RowMissing { .. } => "kept_row_missing",
559 ForkKeptError::RowInvalid { .. } => "kept_row_invalid",
560 ForkKeptError::RowRead { .. } => "kept_row_read",
561 ForkKeptError::RootUnavailable { .. } => "kept_root_unavailable",
562 ForkKeptError::Fork(_) if installed => "durability_unknown",
563 ForkKeptError::Fork(_) => "write_refused",
564 };
565 Failure::new(code, error.to_string())
566}
567
568#[cfg(test)]
569#[path = "mutations_kept_tests.rs"]
570mod kept_tests;
571
572fn validate_import_rows(
573 import: &DocumentImport,
574 schema: Option<&CollectionSchema>,
575) -> Result<(), Failure> {
576 let mut ids = std::collections::BTreeSet::new();
577 for (index, document) in import.documents.iter().enumerate() {
578 identifier(&document.document_id).map_err(|mut failure| {
579 failure.path = Some(format!("/documents/{index}/document_id"));
580 failure
581 })?;
582 if !ids.insert(document.document_id.as_str()) {
583 return Err(Failure {
584 code: "import_duplicate".to_owned(),
585 message: format!("repeated document_id {}", document.document_id),
586 path: Some(format!("/documents/{index}/document_id")),
587 cause: None,
588 });
589 }
590 if let Some(schema) = schema {
591 let value = serde_json::from_str(document.document.get()).map_err(|error| Failure {
592 code: "invalid_document".to_owned(),
593 message: error.to_string(),
594 path: Some(format!("/documents/{index}/document")),
595 cause: None,
596 })?;
597 schema.validate(&value).map_err(|error| Failure {
598 code: error.code().to_owned(),
599 message: error.reason,
600 path: Some(format!("/documents/{index}/document{}", error.path)),
601 cause: None,
602 })?;
603 }
604 }
605 Ok(())
606}
607
608fn validate_import_bound(
609 change: &Change,
610 max_page: Option<std::num::NonZeroU32>,
611) -> Result<(), Failure> {
612 let (rows, kind) = match change {
613 Change::PutMany(import) => (import.documents.len(), "documents"),
614 Change::DeleteMany(delete) => (delete.document_ids.len(), "document_ids"),
615 Change::EdgePutMany(import) => (import.edges.len(), "edges"),
616 Change::VectorPutMany(import) => (import.vectors.len(), "vectors"),
617 _ => return Ok(()),
618 };
619 let max_page = max_page.ok_or_else(|| {
620 Failure::new(
621 "import_limit_required",
622 "an import requires the service page bound",
623 )
624 })?;
625 let bound = u64::from(max_page.get())
626 .checked_mul(64)
627 .ok_or_else(|| Failure::new("import_limit", "import row bound exceeds u64"))?;
628 if u64::try_from(rows).map_err(Failure::storage)? > bound {
629 return Err(Failure {
630 code: "import_limit".to_owned(),
631 message: format!("maximum import size is {bound} {kind}"),
632 path: Some(format!("/{kind}/{bound}")),
633 cause: None,
634 });
635 }
636 Ok(())
637}
638
639fn schema_failure(error: crate::serve::schema::Violation) -> Failure {
640 Failure {
641 code: error.code().to_owned(),
642 message: error.reason,
643 path: Some(error.path),
644 cause: None,
645 }
646}
647
648#[cfg(test)]
649struct PublishPause {
650 paused: std::sync::mpsc::Sender<()>,
651 release: std::sync::mpsc::Receiver<()>,
652}
653
654#[cfg(test)]
655thread_local! {
656 static PUBLISH_PAUSE: std::cell::RefCell<Option<PublishPause>> =
657 const { std::cell::RefCell::new(None) };
658}
659
660#[cfg(test)]
663fn publish_pause_point() {
664 let Some(pause) = PUBLISH_PAUSE.with(|slot| slot.borrow_mut().take()) else {
665 return;
666 };
667 assert!(
668 pause.paused.send(()).is_ok(),
669 "paused signal receiver was dropped"
670 );
671 let wait = std::time::Duration::from_secs(10);
672 let released = pause.release.recv_timeout(wait);
673 assert!(released.is_ok(), "release signal not received within 10 s");
674}