Skip to main content

Transaction

Struct Transaction 

pub struct Transaction<'db, I: IsolationLevel> { /* private fields */ }
Expand description

A transaction at isolation level I.

I is a typestate: Transaction<'_, ReadCommitted> and Transaction<'_, Serializable> are different types with different generated code. See crate::core::isolation.

Implementations§

§

impl<'db, I: IsolationLevel> Transaction<'db, I>

pub fn id(&self) -> TxnId

This transaction’s id, drawn from the same counter as commit timestamps. Stable for its whole life, and never reused.

pub fn snapshot(&self) -> Timestamp

The snapshot this transaction reads at.

pub fn get<T: Versioned>(&mut self, key: &T::Key) -> Result<Option<Ref<'_, T>>>

Read by primary key.

Returns a guard over the version in place rather than a copy. Reads take no locks and never block, whatever else is happening to the record.

Examples found in repository?
examples/game.rs (line 111)
110fn gold_of<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>, hero: u64) -> Result<i64> {
111    Ok(tx.get::<Hero>(&hero)?.map(|h| h.gold).unwrap_or(0))
112}
113
114fn name_of<I: mvcc::IsolationLevel>(
115    tx: &mut mvcc::Transaction<'_, I>,
116    hero: u64,
117) -> Result<String> {
118    Ok(tx
119        .get::<Hero>(&hero)?
120        .map(|h| h.name.clone())
121        .unwrap_or_else(|| "?".into()))
122}
123
124fn party_size<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>) -> Result<usize> {
125    Ok(tx.scan_where::<PartyMember, _>(|m| m.party == PARTY)?.len())
126}
127
128fn total_gold(db: &Database) -> Result<i64> {
129    let mut tx = db.begin();
130    Ok(tx.scan::<Hero>()?.iter().map(|h| h.gold).sum())
131}
132
133fn act(n: u32, title: &str) {
134    println!("\n\x1b[1m── Act {n}. {title}\x1b[0m");
135}
136
137// A narrative example: one linear script of acts, read top to bottom. Splitting
138// it into helpers to satisfy the length and complexity budgets would make it
139// harder to follow, which is the only thing this file is for.
140#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}
More examples
Hide additional examples
examples/basic.rs (line 55)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
examples/concurrent.rs (line 102)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (line 55)
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}

pub fn insert<T: Versioned>(&mut self, value: T) -> Result<()>

Insert a new record.

Fails with Error::DuplicateKey if the primary key already has a visible, non-deleted version, or if a unique index would be violated, and with Error::WriteConflict if another in-flight transaction is writing one of the same unique keys.

Examples found in repository?
examples/basic.rs (lines 32-37)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
More examples
Hide additional examples
examples/concurrent.rs (lines 35-38)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (lines 38-42)
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}
examples/game.rs (lines 153-158)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn update<T: Versioned>( &mut self, key: &T::Key, f: impl FnOnce(&mut T), ) -> Result<bool>

Read-modify-write by primary key. Returns false if the record does not exist at this snapshot.

A closure rather than a &mut guard, deliberately: the engine needs to know exactly when mutation ends so it can install the version and update the indexes. A guard doing that work in Drop could not report a write conflict, because Drop has nowhere to return an error to.

Examples found in repository?
examples/basic.rs (line 64)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
More examples
Hide additional examples
examples/concurrent.rs (line 106)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
examples/isolation.rs (line 58)
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}
examples/game.rs (lines 194-197)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn delete<T: Versioned>(&mut self, key: &T::Key) -> Result<bool>

Delete by primary key. Returns false if the record does not exist at this snapshot.

Installs a tombstone version rather than removing anything, so readers at older snapshots still see the record alive.

Examples found in repository?
examples/basic.rs (line 99)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}

pub fn scan<T: Versioned>(&mut self) -> Result<Vec<Ref<'_, T>>>

Every record of T visible at this snapshot, in primary key order.

Equivalent to Transaction::scan_where with a predicate that accepts everything. Under Serializable that means the whole table becomes part of the read set, so any concurrent insert or update anywhere in it will abort this transaction. Prefer scan_where when you have a predicate: it is both faster to validate and far less likely to abort.

