Skip to main content

doc_db/
trx.rs

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/// Wrapper around UnsafeCell that asserts Send/Sync when T: Send.
20/// DocHandle holds the only outstanding reference within a trx, so concurrent
21/// access cannot occur in practice; the trx future itself is the unit of work.
22#[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}