Skip to main content

Mvcc

Derive Macro Mvcc 

#[derive(Mvcc)]
{
    // Attributes available to this derive:
    #[mvcc]
}
Expand description

Make a struct storable in a Database, by implementing Versioned for it.

Exactly one field must be marked #[mvcc(primary_key)]. Any others may be indexed. The struct must also be Clone, because an update copies the record before mutating it into a new version.

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

#[derive(Mvcc, Clone, Debug)]
#[mvcc(table = "accounts")]
pub struct Account {
    #[mvcc(primary_key)]
    pub id: u64,

    /// No two accounts may share an owner.
    #[mvcc(index(unique))]
    pub owner: String,

    /// Range-scannable, duplicates allowed.
    #[mvcc(index)]
    pub branch: u32,

    pub balance: i64,
}

let db = Database::open(Config::in_memory())?;
db.register::<Account>()?;

db.transaction(|tx| {
    tx.insert(Account { id: 1, owner: "ada".into(), branch: 10, balance: 500 })
})?;

// The derive emits `Account::BRANCH` for the indexed field, and that const —
// not a string — is what a scan takes.
let mut tx = db.begin();
let at_branch_10 = tx.scan_index(Account::BRANCH, 10u32..=10)?;
assert_eq!(at_branch_10.len(), 1);

§Attribute reference

attributepositionmeaning
table = "name"structtable name, used only in error messages; defaults to the type name
primary_keyfieldrequired, exactly one
indexfieldsecondary index on this field
index(unique)fieldunique secondary index

An index is always named after its field. There is no rename knob: the name is not a string anyone types, it is the associated const above, so renaming it would only decouple the const from the field it reads.

There is no skip: nothing is serialised, so every field is simply carried along in the struct. Fields need no traits beyond what Versioned requires of the struct as a whole — a record can hold a HashMap, an Instant, or a function pointer.

§What it expands to

  • impl Versioned for Account — key type and extraction, memcmp key encoding, and the index descriptor table.
  • a private static holding the index descriptors, const-constructed with extract as a plain fn pointer.
  • a private static OnceLock<TableId>, filled by Database::register.
  • Account::OWNER: Index<Account, String> — one associated const per indexed field, named after the field in upper case, which is how scans name an index. Carrying the field’s type is the point: it is what makes tx.scan_index(Account::OWNER, 1u64..=2) a type error rather than a scan that matches nothing.

Everything is emitted inside a const _: () = { … }; block so the generated statics cannot collide with user items or with a second derive in the same module.

§What it deliberately does not do

It does not make the struct itself transactional. Account gains no interior mutability, no Drop, and no hidden fields — it stays a plain Rust struct you can construct, match on, and pass around. All transactional behaviour lives on Transaction, which is where errors can actually be returned.

§Compile errors

Missing a primary key is rejected at expansion time rather than producing a type that fails obscurely later:

#[derive(Mvcc, Clone)]
struct NoKey {
    name: String,
}

So is declaring two of them, and so is applying the derive to an enum, a union, or a tuple struct.

The macro itself lives in the mvcc-derive crate. It is documented here, on the re-export, because a compiling example needs the Versioned trait it implements — which lives in this crate, so documenting it from the other direction would mean a dependency cycle. Derive Versioned. Documented at mvcc::Mvcc, which is how users reach it.