8. Optimistic concurrency
Mark an RSL field with @rent.version. Updating an entity instance automatically adds its old version to the predicate and increments the stored version:
let published = client
.update_post_one(&original)
.published(true)
.save_one()
.await?;
assert_eq!(published.version, original.version + 1);
let rejected = client
.update_post_one(&stale)
.title("Stale edit")
.save_one()
.await;
assert!(matches!(rejected, Err(EntityError::OptimisticLock { .. })));Imports and bulk edits
An import can use an upsert without leaving an older editor's version token valid:
use generated::post;
let imported = client
.upsert_post(post::fields::ID)
.id(published.id)
.title("Updated by import")
.body("Import content")
.published(true)
.author_id(ada.id)
.save()
.await?;
assert_eq!(imported.version, published.version + 1);The insert branch uses the initial version; the conflict branch increments the existing version. Fields marked
@rent.immutable keep their existing values on conflict. The upsert itself is unconditional: choose an
entity-targeted update when the write must reject stale input.
Filtered updates also increment the version on each matched row:
client
.update_post()
.id_eq(imported.id)
.published(false)
.save()
.await?;
let moderated = client
.post()
.id_eq(imported.id)
.only()
.await?;
assert_eq!(moderated.version, imported.version + 1);Saving published after the import, or imported after moderation, returns OptimisticLock. The executable
chapter checks both cases. Declare only one version field, using a non-null mutable UInt or BigInt outside
the primary key. Version exhaustion rejects the write; it never resets the counter. Raw SQL writers must
increment the version as part of their own update statement.
See the @rent.version declaration in
crates/rent/examples/tutorial_07_optimistic_locking/schema.rsl.
Run cargo run -p rent --example tutorial_07_optimistic_locking. The same stale-write workflow runs under nextest.