Examples found in repository?
examples/game.rs (line 130)
128fn total_gold(db: &Database) -> Result<i64> {
129    let mut tx = db.begin();
130    Ok(tx.scan::<Hero>()?.iter().map(|h| h.gold).sum())
131}
132
133fn act(n: u32, title: &str) {
134    println!("\n\x1b[1m── Act {n}. {title}\x1b[0m");
135}
136
137// A narrative example: one linear script of acts, read top to bottom. Splitting
138// it into helpers to satisfy the length and complexity budgets would make it
139// harder to follow, which is the only thing this file is for.
140#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}
More examples
Hide additional examples
examples/basic.rs (line 72)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}
examples/concurrent.rs (line 61)
29fn main() -> Result<()> {
30    let db = Arc::new(Database::open(Config::in_memory())?);
31    db.register::<Account>()?;
32
33    db.transaction(|tx| {
34        for id in 0..ACCOUNTS {
35            tx.insert(Account {
36                id,
37                balance: STARTING_BALANCE,
38            })?;
39        }
40        Ok(())
41    })?;
42
43    let total_before = total(&db)?;
44    println!("starting total: {total_before}");
45
46    let stop = Arc::new(AtomicBool::new(false));
47    let reads = Arc::new(AtomicU64::new(0));
48    let retries = Arc::new(AtomicU64::new(0));
49
50    // ---- a reader that must never observe a torn transfer -----------------
51    // Money moves between accounts constantly. Under snapshot isolation this
52    // thread's scans must always sum to the same total: it either sees both
53    // halves of a transfer or neither.
54    let auditor = {
55        let db = Arc::clone(&db);
56        let stop = Arc::clone(&stop);
57        let reads = Arc::clone(&reads);
58        thread::spawn(move || -> Result<()> {
59            while !stop.load(Ordering::Relaxed) {
60                let mut tx = db.begin();
61                let sum: i64 = tx.scan::<Account>()?.iter().map(|a| a.balance).sum();
62                assert_eq!(
63                    sum,
64                    ACCOUNTS as i64 * STARTING_BALANCE,
65                    "a reader observed a partially applied transfer"
66                );
67                reads.fetch_add(1, Ordering::Relaxed);
68            }
69            Ok(())
70        })
71    };
72
73    // ---- writers ----------------------------------------------------------
74    let start = Instant::now();
75    let mut writers = Vec::new();
76    for t in 0..TRANSFER_THREADS {
77        let db = Arc::clone(&db);
78        let retries = Arc::clone(&retries);
79        writers.push(thread::spawn(move || -> Result<()> {
80            // A cheap per-thread PRNG; no dependency needed for this.
81            let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64 + 1);
82            let mut next = move || {
83                seed ^= seed << 13;
84                seed ^= seed >> 7;
85                seed ^= seed << 17;
86                seed
87            };
88
89            for _ in 0..TRANSFERS_PER_THREAD {
90                let from = next() % ACCOUNTS;
91                let to = next() % ACCOUNTS;
92                if from == to {
93                    continue;
94                }
95
96                let mut attempts = 0u64;
97                // Serializable, because the transfer's *write* depends on a
98                // balance it *read* — the write-skew shape. Snapshot isolation
99                // would let two concurrent transfers both pass the check.
100                db.transaction_with::<Serializable, _, _>(|tx| {
101                    attempts += 1;
102                    let balance = tx.get::<Account>(&from)?.map(|a| a.balance).unwrap_or(0);
103                    if balance < 10 {
104                        return Ok(());
105                    }
106                    tx.update::<Account>(&from, |a| a.balance -= 10)?;
107                    tx.update::<Account>(&to, |a| a.balance += 10)?;
108                    Ok(())
109                })?;
110                retries.fetch_add(attempts - 1, Ordering::Relaxed);
111            }
112            Ok(())
113        }));
114    }
115
116    for w in writers {
117        w.join().expect("writer panicked")?;
118    }
119    let elapsed = start.elapsed();
120
121    stop.store(true, Ordering::Relaxed);
122    auditor.join().expect("auditor panicked")?;
123
124    // ---- results ----------------------------------------------------------
125    let total_after = total(&db)?;
126    let committed = (TRANSFER_THREADS * TRANSFERS_PER_THREAD) as u64;
127
128    println!("final total:    {total_after}");
129    assert_eq!(total_before, total_after, "money was created or destroyed");
130
131    println!("\n{committed} transfers in {elapsed:.2?}");
132    println!(
133        "  {:.0} transfers/sec",
134        committed as f64 / elapsed.as_secs_f64()
135    );
136    println!(
137        "  {} concurrent audit scans completed, none of them blocked",
138        reads.load(Ordering::Relaxed)
139    );
140    println!(
141        "  {} retries ({:.1} per transfer)",
142        retries.load(Ordering::Relaxed),
143        retries.load(Ordering::Relaxed) as f64 / committed as f64
144    );
145
146    // The GC watermark should be advancing. If `active_transactions` climbs and
147    // the watermark stalls, some transaction is being leaked — see engine::gc.
148    let stats = db.stats();
149    println!(
150        "\ngc watermark {:?}, {} transactions still live",
151        stats.watermark, stats.active_transactions
152    );
153
154    println!("\n✓ no torn reads, no lost updates, conservation held");
155
156    // Slow readers are what pin the GC watermark. Left here as the shape of the
157    // thing to watch for, not as something this example demonstrates going wrong.
158    thread::sleep(Duration::from_millis(1));
159    Ok(())
160}
161
162fn total(db: &Database) -> Result<i64> {
163    let mut tx = db.begin();
164    Ok(tx.scan::<Account>()?.iter().map(|a| a.balance).sum())
165}
examples/isolation.rs (line 125)
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}

pub fn scan_where<T, P>(&mut self, predicate: P) -> Result<Vec<Ref<'_, T>>>
where T: Versioned, P: Fn(&T) -> bool + Send + Sync + 'static,

Every record of T visible at this snapshot that satisfies predicate, in primary key order.

The predicate is handed to the engine rather than applied by the caller afterwards, and that is the entire point under Serializable: the engine records it, re-evaluates it at commit, and aborts if the set of matching rows changed. That is what makes phantoms visible — see PredicateRead — and it is why the first form below is a materially stronger statement than the second, even though both return the same rows:

db.transaction_with::<Serializable, _, _>(|tx| {
    // The engine holds the predicate and re-runs it at commit, so an
    // overdrawn account appearing concurrently aborts this transaction.
    let overdrawn = tx.scan_where::<Account, _>(|a| a.balance < 0)?;
    assert_eq!(overdrawn.len(), 1);

    // Same rows, weaker claim: this says only "I read the whole table",
    // so *any* concurrent write to it aborts, and no phantom is named.
    let same: Vec<_> = tx
        .scan::<Account>()?
        .into_iter()
        .filter(|a| a.balance < 0)
        .collect();
    assert_eq!(same.len(), 1);
    Ok(())
})?;

predicate must be Send + Sync + 'static because it is retained for the transaction’s lifetime and re-run at commit. It should be pure: it will be called more than once, and on rows this call never returns.

Examples found in repository?
examples/game.rs (line 125)
124fn party_size<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>) -> Result<usize> {
125    Ok(tx.scan_where::<PartyMember, _>(|m| m.party == PARTY)?.len())
126}
127
128fn total_gold(db: &Database) -> Result<i64> {
129    let mut tx = db.begin();
130    Ok(tx.scan::<Hero>()?.iter().map(|h| h.gold).sum())
131}
132
133fn act(n: u32, title: &str) {
134    println!("\n\x1b[1m── Act {n}. {title}\x1b[0m");
135}
136
137// A narrative example: one linear script of acts, read top to bottom. Splitting
138// it into helpers to satisfy the length and complexity budgets would make it
139// harder to follow, which is the only thing this file is for.
140#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn scan_index<T, K, R>( &mut self, index: Index<T, K>, range: R, ) -> Result<Vec<Ref<'_, T>>>
where T: Versioned, K: Encodable, R: RangeBounds<K>,

