use std::path::Path;
use std::process::Command;
use diesel::{Connection as _, PgConnection, QueryableByName, RunQueryDsl as _, sql_query};
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
fn run_autumn_ok(dir: &Path, args: &[&str], envs: &[(&str, &str)]) {
let output = Command::new(autumn_bin())
.args(args)
.current_dir(dir)
.envs(envs.iter().copied())
.output()
.expect("failed to run autumn");
assert!(
output.status.success(),
"autumn {args:?} failed (exit={:?})\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[derive(QueryableByName)]
struct TitleVersion {
#[diesel(sql_type = diesel::sql_types::Text)]
title: String,
#[diesel(sql_type = diesel::sql_types::Integer)]
lock_version: i32,
}
fn row(conn: &mut PgConnection) -> TitleVersion {
sql_query("SELECT title, lock_version FROM posts WHERE id = 1")
.get_result::<TitleVersion>(conn)
.expect("posts row 1")
}
const GUARDED_UPDATE: &str = "UPDATE posts SET title = $1, lock_version = lock_version + 1 \
WHERE id = 1 AND lock_version = $2";
#[tokio::test]
#[ignore = "requires Docker (testcontainers); run with -- --ignored"]
async fn scaffolded_lock_version_rejects_a_stale_write_and_survives_a_real_race() {
use testcontainers::runners::AsyncRunner as _;
use testcontainers_modules::postgres::Postgres;
let container = Postgres::default()
.start()
.await
.expect("failed to start Postgres testcontainer — is Docker running?");
let host = container.get_host().await.unwrap();
let port = container.get_host_port_ipv4(5432).await.unwrap();
let db_url = format!("postgres://postgres:postgres@{host}:{port}/postgres");
let envs = [("AUTUMN_DATABASE__URL", db_url.as_str())];
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path();
std::fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"lock_app\"\n",
)
.unwrap();
run_autumn_ok(
project,
&[
"generate",
"model",
"Post",
"title:String",
"lock_version:i32",
],
&[],
);
let model = std::fs::read_to_string(project.join("src/models/post.rs")).unwrap();
assert!(
model.contains("#[lock_version]\n pub lock_version: i32,"),
"generated model must carry #[lock_version]:\n{model}"
);
run_autumn_ok(project, &["migrate"], &envs);
let mut conn = PgConnection::establish(&db_url).expect("connect to postgres");
sql_query("INSERT INTO posts (title) VALUES ('original')")
.execute(&mut conn)
.expect("INSERT omitting lock_version must succeed (migration supplies DEFAULT 0)");
let seeded = row(&mut conn);
assert_eq!(seeded.lock_version, 0, "a fresh row starts at version 0");
let affected = sql_query(GUARDED_UPDATE)
.bind::<diesel::sql_types::Text, _>("first-author")
.bind::<diesel::sql_types::Integer, _>(0)
.execute(&mut conn)
.unwrap();
assert_eq!(affected, 1, "a fresh submit must apply");
let after_first = row(&mut conn);
assert_eq!(after_first.title, "first-author");
assert_eq!(after_first.lock_version, 1, "a successful update bumps");
let affected = sql_query(GUARDED_UPDATE)
.bind::<diesel::sql_types::Text, _>("second-author")
.bind::<diesel::sql_types::Integer, _>(0)
.execute(&mut conn)
.unwrap();
assert_eq!(affected, 0, "a stale submit must match zero rows");
let after_stale = row(&mut conn);
assert_eq!(
after_stale.title, "first-author",
"the stale submit must leave the row untouched"
);
assert_eq!(after_stale.lock_version, 1, "and must not bump the version");
let affected = sql_query(GUARDED_UPDATE)
.bind::<diesel::sql_types::Text, _>("second-author")
.bind::<diesel::sql_types::Integer, _>(1)
.execute(&mut conn)
.unwrap();
assert_eq!(
affected, 1,
"the retry against the fresh version must apply"
);
assert_eq!(row(&mut conn).lock_version, 2);
let mut a = PgConnection::establish(&db_url).expect("session A");
let mut b = PgConnection::establish(&db_url).expect("session B");
sql_query("BEGIN").execute(&mut a).unwrap();
let a_affected = sql_query(GUARDED_UPDATE)
.bind::<diesel::sql_types::Text, _>("A-wins")
.bind::<diesel::sql_types::Integer, _>(2)
.execute(&mut a)
.unwrap();
assert_eq!(a_affected, 1, "A holds the current version, so A applies");
sql_query("COMMIT").execute(&mut a).unwrap();
sql_query("BEGIN").execute(&mut b).unwrap();
let b_affected = sql_query(GUARDED_UPDATE)
.bind::<diesel::sql_types::Text, _>("B-wins")
.bind::<diesel::sql_types::Integer, _>(2)
.execute(&mut b)
.unwrap();
sql_query("COMMIT").execute(&mut b).unwrap();
assert_eq!(
b_affected, 0,
"B read the same version A did, so B's write must be rejected, not applied"
);
let final_row = row(&mut conn);
assert_eq!(
final_row.title, "A-wins",
"the loser's write must not have overwritten the winner's"
);
assert_eq!(final_row.lock_version, 3, "exactly one bump, not two");
}