///|
/// Optimistic concurrency control: `Session::modify_versioned` runs a version-guarded
/// `UPDATE` and raises `LostUpdate` when it matches no row — the row's version moved
/// on since it was read (SQLAlchemy's `version_id_col` / `StaleDataError`). The new
/// values and the version guard both bind as `?`, so the whole check stays
/// injection-safe. The success path runs over `@moondb.MockDriver`; the lost-update
/// path over a tiny driver that reports zero rows changed.
///
/// moon run examples/17-optimistic-lock
fn render(v : @moondb.Value) -> String {
match v {
Null => "Null"
Bool(b) => "Bool(\{b})"
Int(n) => "Int(\{n})"
Int64(n) => "Int64(\{n})"
Double(d) => "Double(\{d})"
Text(s) => "Text(\"\{s}\")"
Blob(b) => "Blob(\{b.length()} bytes)"
Time(s) => "Time(\"\{s}\")"
Numeric(s) => "Numeric(\{s})"
Uuid(b) => "Uuid(\{b.length()} bytes)"
Json(s) => "Json(\{s})"
Array(items) => "Array(\{items.length()} items)"
}
}
///|
fn params_str(vals : Array[@moondb.Value]) -> String {
"[" + vals.map(render).join(", ") + "]"
}
///|
/// A driver whose statements always change zero rows — the condition an optimistic
/// `UPDATE` reads as "someone else won the race".
struct Stale {
affected : Int64
}
///|
impl @moondb.Driver for Stale with fn execute(
self : Stale,
_sql : String,
_params : Array[@moondb.Value],
) -> @moondb.ExecResult raise @moondb.DbError {
{ rows_affected: self.affected, last_insert_id: 0L, }
}
///|
impl @moondb.Driver for Stale with fn query(
_self : Stale,
_sql : String,
_params : Array[@moondb.Value],
) -> Array[@moondb.Row] raise @moondb.DbError {
[]
}
///|
impl @moondb.Driver for Stale with fn begin(_self : Stale) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for Stale with fn commit(_self : Stale) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for Stale with fn rollback(_self : Stale) -> Unit raise @moondb.DbError {
}
///|
impl @moondb.Driver for Stale with fn close(_self : Stale) -> Unit {
}
///|
#coverage.skip
fn main {
// Bump the version in SET, guard the expected version in WHERE.
let stmt = fn() {
@moonorm.update("heroes")
.set("power", @moondb.Int(23333))
.set("version", @moondb.Int(2))
.where_("id", "=", @moondb.Int(233))
.where_("version", "=", @moondb.Int(1))
}
let (sql, params) = stmt().build()
println("-- versioned update")
println(sql)
println("params: " + params_str(params))
// Success: a row matched the expected version, so the guarded write lands.
try {
let sess = @moonorm.Session::new(@moondb.MockDriver::new())
let res = sess.modify_versioned(stmt(), what="hero")
println("")
println("-- modify_versioned ok: rows_affected \{res.rows_affected}")
sess.close()
} catch {
e => println("unexpected: \{e}")
}
// Lost update: no row matched the expected version -> LostUpdate, not a silent no-op.
let sess2 = @moonorm.Session::new(Stale::{ affected: 0L, })
try {
sess2.modify_versioned(stmt(), what="hero") |> ignore
println("-- unexpected: stale write succeeded")
} catch {
@session.LostUpdate(msg) => println("-- LostUpdate raised: " + msg)
e => println("-- other error: \{e}")
}
sess2.close()
}