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