1use crate::{
2 Database, DocDbRevision, ObservedDocument, TransactCondition, TransactMutation, TransactRequest,
3};
4use anyhow::{Result, anyhow, bail};
5use serde::{Serialize, de::DeserializeOwned};
6use std::{
7 any::type_name,
8 cell::UnsafeCell,
9 collections::HashMap,
10 marker::PhantomData,
11 ops::{Deref, DerefMut},
12 sync::{
13 Arc, Mutex, Weak,
14 atomic::{AtomicBool, Ordering},
15 },
16};
17use tracing::Instrument;
18
19#[repr(transparent)]
23pub(crate) struct SyncUnsafeCell<T>(UnsafeCell<T>);
24
25unsafe impl<T: Send> Send for SyncUnsafeCell<T> {}
26unsafe impl<T: Send> Sync for SyncUnsafeCell<T> {}
27
28impl<T> SyncUnsafeCell<T> {
29 fn new(value: T) -> Self {
30 Self(UnsafeCell::new(value))
31 }
32 fn get(&self) -> *mut T {
33 self.0.get()
34 }
35}
36
37#[derive(Clone, Debug, Eq, Hash, PartialEq)]
38pub struct DocKey {
39 pub pk: String,
40 pub sk: String,
41}
42
43impl DocKey {
44 pub fn new(pk: impl Into<String>, sk: impl Into<String>) -> Self {
45 Self {
46 pk: pk.into(),
47 sk: sk.into(),
48 }
49 }
50}
51
52pub trait Document: Serialize + DeserializeOwned + Send + Sync + 'static {
53 fn key(&self) -> DocKey;
54}
55
56pub trait DocGet {
57 type Doc: Document;
58 fn key(&self) -> DocKey;
59}
60
61#[allow(async_fn_in_trait)]
62pub trait TrxRead: Sized {
63 type Output;
64 fn collect_keys(&self, keys: &mut Vec<DocKey>);
65 async fn finalize(
66 self,
67 tx: &Trx,
68 results: &mut std::vec::IntoIter<ObservedDocument>,
69 ) -> Result<Self::Output>;
70}
71
72impl<R> TrxRead for R
73where
74 R: DocGet,
75{
76 type Output = Option<DocHandle<R::Doc>>;
77
78 fn collect_keys(&self, keys: &mut Vec<DocKey>) {
79 keys.push(self.key());
80 }
81
82 async fn finalize(
83 self,
84 tx: &Trx,
85 results: &mut std::vec::IntoIter<ObservedDocument>,
86 ) -> Result<Self::Output> {
87 let stored = results
88 .next()
89 .ok_or_else(|| anyhow!("trx observed result missing for read"))?;
90 let key = self.key();
91 tx.inner
92 .lock()
93 .unwrap()
94 .register_loaded::<R::Doc>(key, stored)
95 }
96}
97
98macro_rules! impl_trx_read_tuple {
99 ($($T:ident),+) => {
100 #[allow(non_snake_case)]
101 impl<$($T: TrxRead),+> TrxRead for ($($T,)+) {
102 type Output = ($($T::Output,)+);
103
104 fn collect_keys(&self, keys: &mut Vec<DocKey>) {
105 let ($($T,)+) = self;
106 $($T.collect_keys(keys);)+
107 }
108
109 async fn finalize(
110 self,
111 tx: &Trx,
112 results: &mut std::vec::IntoIter<ObservedDocument>,
113 ) -> Result<Self::Output> {
114 let ($($T,)+) = self;
115 Ok(($($T.finalize(tx, results).await?,)+))
116 }
117 }
118 };
119}
120
121impl_trx_read_tuple!(A);
122impl_trx_read_tuple!(A, B);
123impl_trx_read_tuple!(A, B, C);
124impl_trx_read_tuple!(A, B, C, D);
125impl_trx_read_tuple!(A, B, C, D, E);
126impl_trx_read_tuple!(A, B, C, D, E, F);
127impl_trx_read_tuple!(A, B, C, D, E, F, G);
128impl_trx_read_tuple!(A, B, C, D, E, F, G, H);
129impl_trx_read_tuple!(A, B, C, D, E, F, G, H, I);
130impl_trx_read_tuple!(A, B, C, D, E, F, G, H, I, J);
131impl_trx_read_tuple!(A, B, C, D, E, F, G, H, I, J, K);
132impl_trx_read_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
133
134pub struct DocHandle<T> {
135 data: Arc<SyncUnsafeCell<T>>,
136 dirty: Arc<AtomicBool>,
137 deleted: Arc<AtomicBool>,
138 _alive: Arc<()>,
139 _marker: PhantomData<Arc<T>>,
140}
141
142impl<T> DocHandle<T> {
143 pub fn delete(&self) {
144 self.deleted.store(true, Ordering::Release);
145 }
146}
147
148impl<T> Deref for DocHandle<T> {
149 type Target = T;
150
151 fn deref(&self) -> &Self::Target {
152 unsafe { &*self.data.get() }
153 }
154}
155
156impl<T> DerefMut for DocHandle<T> {
157 fn deref_mut(&mut self) -> &mut Self::Target {
158 self.dirty.store(true, Ordering::Release);
159 unsafe { &mut *self.data.get() }
160 }
161}
162
163pub struct Trx {
164 inner: Arc<Mutex<TrxState>>,
165}
166
167impl Trx {
168 #[tracing::instrument(skip_all)]
169 pub async fn get<R>(&self, request: R) -> Result<R::Output>
170 where
171 R: TrxRead,
172 {
173 let mut keys = Vec::new();
174 request.collect_keys(&mut keys);
175
176 {
177 let state = self.inner.lock().unwrap();
178 for key in &keys {
179 if state.index.contains_key(key) {
180 bail!("duplicate trx key access: {}/{}", key.pk, key.sk);
181 }
182 }
183 }
184
185 let key_pairs: Vec<(String, String)> =
186 keys.iter().map(|k| (k.pk.clone(), k.sk.clone())).collect();
187 let stored = self.load_observed(&key_pairs).await?;
188
189 let mut iter = stored.into_iter();
190 request.finalize(self, &mut iter).await
191 }
192
193 pub fn create<T>(&self, doc: T) -> Result<DocHandle<T>>
194 where
195 T: Document,
196 {
197 self.inner.lock().unwrap().create(doc)
198 }
199
200 pub fn commit<Out, Cancel>(self, out: Out) -> Result<TrxControl<Out, Cancel>> {
201 Ok(TrxControl {
202 inner: TrxControlInner::Commit(out),
203 })
204 }
205
206 pub fn cancel<Out, Cancel>(self, reason: Cancel) -> Result<TrxControl<Out, Cancel>> {
207 Ok(TrxControl {
208 inner: TrxControlInner::Cancel(reason),
209 })
210 }
211
212 async fn load_observed(&self, keys: &[(String, String)]) -> Result<Vec<ObservedDocument>> {
213 let db = self.inner.lock().unwrap().db.clone();
214 get_observed_many_concurrently(&db, keys).await
215 }
216}
217
218async fn get_observed_many_concurrently(
219 db: &Database,
220 keys: &[(String, String)],
221) -> Result<Vec<ObservedDocument>> {
222 let results =
223 futures::future::join_all(keys.iter().map(|(pk, sk)| db.get_observed(pk, sk))).await;
224 let mut observations = Vec::with_capacity(results.len());
225 let mut first_error = None;
226 for result in results {
227 match result {
228 Ok(observation) => observations.push(observation),
229 Err(error) => {
230 if first_error.is_none() {
231 first_error = Some(error);
232 }
233 }
234 }
235 }
236 if let Some(error) = first_error {
237 return Err(error);
238 }
239 Ok(observations)
240}
241
242pub struct TrxControl<Out, Cancel> {
243 inner: TrxControlInner<Out, Cancel>,
244}
245
246enum TrxControlInner<Out, Cancel> {
247 Commit(Out),
248 Cancel(Cancel),
249}
250
251#[derive(Debug)]
252pub enum TrxResult<Out, Cancel, Err> {
253 Committed(Out),
254 Cancelled(Cancel),
255 Conflict(ConflictDetails),
256 Err(Err),
257}
258
259#[derive(Clone, Debug, Default)]
260pub struct ConflictDetails {
261 pub keys: Vec<ConflictKey>,
262}
263
264#[derive(Clone, Debug)]
265pub struct ConflictKey {
266 pub key: DocKey,
267 pub expected_revision: Option<DocDbRevision>,
268 pub actual_revision: Option<DocDbRevision>,
269}
270
271const MAX_ATTEMPTS: u32 = 5;
272const BACKOFF_BASE_MS: u64 = 50;
273const BACKOFF_CAP_MS: u64 = 1000;
274
275pub(crate) async fn run<F, Fut, Out, Cancel, E>(db: Database, mut f: F) -> TrxResult<Out, Cancel, E>
276where
277 F: FnMut(Trx) -> Fut,
278 Fut: std::future::Future<Output = Result<TrxControl<Out, Cancel>, E>>,
279 E: From<anyhow::Error>,
280{
281 let mut attempt: u32 = 0;
282 loop {
283 let attempt_span = tracing::info_span!("trx_attempt", attempt = attempt);
284 let result = run_attempt(&db, &mut f).instrument(attempt_span).await;
285 match result {
286 AttemptOutcome::Done(r) => return r,
287 AttemptOutcome::Conflict(details) => {
288 if attempt + 1 >= MAX_ATTEMPTS {
289 return TrxResult::Conflict(details);
290 }
291 let backoff_span = tracing::info_span!("trx_backoff", attempt = attempt);
292 async {
293 let backoff = conflict_backoff(attempt).await;
294 crate::runtime::sleep(backoff).await;
295 }
296 .instrument(backoff_span)
297 .await;
298 attempt += 1;
299 }
300 }
301 }
302}
303
304enum AttemptOutcome<Out, Cancel, E> {
305 Done(TrxResult<Out, Cancel, E>),
306 Conflict(ConflictDetails),
307}
308
309async fn run_attempt<F, Fut, Out, Cancel, E>(
310 db: &Database,
311 f: &mut F,
312) -> AttemptOutcome<Out, Cancel, E>
313where
314 F: FnMut(Trx) -> Fut,
315 Fut: std::future::Future<Output = Result<TrxControl<Out, Cancel>, E>>,
316 E: From<anyhow::Error>,
317{
318 let state = Arc::new(Mutex::new(TrxState::new(db.clone())));
319 let tx = Trx {
320 inner: state.clone(),
321 };
322
323 let user_span = tracing::info_span!("trx_user_closure");
324 let control = match f(tx).instrument(user_span).await {
325 Ok(control) => control,
326 Err(err) => return AttemptOutcome::Done(TrxResult::Err(err)),
327 };
328
329 match control.inner {
330 TrxControlInner::Commit(out) => {
331 let (commit_db, entries_result) = take_entries(state);
332 let entries = match entries_result {
333 Ok(e) => e,
334 Err(err) => return AttemptOutcome::Done(TrxResult::Err(E::from(err))),
335 };
336
337 match commit_entries(commit_db, entries).await {
338 Ok(()) => AttemptOutcome::Done(TrxResult::Committed(out)),
339 Err(CommitFailure::Conflict(details)) => AttemptOutcome::Conflict(details),
340 Err(CommitFailure::Err(err)) => AttemptOutcome::Done(TrxResult::Err(E::from(err))),
341 }
342 }
343 TrxControlInner::Cancel(reason) => AttemptOutcome::Done(TrxResult::Cancelled(reason)),
344 }
345}
346
347async fn conflict_backoff(attempt: u32) -> std::time::Duration {
348 let ceiling = BACKOFF_BASE_MS
349 .checked_shl(attempt)
350 .unwrap_or(BACKOFF_CAP_MS)
351 .min(BACKOFF_CAP_MS);
352 let mut buf = [0u8; 8];
353 crate::runtime::random_bytes(&mut buf).await;
354 let raw = u64::from_le_bytes(buf);
355 let delay_ms = raw % (ceiling + 1);
356 std::time::Duration::from_millis(delay_ms)
357}
358
359struct TrxState {
360 db: Database,
361 entries: Vec<TrackedEntry>,
362 index: HashMap<DocKey, usize>,
363}
364
365impl TrxState {
366 fn new(db: Database) -> Self {
367 Self {
368 db,
369 entries: Vec::new(),
370 index: HashMap::new(),
371 }
372 }
373
374 fn register_loaded<T>(
375 &mut self,
376 key: DocKey,
377 observed: ObservedDocument,
378 ) -> Result<Option<DocHandle<T>>>
379 where
380 T: Document,
381 {
382 if self.index.contains_key(&key) {
383 bail!("duplicate trx key access: {}/{}", key.pk, key.sk);
384 }
385
386 let idx = self.entries.len();
387 self.index.insert(key.clone(), idx);
388
389 match observed {
390 ObservedDocument::Present { data, revision } => {
391 let doc = serde_json::from_slice::<T>(&data).map_err(|err| {
392 anyhow!(
393 "failed to deserialize {} at {}/{}: {}",
394 type_name::<T>(),
395 key.pk,
396 key.sk,
397 err
398 )
399 })?;
400 let (shared, handle) = new_shared_doc(doc);
401 self.entries.push(TrackedEntry {
402 key,
403 expected_revision: Some(revision),
404 observed: true,
405 state: TrackedState::Managed {
406 shared,
407 created: false,
408 },
409 });
410 Ok(Some(handle))
411 }
412 ObservedDocument::Missing { revision } => {
413 self.entries.push(TrackedEntry {
414 key,
415 expected_revision: revision,
416 observed: true,
417 state: TrackedState::Missing,
418 });
419 Ok(None)
420 }
421 }
422 }
423
424 fn create<T>(&mut self, doc: T) -> Result<DocHandle<T>>
425 where
426 T: Document,
427 {
428 let key = doc.key();
429 let (shared, handle) = new_shared_doc(doc);
430
431 match self.index.get(&key).copied() {
432 None => {
433 let idx = self.entries.len();
434 self.index.insert(key.clone(), idx);
435 self.entries.push(TrackedEntry {
436 key,
437 expected_revision: None,
438 observed: false,
439 state: TrackedState::Managed {
440 shared,
441 created: true,
442 },
443 });
444 Ok(handle)
445 }
446 Some(idx) => match self.entries.get_mut(idx) {
447 Some(TrackedEntry {
448 expected_revision: _,
449 state: TrackedState::Missing,
450 ..
451 }) => {
452 self.entries[idx].state = TrackedState::Managed {
453 shared,
454 created: true,
455 };
456 Ok(handle)
457 }
458 _ => bail!("duplicate trx key access: {}/{}", key.pk, key.sk),
459 },
460 }
461 }
462
463 fn take_entries(&mut self) -> (Database, Result<Vec<TrackedEntry>>) {
464 let db = self.db.clone();
465 for entry in &self.entries {
466 if let TrackedState::Managed { shared, .. } = &entry.state
467 && shared.handle_alive.upgrade().is_some()
468 {
469 let err = anyhow!(
470 "live doc handle escaped trx for key {}/{}; commit outputs must not contain DocHandle values",
471 entry.key.pk,
472 entry.key.sk
473 );
474 self.entries.clear();
475 return (db, Err(err));
476 }
477 }
478 (db, Ok(std::mem::take(&mut self.entries)))
479 }
480}
481
482struct TrackedEntry {
483 key: DocKey,
484 expected_revision: Option<DocDbRevision>,
485 observed: bool,
486 state: TrackedState,
487}
488
489impl TrackedEntry {
490 fn condition(&self) -> Option<TransactCondition> {
491 if !self.observed {
492 let is_live_created = matches!(
493 &self.state,
494 TrackedState::Managed {
495 shared,
496 created: true,
497 } if !shared.deleted.load(Ordering::Acquire)
498 );
499 if !is_live_created {
500 return None;
501 }
502 }
503 match self.expected_revision {
504 Some(expected_revision) => Some(TransactCondition::RevisionEquals {
505 pk: self.key.pk.clone(),
506 sk: self.key.sk.clone(),
507 expected_revision,
508 }),
509 None => Some(TransactCondition::NotExists {
510 pk: self.key.pk.clone(),
511 sk: self.key.sk.clone(),
512 }),
513 }
514 }
515
516 fn mutation(&self) -> Result<Option<TransactMutation>> {
517 let TrackedState::Managed { shared, created } = &self.state else {
518 return Ok(None);
519 };
520 if shared.deleted.load(Ordering::Acquire) {
521 if *created {
522 return Ok(None);
523 }
524 return Ok(Some(TransactMutation::Delete {
525 pk: self.key.pk.clone(),
526 sk: self.key.sk.clone(),
527 }));
528 }
529 if *created || shared.dirty.load(Ordering::Acquire) {
530 return Ok(Some(TransactMutation::Put {
531 pk: self.key.pk.clone(),
532 sk: self.key.sk.clone(),
533 data: (shared.serialize)()?,
534 }));
535 }
536 Ok(None)
537 }
538}
539
540enum TrackedState {
541 Missing,
542 Managed { shared: SharedDoc, created: bool },
543}
544
545struct SharedDoc {
546 dirty: Arc<AtomicBool>,
547 deleted: Arc<AtomicBool>,
548 handle_alive: Weak<()>,
549 serialize: Box<dyn Fn() -> Result<Vec<u8>> + Send + Sync>,
550}
551
552fn new_shared_doc<T>(doc: T) -> (SharedDoc, DocHandle<T>)
553where
554 T: Document + Send + Sync,
555{
556 let data = Arc::new(SyncUnsafeCell::new(doc));
557 let dirty = Arc::new(AtomicBool::new(false));
558 let deleted = Arc::new(AtomicBool::new(false));
559 let alive = Arc::new(());
560
561 let serialize_data = data.clone();
562 let shared = SharedDoc {
563 dirty: dirty.clone(),
564 deleted: deleted.clone(),
565 handle_alive: Arc::downgrade(&alive),
566 serialize: Box::new(move || {
567 let doc_ref = unsafe { &*serialize_data.get() };
568 serde_json::to_vec(doc_ref).map_err(Into::into)
569 }),
570 };
571
572 let handle = DocHandle {
573 data,
574 dirty,
575 deleted,
576 _alive: alive,
577 _marker: PhantomData,
578 };
579
580 (shared, handle)
581}
582
583fn take_entries(state: Arc<Mutex<TrxState>>) -> (Database, Result<Vec<TrackedEntry>>) {
584 state.lock().unwrap().take_entries()
585}
586
587enum CommitFailure {
588 Conflict(ConflictDetails),
589 Err(anyhow::Error),
590}
591
592#[tracing::instrument(skip_all, fields(entries = entries.len()))]
593async fn commit_entries(
594 db: Database,
595 entries: Vec<TrackedEntry>,
596) -> std::result::Result<(), CommitFailure> {
597 let mut conditions = Vec::new();
598 let mut mutations = Vec::new();
599 for entry in &entries {
600 if let Some(condition) = entry.condition() {
601 conditions.push(condition);
602 }
603 if let Some(mutation) = entry.mutation().map_err(CommitFailure::Err)? {
604 mutations.push(mutation);
605 }
606 }
607
608 if conditions.is_empty() && mutations.is_empty() {
609 return Ok(());
610 }
611
612 let request = TransactRequest {
613 conditions,
614 mutations,
615 };
616 let outcome = db.transact(&request).await.map_err(CommitFailure::Err)?;
617
618 let mut conflicts = Vec::new();
619
620 if let Some(info) = outcome.conflict {
621 let condition = request.conditions.get(info.condition_index).ok_or_else(|| {
622 CommitFailure::Err(anyhow!(
623 "backend returned invalid transaction conflict condition_index {} for {} conditions",
624 info.condition_index,
625 request.conditions.len()
626 ))
627 })?;
628 let (pk, sk, expected_revision) = condition_key_and_expected(condition);
629 conflicts.push(ConflictKey {
630 key: DocKey { pk, sk },
631 expected_revision,
632 actual_revision: None,
633 });
634 }
635
636 if !conflicts.is_empty() {
637 let key_pairs: Vec<(String, String)> = conflicts
638 .iter()
639 .map(|c| (c.key.pk.clone(), c.key.sk.clone()))
640 .collect();
641 if let Ok(observed) = get_observed_many_concurrently(&db, &key_pairs).await {
642 for (conflict, observation) in conflicts.iter_mut().zip(observed.into_iter()) {
643 conflict.actual_revision = match observation {
644 ObservedDocument::Present { revision, .. } => Some(revision),
645 ObservedDocument::Missing { revision } => revision,
646 };
647 }
648 }
649 return Err(CommitFailure::Conflict(ConflictDetails { keys: conflicts }));
650 }
651
652 Ok(())
653}
654
655fn condition_key_and_expected(
656 condition: &TransactCondition,
657) -> (String, String, Option<DocDbRevision>) {
658 match condition {
659 TransactCondition::Exists { pk, sk } | TransactCondition::NotExists { pk, sk } => {
660 (pk.clone(), sk.clone(), None)
661 }
662 TransactCondition::RevisionEquals {
663 pk,
664 sk,
665 expected_revision,
666 } => (pk.clone(), sk.clone(), Some(*expected_revision)),
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[derive(Clone, serde::Serialize, serde::Deserialize)]
675 struct TestDoc {
676 id: String,
677 value: i32,
678 }
679
680 impl Document for TestDoc {
681 fn key(&self) -> DocKey {
682 DocKey::new("TestDoc", format!("id={}", self.id))
683 }
684 }
685
686 struct TestDocGet {
687 id: String,
688 }
689
690 impl DocGet for TestDocGet {
691 type Doc = TestDoc;
692
693 fn key(&self) -> DocKey {
694 DocKey::new("TestDoc", format!("id={}", self.id))
695 }
696 }
697
698 fn test_state() -> TrxState {
699 TrxState::new(crate::memory())
700 }
701
702 #[test]
703 fn create_produces_insert_even_without_mutation() {
704 let mut state = test_state();
705 let _handle = state
706 .create(TestDoc {
707 id: "a".into(),
708 value: 1,
709 })
710 .expect("create should succeed");
711
712 let write = state.entries[0]
713 .mutation()
714 .expect("transaction mutation")
715 .expect("transaction mutation should exist");
716 match write {
717 TransactMutation::Put { data, .. } => {
718 let doc: TestDoc = serde_json::from_slice(&data).expect("deserialize insert");
719 assert_eq!(doc.id, "a");
720 assert_eq!(doc.value, 1);
721 }
722 _ => panic!("expected insert"),
723 }
724 }
725
726 #[test]
727 fn loaded_doc_marks_dirty_on_deref_mut() {
728 let mut state = test_state();
729 let doc = TestDoc {
730 id: "a".into(),
731 value: 1,
732 };
733 let key = TestDocGet { id: "a".into() }.key();
734 let mut handle = state
735 .register_loaded::<TestDoc>(
736 key,
737 ObservedDocument::Present {
738 data: serde_json::to_vec(&doc).expect("serialize").into(),
739 revision: DocDbRevision::new(7),
740 },
741 )
742 .expect("load should succeed")
743 .expect("doc should exist");
744
745 handle.value = 5;
746
747 match state.entries[0]
748 .mutation()
749 .expect("transaction mutation")
750 .expect("transaction mutation should exist")
751 {
752 TransactMutation::Put { data, .. } => {
753 let doc: TestDoc = serde_json::from_slice(&data).expect("deserialize update");
754 assert_eq!(doc.value, 5);
755 }
756 _ => panic!("expected update"),
757 }
758 assert!(matches!(
759 state.entries[0].condition(),
760 Some(TransactCondition::RevisionEquals {
761 expected_revision,
762 ..
763 }) if expected_revision == DocDbRevision::new(7)
764 ));
765 }
766
767 #[test]
768 fn loaded_doc_delete_produces_delete_write() {
769 let mut state = test_state();
770 let doc = TestDoc {
771 id: "a".into(),
772 value: 1,
773 };
774 let key = TestDocGet { id: "a".into() }.key();
775 let handle = state
776 .register_loaded::<TestDoc>(
777 key,
778 ObservedDocument::Present {
779 data: serde_json::to_vec(&doc).expect("serialize").into(),
780 revision: DocDbRevision::new(7),
781 },
782 )
783 .expect("load should succeed")
784 .expect("doc should exist");
785
786 handle.delete();
787
788 match state.entries[0]
789 .mutation()
790 .expect("transaction mutation")
791 .expect("transaction mutation should exist")
792 {
793 TransactMutation::Delete { .. } => {}
794 _ => panic!("expected delete"),
795 }
796 assert!(matches!(
797 state.entries[0].condition(),
798 Some(TransactCondition::RevisionEquals {
799 expected_revision,
800 ..
801 }) if expected_revision == DocDbRevision::new(7)
802 ));
803 }
804
805 #[test]
806 fn missing_read_can_be_promoted_to_create() {
807 let mut state = test_state();
808 let key = TestDocGet { id: "a".into() }.key();
809 let loaded = state
810 .register_loaded::<TestDoc>(key, ObservedDocument::Missing { revision: None })
811 .expect("register missing should succeed");
812 assert!(loaded.is_none());
813
814 let handle = state
815 .create(TestDoc {
816 id: "a".into(),
817 value: 3,
818 })
819 .expect("create after missing get should succeed");
820 assert_eq!(handle.value, 3);
821
822 assert!(matches!(
823 state.entries[0].mutation().expect("transaction mutation"),
824 Some(TransactMutation::Put { .. })
825 ));
826 assert!(matches!(
827 state.entries[0].condition(),
828 Some(TransactCondition::NotExists { .. })
829 ));
830 }
831
832 #[test]
833 fn exact_missing_revision_becomes_revision_condition() {
834 let mut state = test_state();
835 let key = TestDocGet { id: "a".into() }.key();
836 assert!(
837 state
838 .register_loaded::<TestDoc>(
839 key,
840 ObservedDocument::Missing {
841 revision: Some(DocDbRevision::new(11)),
842 },
843 )
844 .expect("register missing should succeed")
845 .is_none()
846 );
847
848 assert!(matches!(
849 state.entries[0].condition(),
850 Some(TransactCondition::RevisionEquals {
851 expected_revision,
852 ..
853 }) if expected_revision == DocDbRevision::new(11)
854 ));
855 assert!(state.entries[0].mutation().unwrap().is_none());
856 }
857
858 #[test]
859 fn missing_read_then_create_then_delete_keeps_missing_dependency() {
860 let mut state = test_state();
861 let key = TestDocGet { id: "a".into() }.key();
862 assert!(
863 state
864 .register_loaded::<TestDoc>(key, ObservedDocument::Missing { revision: None })
865 .expect("register missing should succeed")
866 .is_none()
867 );
868
869 let handle = state
870 .create(TestDoc {
871 id: "a".into(),
872 value: 3,
873 })
874 .expect("create after missing get should succeed");
875 handle.delete();
876 drop(handle);
877
878 assert!(matches!(
879 state.entries[0].condition(),
880 Some(TransactCondition::NotExists { .. })
881 ));
882 assert!(
883 state.entries[0]
884 .mutation()
885 .expect("transaction mutation")
886 .is_none()
887 );
888 }
889
890 #[test]
891 fn create_then_delete_without_read_is_a_noop() {
892 let mut state = test_state();
893 let handle = state
894 .create(TestDoc {
895 id: "a".into(),
896 value: 3,
897 })
898 .expect("create should succeed");
899 handle.delete();
900 drop(handle);
901
902 assert!(
903 state.entries[0].condition().is_none()
904 && state.entries[0]
905 .mutation()
906 .expect("transaction mutation")
907 .is_none()
908 );
909 }
910
911 #[test]
912 fn loaded_doc_without_mutation_produces_version_check() {
913 let mut state = test_state();
914 let key = TestDocGet { id: "a".into() }.key();
915 let handle = state
916 .register_loaded::<TestDoc>(
917 key,
918 ObservedDocument::Present {
919 data: serde_json::to_vec(&TestDoc {
920 id: "a".into(),
921 value: 1,
922 })
923 .expect("serialize")
924 .into(),
925 revision: DocDbRevision::new(7),
926 },
927 )
928 .expect("load should succeed")
929 .expect("doc should exist");
930 drop(handle);
931
932 assert!(matches!(
933 state.entries[0].condition(),
934 Some(TransactCondition::RevisionEquals {
935 expected_revision,
936 ..
937 }) if expected_revision == DocDbRevision::new(7)
938 ));
939 }
940
941 #[test]
942 fn duplicate_key_access_is_rejected() {
943 let mut state = test_state();
944 let first = state.register_loaded::<TestDoc>(
945 TestDocGet { id: "a".into() }.key(),
946 ObservedDocument::Missing { revision: None },
947 );
948 assert!(first.is_ok());
949
950 let second = state.register_loaded::<TestDoc>(
951 TestDocGet { id: "a".into() }.key(),
952 ObservedDocument::Missing { revision: None },
953 );
954 assert!(second.is_err());
955 }
956
957 #[test]
958 fn live_handle_cannot_escape_commit_boundary() {
959 let mut state = test_state();
960 let _handle = state
961 .create(TestDoc {
962 id: "a".into(),
963 value: 1,
964 })
965 .expect("create should succeed");
966
967 let (_, result) = state.take_entries();
968 match result {
969 Ok(_) => panic!("live handle should fail"),
970 Err(err) => assert!(err.to_string().contains("live doc handle escaped trx")),
971 }
972 }
973}