Range scan over a secondary index, in index key order.

The index is the associated const the derive emitted for the field — #[mvcc(index)] owner: u64 gives Item::OWNER — so a wrong name does not compile, and neither does a range of the wrong type. Naming an index by string would make both runtime outcomes instead: a bad name an error, a mistyped range an empty result.

use mvcc::{Config, Database, Mvcc};

#[derive(Mvcc, Clone)]
struct Item {
    #[mvcc(primary_key)] id: u64,
    #[mvcc(index)] owner: u64,
}

let db = Database::open(Config::in_memory())?;
db.register::<Item>()?;
let mut tx = db.begin();
let owned = tx.scan_index(Item::OWNER, 1u64..=3)?;
assert!(owned.is_empty());

Scanning that index with a range of the wrong type does not compile:

// `owner` is a `u64`, so a `String` range has nothing to unify with.
tx.scan_index(Item::OWNER, "a".to_string().."b".to_string()).unwrap();

Under Serializable the range is recorded and re-scanned at commit, so a concurrent insert into the range aborts this transaction while one outside it does not.

Examples found in repository?
examples/game.rs (line 94)
89fn inventory<I: mvcc::IsolationLevel>(
90    tx: &mut mvcc::Transaction<'_, I>,
91    hero: u64,
92) -> Result<Vec<(u64, String, i32)>> {
93    Ok(tx
94        .scan_index(Item::OWNER, hero..=hero)?
95        .iter()
96        .map(|i| (i.id, i.name.clone(), i.weight))
97        .collect())
98}
99
100/// How much a hero is carrying. Uses the same index read, so under
101/// `Serializable` it is *this* that a concurrent pickup invalidates.
102fn carried<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>, hero: u64) -> Result<i32> {
103    Ok(tx
104        .scan_index(Item::OWNER, hero..=hero)?
105        .iter()
106        .map(|i| i.weight)
107        .sum())
108}
109
110fn gold_of<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>, hero: u64) -> Result<i64> {
111    Ok(tx.get::<Hero>(&hero)?.map(|h| h.gold).unwrap_or(0))
112}
113
114fn name_of<I: mvcc::IsolationLevel>(
115    tx: &mut mvcc::Transaction<'_, I>,
116    hero: u64,
117) -> Result<String> {
118    Ok(tx
119        .get::<Hero>(&hero)?
120        .map(|h| h.name.clone())
121        .unwrap_or_else(|| "?".into()))
122}
123
124fn party_size<I: mvcc::IsolationLevel>(tx: &mut mvcc::Transaction<'_, I>) -> Result<usize> {
125    Ok(tx.scan_where::<PartyMember, _>(|m| m.party == PARTY)?.len())
126}
127
128fn total_gold(db: &Database) -> Result<i64> {
129    let mut tx = db.begin();
130    Ok(tx.scan::<Hero>()?.iter().map(|h| h.gold).sum())
131}
132
133fn act(n: u32, title: &str) {
134    println!("\n\x1b[1m── Act {n}. {title}\x1b[0m");
135}
136
137// A narrative example: one linear script of acts, read top to bottom. Splitting
138// it into helpers to satisfy the length and complexity budgets would make it
139// harder to follow, which is the only thing this file is for.
140#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}
More examples
Hide additional examples
examples/basic.rs (line 81)
24fn main() -> Result<()> {
25    let db = Database::open(Config::in_memory())?;
26    db.register::<Account>()?;
27
28    // ---- insert -----------------------------------------------------------
29    // `transaction` runs the closure, commits it, and retries it if it hits a
30    // retriable conflict. Snapshot isolation by default.
31    db.transaction(|tx| {
32        tx.insert(Account {
33            id: 1,
34            owner: "ada".into(),
35            branch: 10,
36            balance: 500,
37        })?;
38        tx.insert(Account {
39            id: 2,
40            owner: "bob".into(),
41            branch: 10,
42            balance: 250,
43        })?;
44        tx.insert(Account {
45            id: 3,
46            owner: "cleo".into(),
47            branch: 20,
48            balance: 900,
49        })?;
50        Ok(())
51    })?;
52
53    // ---- read -------------------------------------------------------------
54    let mut tx = db.begin();
55    let ada = tx.get::<Account>(&1)?.expect("just inserted");
56    println!("ada: branch {}, balance {}", ada.branch, ada.balance);
57
58    // A read-only transaction has nothing to commit; dropping it rolls back,
59    // which for a reader means simply releasing its snapshot.
60    drop(tx);
61
62    // ---- update -----------------------------------------------------------
63    db.transaction(|tx| {
64        tx.update::<Account>(&1, |a| a.balance -= 100)?;
65        tx.update::<Account>(&2, |a| a.balance += 100)?;
66        Ok(())
67    })?;
68
69    // ---- scan by primary key ----------------------------------------------
70    let mut tx = db.begin();
71    println!("\nall accounts:");
72    for account in tx.scan::<Account>()? {
73        println!(
74            "  {:>4}  {:<6} branch {}  {:>5}",
75            account.id, account.owner, account.branch, account.balance
76        );
77    }
78
79    // ---- scan by secondary index ------------------------------------------
80    println!("\nbranch 10:");
81    for account in tx.scan_index(Account::BRANCH, 10u32..=10)? {
82        println!("  {} ({})", account.owner, account.balance);
83    }
84    drop(tx);
85
86    // ---- constraint violations --------------------------------------------
87    let mut tx = db.begin();
88    let duplicate = tx.insert(Account {
89        id: 99,
90        owner: "ada".into(),
91        branch: 30,
92        balance: 0,
93    });
94    println!("\nreusing owner 'ada': {}", duplicate.unwrap_err());
95    drop(tx);
96
97    // ---- delete -----------------------------------------------------------
98    db.transaction(|tx| {
99        let existed = tx.delete::<Account>(&3)?;
100        println!("deleted cleo: {existed}");
101        Ok(())
102    })?;
103
104    let mut tx = db.begin();
105    println!(
106        "cleo now: {:?}",
107        tx.get::<Account>(&3)?.map(|r| r.to_owned())
108    );
109
110    Ok(())
111}

pub fn commit(self) -> Result<()>

Commit.

Consumes the transaction, so use-after-commit is a compile error rather than a runtime check.

Examples found in repository?
examples/isolation.rs (line 90)
32fn main() -> Result<()> {
33    let db = Database::open(Config::in_memory())?;
34    db.register::<Doctor>()?;
35    db.register::<Counter>()?;
36
37    db.transaction(|tx| {
38        tx.insert(Doctor {
39            id: 1,
40            name: "ada".into(),
41            on_call: true,
42        })?;
43        tx.insert(Doctor {
44            id: 2,
45            name: "bob".into(),
46            on_call: true,
47        })?;
48        tx.insert(Counter { id: 1, value: 0 })
49    })?;
50
51    // ------------------------------------------------------------------
52    banner("1. Snapshot: a reader is unaffected by concurrent commits");
53    {
54        let mut reader = db.begin_with::<Snapshot>();
55        let before = reader.get::<Counter>(&1)?.unwrap().value;
56
57        // Someone else commits, start to finish, while `reader` is open.
58        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 42).map(|_| ()))?;
59
60        let after = reader.get::<Counter>(&1)?.unwrap().value;
61        println!("  reader saw {before} before, {after} after a concurrent commit");
62        assert_eq!(before, after, "snapshot isolation must be stable");
63        println!("  ✓ the snapshot held: readers never block and never change under you");
64    }
65
66    // ------------------------------------------------------------------
67    banner("2. ReadCommitted: each statement sees a fresh snapshot");
68    {
69        let mut reader = db.begin_with::<ReadCommitted>();
70        let before = reader.get::<Counter>(&1)?.unwrap().value;
71
72        db.transaction(|tx| tx.update::<Counter>(&1, |c| c.value = 99).map(|_| ()))?;
73
74        let after = reader.get::<Counter>(&1)?.unwrap().value;
75        println!("  reader saw {before} before, {after} after a concurrent commit");
76        assert_ne!(
77            before, after,
78            "read committed should observe the new commit"
79        );
80        println!("  ✓ non-repeatable read — the tradeoff this level makes for cheapness");
81    }
82
83    // ------------------------------------------------------------------
84    banner("3. Write-write conflict: first committer wins");
85    {
86        let mut first = db.begin_with::<Snapshot>();
87        let mut second = db.begin_with::<Snapshot>();
88
89        first.update::<Counter>(&1, |c| c.value += 1)?;
90        first.commit()?;
91
92        // `second` took its snapshot before `first` committed, so writing here
93        // would silently drop `first`'s update.
94        let result = second.update::<Counter>(&1, |c| c.value += 1);
95        match result {
96            Err(e) => println!(
97                "  second transaction: {e}  (retriable: {})",
98                e.is_retriable()
99            ),
100            Ok(_) => unreachable!("the stale write should have been rejected"),
101        }
102        println!("  ✓ no lost update");
103    }
104
105    // ------------------------------------------------------------------
106    banner("4. Write skew: allowed under Snapshot");
107    {
108        // The rule: at least one doctor must stay on call. Each transaction
109        // checks the rule, sees it satisfied, and takes a *different* doctor
110        // off call — so there is no write-write conflict to catch them.
111        db.transaction(|tx| {
112            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
113            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
114        })?;
115
116        let mut t1 = db.begin_with::<Snapshot>();
117        let mut t2 = db.begin_with::<Snapshot>();
118
119        let t1_sees = t1.get::<Doctor>(&1)?.unwrap().on_call as u8
120            + t1.get::<Doctor>(&2)?.unwrap().on_call as u8;
121        let t2_sees = t2.get::<Doctor>(&1)?.unwrap().on_call as u8
122            + t2.get::<Doctor>(&2)?.unwrap().on_call as u8;
123        let names: Vec<String> = {
124            let mut tx = db.begin();
125            tx.scan::<Doctor>()?
126                .iter()
127                .map(|d| d.name.clone())
128                .collect()
129        };
130        println!("  on the rota: {}", names.join(", "));
131        println!("  t1 counts {t1_sees} on call, t2 counts {t2_sees} — both think it is safe");
132
133        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
134        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
135        t1.commit()?;
136        t2.commit()?;
137
138        let mut check = db.begin();
139        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
140            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
141        println!("  ✗ {remaining} doctors on call — the invariant is broken");
142        println!("    Both transactions were individually legal. This is write skew,");
143        println!("    and it is why Snapshot is not Serializable.");
144    }
145
146    // ------------------------------------------------------------------
147    banner("5. Write skew: prevented under Serializable");
148    {
149        db.transaction(|tx| {
150            tx.update::<Doctor>(&1, |d| d.on_call = true)?;
151            tx.update::<Doctor>(&2, |d| d.on_call = true).map(|_| ())
152        })?;
153
154        let mut t1 = db.begin_with::<Serializable>();
155        let mut t2 = db.begin_with::<Serializable>();
156
157        // Both read both rows, exactly as before.
158        let _ = t1.get::<Doctor>(&1)?;
159        let _ = t1.get::<Doctor>(&2)?;
160        let _ = t2.get::<Doctor>(&1)?;
161        let _ = t2.get::<Doctor>(&2)?;
162
163        t1.update::<Doctor>(&1, |d| d.on_call = false)?;
164        t2.update::<Doctor>(&2, |d| d.on_call = false)?;
165
166        t1.commit()?;
167        match t2.commit() {
168            Err(e) => println!("  t2: {e}  (retriable: {})", e.is_retriable()),
169            Ok(()) => unreachable!("t2 read a row t1 changed; it must not commit"),
170        }
171
172        let mut check = db.begin();
173        let remaining = check.get::<Doctor>(&1)?.unwrap().on_call as u8
174            + check.get::<Doctor>(&2)?.unwrap().on_call as u8;
175        println!("  ✓ {remaining} doctor still on call — the invariant held");
176        println!("    t2 read doctor 1, which t1 changed, so t2 could not be serialized.");
177    }
178
179    // ------------------------------------------------------------------
180    banner("6. Retries are the normal way to use Serializable");
181    {
182        // `transaction_with` reruns the closure on a retriable failure, so the
183        // abort in scenario 5 becomes invisible to the caller.
184        let mut attempts = 0;
185        db.transaction_with::<Serializable, _, _>(|tx| {
186            attempts += 1;
187            let ada = tx.get::<Doctor>(&1)?.unwrap().on_call;
188            let bob = tx.get::<Doctor>(&2)?.unwrap().on_call;
189            if ada && bob {
190                tx.update::<Doctor>(&1, |d| d.on_call = false)?;
191            }
192            Ok(())
193        })?;
194        println!("  committed after {attempts} attempt(s)");
195        println!("  ✓ write your transaction as if it runs alone; let the engine retry it");
196    }
197
198    banner("Summary");
199    println!("  ReadCommitted   cheapest; non-repeatable reads and phantoms");
200    println!("  RepeatableRead  stable snapshot; write skew possible");
201    println!("  Snapshot        stable snapshot; write skew possible   ← default");
202    println!("  Serializable    no anomalies; expect retriable aborts");
203
204    Ok(())
205}
More examples
Hide additional examples
examples/game.rs (line 253)
141fn main() -> Result<()> {
142    let db = Arc::new(Database::open(Config::in_memory())?);
143    db.register::<Hero>()?;
144    db.register::<Item>()?;
145    db.register::<PartyMember>()?;
146
147    let (ada, bram, cleo) = (1u64, 2u64, 3u64);
148
149    // -----------------------------------------------------------------------
150    act(1, "The world is created");
151
152    db.transaction(|tx| {
153        tx.insert(Hero {
154            id: ada,
155            name: "Ada".into(),
156            gold: 120,
157            strength: 100,
158        })?;
159        tx.insert(Hero {
160            id: bram,
161            name: "Bram".into(),
162            gold: 80,
163            strength: 100,
164        })?;
165        tx.insert(Hero {
166            id: cleo,
167            name: "Cleo".into(),
168            gold: 200,
169            strength: 60,
170        })?;
171
172        let mut item = |id, owner, name: &str, weight, value, kind| {
173            tx.insert(Item {
174                id,
175                owner,
176                name: name.into(),
177                weight,
178                value,
179                kind,
180                flavour: HashMap::new(),
181            })
182        };
183        item(10, ada, "Rusty Sword", 30, 15, Kind::Weapon)?;
184        item(11, ada, "Health Potion", 5, 20, Kind::Potion)?;
185        item(12, bram, "Oak Shield", 40, 35, Kind::Weapon)?;
186        item(13, cleo, "Lucky Charm", 2, 90, Kind::Trinket)?;
187        // Loot lying in the dungeon, owned by nobody.
188        item(20, GROUND, "Flaming Greatsword", 55, 500, Kind::Weapon)?;
189        item(21, GROUND, "Elven Cloak", 20, 240, Kind::Trinket)?;
190        item(22, GROUND, "Iron Helm", 35, 60, Kind::Weapon)?;
191
192        // Arbitrary per-item state, in a database row, with no schema to
193        // declare and no serialisation format to agree on.
194        tx.update::<Item>(&20, |i| {
195            i.flavour.insert("enchantment".into(), "flame".into());
196            i.flavour.insert("forged_by".into(), "Durin".into());
197        })?;
198        Ok(())
199    })?;
200
201    // A whole transaction that fails partway leaves nothing behind. The unique
202    // index on `name` refuses the second Ada, and the gold change goes with it.
203    let before = gold_of(&mut db.begin(), ada)?;
204    let doomed = db.transaction(|tx| {
205        tx.update::<Hero>(&ada, |h| h.gold += 10_000)?;
206        tx.insert(Hero {
207            id: 99,
208            name: "Ada".into(),
209            gold: 0,
210            strength: 10,
211        })
212    });
213    println!("  a duplicate hero name: {}", doomed.unwrap_err());
214    println!(
215        "  Ada's gold is still {} — the whole transaction rolled back, not just the insert",
216        gold_of(&mut db.begin(), ada)?
217    );
218    assert_eq!(gold_of(&mut db.begin(), ada)?, before);
219
220    let mut tx = db.begin();
221    for hero in [ada, bram, cleo] {
222        let name = name_of(&mut tx, hero)?;
223        let carried = carried(&mut tx, hero)?;
224        let items = inventory(&mut tx, hero)?;
225        let names: Vec<_> = items.iter().map(|(_, n, _)| n.as_str()).collect();
226        println!("  {name:<5} carries {carried:>3} — {}", names.join(", "));
227    }
228    drop(tx);
229
230    // -----------------------------------------------------------------------
231    act(2, "Two heroes reach for the same greatsword");
232
233    // Both begin, both see the sword unclaimed. The first to write takes it;
234    // the second is told immediately rather than being made to wait.
235    let mut ada_grabs = db.begin_with::<Snapshot>();
236    let mut bram_grabs = db.begin_with::<Snapshot>();
237
238    ada_grabs.update::<Item>(&20, |i| i.owner = ada)?;
239
240    match bram_grabs.update::<Item>(&20, |i| i.owner = bram) {
241        Err(e @ Error::WriteConflict { .. }) => {
242            println!("  Bram: {e}");
243            println!(
244                "  ...retriable: {} — he can try for something else",
245                e.is_retriable()
246            );
247        }
248        other => unreachable!("expected a conflict, got {other:?}"),
249    }
250
251    // A failed write does not end the transaction. Bram takes the cloak instead.
252    bram_grabs.update::<Item>(&21, |i| i.owner = bram)?;
253    ada_grabs.commit()?;
254    bram_grabs.commit()?;
255    println!("  Ada takes the greatsword, Bram takes the cloak. Nobody blocked.");
256
257    // -----------------------------------------------------------------------
258    act(3, "Cleo overloads herself — write skew, and the fix");
259
260    // Cleo can carry 60 and is carrying 2. Either 35kg helm fits; both do not.
261    // Each transaction reads her inventory, checks the total, and picks up a
262    // *different* item — so the two writes never touch the same record, and
263    // first-updater-wins has nothing to catch.
264    db.transaction(|tx| {
265        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
266        tx.insert(Item {
267            id: 23,
268            owner: GROUND,
269            name: "Steel Helm".into(),
270            weight: 35,
271            value: 70,
272            kind: Kind::Weapon,
273            flavour: HashMap::new(),
274        })
275    })?;
276
277    let pick_up = |item: u64| {
278        move |tx: &mut mvcc::Transaction<'_, Serializable>| -> Result<bool> {
279            let hero = tx.get::<Hero>(&cleo)?.expect("Cleo exists");
280            let strength = hero.strength;
281            let load = carried(tx, cleo)?;
282            let weight = tx.get::<Item>(&item)?.map(|i| i.weight).unwrap_or(0);
283            if load + weight > strength {
284                return Ok(false);
285            }
286            tx.update::<Item>(&item, |i| i.owner = cleo)?;
287            Ok(true)
288        }
289    };
290
291    // Under Snapshot, both checks pass against a stale inventory.
292    {
293        let mut t1 = db.begin_with::<Snapshot>();
294        let mut t2 = db.begin_with::<Snapshot>();
295        let (load1, load2) = (carried(&mut t1, cleo)?, carried(&mut t2, cleo)?);
296        println!(
297            "  Snapshot:     both transactions see {load1}/{load2} carried, both think a 35kg helm fits"
298        );
299        t1.update::<Item>(&22, |i| i.owner = cleo)?;
300        t2.update::<Item>(&23, |i| i.owner = cleo)?;
301        t1.commit()?;
302        t2.commit()?;
303        let over = carried(&mut db.begin(), cleo)?;
304        println!(
305            "  ✗ Cleo now carries {over} of a possible 60. Two legal transactions, one broken rule."
306        );
307    }
308
309    // Put it back and try again at Serializable.
310    db.transaction(|tx| {
311        tx.update::<Item>(&22, |i| i.owner = GROUND)?;
312        tx.update::<Item>(&23, |i| i.owner = GROUND).map(|_| ())
313    })?;
314
315    {
316        let mut t1 = db.begin_with::<Serializable>();
317        let mut t2 = db.begin_with::<Serializable>();
318        // Both read the inventory, as before.
319        let _ = carried(&mut t1, cleo)?;
320        let _ = carried(&mut t2, cleo)?;
321        t1.update::<Item>(&22, |i| i.owner = cleo)?;
322        t2.update::<Item>(&23, |i| i.owner = cleo)?;
323        t1.commit()?;
324        match t2.commit() {
325            Err(e @ Error::SerializationFailure) => println!("  Serializable: second pickup {e}"),
326            other => unreachable!("expected a serialization failure, got {other:?}"),
327        }
328        println!(
329            "  ✓ Cleo carries {} — the second pickup read an inventory the first changed.",
330            carried(&mut db.begin(), cleo)?
331        );
332    }
333
334    // In real code you would not hand-roll that. `transaction_with` retries,
335    // and the retry re-reads the inventory and correctly declines.
336    let took_it = db.transaction_with::<Serializable, _, _>(pick_up(23))?;
337    println!("  ...and on retry the second helm is refused on its merits: picked_up = {took_it}");
338
339    // -----------------------------------------------------------------------
340    act(4, "The party fills up — a phantom, not a conflict");
341
342    db.transaction(|tx| {
343        tx.insert(PartyMember {
344            hero: ada,
345            party: PARTY,
346            role: "Vanguard".into(),
347        })?;
348        tx.insert(PartyMember {
349            hero: bram,
350            party: PARTY,
351            role: "Shield".into(),
352        })?;
353        tx.insert(PartyMember {
354            hero: cleo,
355            party: PARTY,
356            role: "Scout".into(),
357        })
358    })?;
359
360    // Two newcomers apply at once, with three of four slots taken. Neither
361    // writes a row the other wrote — they insert *different* rows — so there is
362    // no write conflict to detect. What they collide on is a row that did not
363    // exist when either of them counted.
364    let (dara, finn) = (4u64, 5u64);
365    db.transaction(|tx| {
366        tx.insert(Hero {
367            id: dara,
368            name: "Dara".into(),
369            gold: 40,
370            strength: 80,
371        })?;
372        tx.insert(Hero {
373            id: finn,
374            name: "Finn".into(),
375            gold: 40,
376            strength: 80,
377        })
378    })?;
379
380    let mut d = db.begin_with::<Serializable>();
381    let mut f = db.begin_with::<Serializable>();
382    println!(
383        "  Dara counts {} members, Finn counts {} — both see a free slot",
384        party_size(&mut d)?,
385        party_size(&mut f)?
386    );
387    d.insert(PartyMember {
388        hero: dara,
389        party: PARTY,
390        role: "Healer".into(),
391    })?;
392    f.insert(PartyMember {
393        hero: finn,
394        party: PARTY,
395        role: "Healer".into(),
396    })?;
397    d.commit()?;
398    match f.commit() {
399        Err(e @ Error::SerializationFailure) => println!("  Finn: {e}"),
400        other => unreachable!("expected a serialization failure, got {other:?}"),
401    }
402    let mut tx = db.begin();
403    let mut roster: Vec<_> = tx
404        .scan_where::<PartyMember, _>(|m| m.party == PARTY)?
405        .iter()
406        .map(|m| (m.hero, m.role.clone()))
407        .collect();
408    drop(tx);
409    roster.sort();
410    let size = roster.len();
411    let mut tx = db.begin();
412    let listed: Vec<String> = roster
413        .iter()
414        .map(|(h, role)| Ok(format!("{} the {role}", name_of(&mut tx, *h)?)))
415        .collect::<Result<_>>()?;
416    drop(tx);
417    println!("  party: {}", listed.join(", "));
418    println!("  ✓ {size}/{PARTY_LIMIT} filled. Finn's insert became a phantom in Dara's count.");
419    println!("    (`scan_where` hands the engine the predicate, so it can re-check it at commit.)");
420    assert!(size <= PARTY_LIMIT);
421
422    // -----------------------------------------------------------------------
423    act(5, "A trade — two records, one atomic step");
424
425    let before = total_gold(&db)?;
426
427    let trade = |seller: u64, buyer: u64, item: u64, price: i64| {
428        let db = Arc::clone(&db);
429        move || -> Result<bool> {
430            db.transaction_with::<Serializable, _, _>(|tx| {
431                let buyer_gold = gold_of(tx, buyer)?;
432                if buyer_gold < price {
433                    return Ok(false);
434                }
435                let owned_by_seller = tx
436                    .get::<Item>(&item)?
437                    .map(|i| i.owner == seller)
438                    .unwrap_or(false);
439                if !owned_by_seller {
440                    return Ok(false);
441                }
442                tx.update::<Hero>(&buyer, |h| h.gold -= price)?;
443                tx.update::<Hero>(&seller, |h| h.gold += price)?;
444                tx.update::<Item>(&item, |i| i.owner = buyer)?;
445                Ok(true)
446            })
447        }
448    };
449
450    let sold = trade(bram, cleo, 21, 150)()?;
451    println!("  Bram sells the Elven Cloak to Cleo for 150g: {sold}");
452    let mut tx = db.begin();
453    let cloak_owner = tx.get::<Item>(&21)?.expect("cloak").owner;
454    println!(
455        "  Bram {}g, Cleo {}g, cloak owner is {}",
456        gold_of(&mut tx, bram)?,
457        gold_of(&mut tx, cleo)?,
458        name_of(&mut tx, cloak_owner)?
459    );
460    drop(tx);
461    assert_eq!(total_gold(&db)?, before, "gold must be conserved");
462    println!("  ✓ total gold unchanged at {before} — no step of that was separately visible");
463
464    // -----------------------------------------------------------------------
465    act(6, "The chronicler reads while the world moves");
466
467    // A long report scanning the whole world while combat rewrites it. It never
468    // blocks a writer, and its view never shifts underneath it.
469    let mut chronicler = db.begin_with::<Snapshot>();
470    let opening = total_gold(&db)?;
471    let seen_first = chronicler.scan::<Hero>()?.len();
472
473    for _ in 0..50 {
474        db.transaction(|tx| {
475            tx.update::<Hero>(&ada, |h| h.gold += 1)?;
476            tx.update::<Hero>(&bram, |h| h.gold -= 1)?;
477            Ok(())
478        })?;
479    }
480    db.transaction(|tx| {
481        tx.insert(Hero {
482            id: 6,
483            name: "Mira".into(),
484            gold: 0,
485            strength: 50,
486        })
487    })?;
488
489    let chron_gold: i64 = chronicler.scan::<Hero>()?.iter().map(|h| h.gold).sum();
490    let seen_after = chronicler.scan::<Hero>()?.len();
491    let in_world = db.begin().scan::<Hero>()?.len();
492    println!("  chronicler saw {seen_first} heroes at the start and still sees {seen_after};");
493    println!("  the world now holds {in_world}, after 50 commits and one new arrival");
494    println!(
495        "  chronicler totals {chron_gold}g; the world now totals {}g",
496        total_gold(&db)?
497    );
498    assert_eq!(chron_gold, opening, "a snapshot must not move");
499    chronicler.commit()?;
500    println!("  ✓ a reader that ran across 51 commits saw exactly one consistent world");
501
502    // -----------------------------------------------------------------------
503    act(7, "The raid — many adventurers at once");
504
505    let stop = Arc::new(AtomicBool::new(false));
506    let trades = Arc::new(AtomicU64::new(0));
507    let retries = Arc::new(AtomicU64::new(0));
508    let audits = Arc::new(AtomicU64::new(0));
509    let opening = total_gold(&db)?;
510    let heroes = [ada, bram, cleo, dara, finn];
511
512    // An auditor that must never catch a trade half-applied.
513    let auditor = {
514        let (db, stop, audits) = (Arc::clone(&db), Arc::clone(&stop), Arc::clone(&audits));
515        thread::spawn(move || -> Result<()> {
516            while !stop.load(Ordering::Relaxed) {
517                let mut tx = db.begin();
518                let total: i64 = tx.scan::<Hero>()?.iter().map(|h| h.gold).sum();
519                assert_eq!(total, opening, "auditor saw a half-finished trade");
520                audits.fetch_add(1, Ordering::Relaxed);
521            }
522            Ok(())
523        })
524    };
525
526    let start = Instant::now();
527    let raiders: Vec<_> = (0..4)
528        .map(|t| {
529            let (db, trades, retries) =
530                (Arc::clone(&db), Arc::clone(&trades), Arc::clone(&retries));
531            thread::spawn(move || -> Result<()> {
532                let mut seed = 0x9e37_79b9_7f4a_7c15u64 ^ (t + 1);
533                for _ in 0..400 {
534                    seed ^= seed << 13;
535                    seed ^= seed >> 7;
536                    seed ^= seed << 17;
537                    let from = heroes[(seed % heroes.len() as u64) as usize];
538                    let to = heroes[((seed >> 8) % heroes.len() as u64) as usize];
539                    if from == to {
540                        continue;
541                    }
542
543                    let mut attempts = 0u64;
544                    // Serializable, because the payment depends on a balance we
545                    // read — the same shape as Cleo's carry limit in Act 3.
546                    db.transaction_with::<Serializable, _, _>(|tx| {
547                        attempts += 1;
548                        let purse = gold_of(tx, from)?;
549                        if purse < 5 {
550                            return Ok(());
551                        }
552                        tx.update::<Hero>(&from, |h| h.gold -= 5)?;
553                        tx.update::<Hero>(&to, |h| h.gold += 5)?;
554                        Ok(())
555                    })?;
556                    trades.fetch_add(1, Ordering::Relaxed);
557                    retries.fetch_add(attempts - 1, Ordering::Relaxed);
558                }
559                Ok(())
560            })
561        })
562        .collect();
563
564    for r in raiders {
565        r.join().expect("raider panicked")?;
566    }
567    let elapsed = start.elapsed();
568    stop.store(true, Ordering::Relaxed);
569    auditor.join().expect("auditor panicked")?;
570
571    let done = trades.load(Ordering::Relaxed);
572    println!("  {done} trades across 4 threads in {elapsed:.2?}");
573    println!(
574        "  {} audit sweeps completed alongside them, none blocked, none torn",
575        audits.load(Ordering::Relaxed)
576    );
577    println!(
578        "  {} retries ({:.2} per trade) — contention on 5 heroes, handled by the engine",
579        retries.load(Ordering::Relaxed),
580        retries.load(Ordering::Relaxed) as f64 / done.max(1) as f64
581    );
582    assert_eq!(total_gold(&db)?, opening, "gold was created or destroyed");
583    println!("  ✓ total gold still {opening}");
584
585    // -----------------------------------------------------------------------
586    act(8, "Closing the ledger");
587
588    let mut tx = db.begin();
589    let mut roster: Vec<_> = tx
590        .scan::<Hero>()?
591        .iter()
592        .map(|h| (h.id, h.name.clone(), h.gold))
593        .collect();
594    roster.sort_by_key(|(id, _, _)| *id);
595    drop(tx);
596
597    println!(
598        "  {:<6} {:>6} {:>5} {:>7}  inventory",
599        "hero", "gold", "load", "worth"
600    );
601    let mut tx = db.begin();
602    for (id, name, gold) in &roster {
603        let held = tx.scan_index(Item::OWNER, *id..=*id)?;
604        let load: i32 = held.iter().map(|i| i.weight).sum();
605        let worth: i64 = held.iter().map(|i| i.value).sum();
606        let names: Vec<String> = held
607            .iter()
608            .map(|i| match i.kind {
609                Kind::Weapon => format!("[wpn] {}", i.name),
610                Kind::Potion => format!("[pot] {}", i.name),
611                Kind::Trinket => format!("[trk] {}", i.name),
612            })
613            .collect();
614        let names = if names.is_empty() {
615            "—".to_string()
616        } else {
617            names.join(", ")
618        };
619        println!("  {name:<6} {gold:>5}g {load:>4}kg {worth:>6}g  {names}");
620    }
621
622    // Potions anywhere in the world, found by predicate rather than by key —
623    // and `kind` is an ordinary Rust enum the engine knows nothing about.
624    let potions = tx.scan_where::<Item, _>(|i| i.kind == Kind::Potion)?.len();
625    let sword = tx.get::<Item>(&20)?.expect("greatsword");
626    let enchantment = sword
627        .flavour
628        .get("enchantment")
629        .cloned()
630        .unwrap_or_default();
631    let forged_by = sword.flavour.get("forged_by").cloned().unwrap_or_default();
632    drop(tx);
633    // The rule Act 3 was about, checked rather than asserted in prose.
634    let mut tx = db.begin();
635    for (id, name, _) in &roster {
636        let strength = tx.get::<Hero>(id)?.map(|h| h.strength).unwrap_or(0);
637        let load = carried(&mut tx, *id)?;
638        assert!(
639            load <= strength,
640            "{name} is over their carry limit: {load} > {strength}"
641        );
642    }
643    drop(tx);
644
645    println!("\n  potions in the world: {potions}");
646    println!("  the greatsword is {enchantment}-enchanted, forged by {forged_by}");
647
648    // Nothing is reclaimed while a transaction that could still reach it is
649    // alive. This is the number to watch in a long-lived process: a forgotten
650    // transaction pins it and version chains grow without limit.
651    let stats = db.stats();
652    println!(
653        "\n  gc watermark {:?}, {} transactions still live",
654        stats.watermark, stats.active_transactions
655    );
656    println!("  (a watermark that stops moving while writes continue is the leak to look for)");
657
658    Ok(())
659}

pub fn abort(self)

Abort and discard every write.

Infallible: rollback unlinks version pointers, which cannot fail.

Trait Implementations§

§

impl<'db, I: IsolationLevel> Drop for Transaction<'db, I>

§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl<'db, I> !RefUnwindSafe for Transaction<'db, I>

§

impl<'db, I> !Send for Transaction<'db, I>

§

impl<'db, I> !Sync for Transaction<'db, I>

§

impl<'db, I> !UnwindSafe for Transaction<'db, I>

§

impl<'db, I> Freeze for Transaction<'db, I>

§

impl<'db, I> Unpin for Transaction<'db, I>

§

impl<'db, I> UnsafeUnpin for Transaction<'db, I>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.