Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/buzz-cli/src/commands/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ pub async fn dispatch(
clear_assignee,
due_at,
clear_due,
expected_revision,
} => {
let task = uuid("task", &task)?;
let mut payload = Map::new();
Expand All @@ -244,6 +245,9 @@ pub async fn dispatch(
} else if let Some(value) = due_at {
payload.insert("due_at".into(), Value::String(value));
}
if let Some(value) = expected_revision {
payload.insert("expected_revision".into(), json!(value));
}
if payload.is_empty() {
return Err(CliError::Usage(
"update requires at least one mutable field".into(),
Expand Down
3 changes: 3 additions & 0 deletions crates/buzz-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2149,6 +2149,9 @@ pub enum TasksCmd {
due_at: Option<String>,
#[arg(long, default_value_t = false)]
clear_due: bool,
/// HW-017: reject the PATCH with 409 if the task's revision does not match
#[arg(long)]
expected_revision: Option<i32>,
},
/// Append a progress/comment event; use '-' to read stdin
Comment { task: String, body: String },
Expand Down
24 changes: 24 additions & 0 deletions crates/buzz-db/examples/hw017_migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use sqlx::migrate::Migrator;

#[tokio::main]
async fn main() {
let url = std::env::var("DATABASE_URL").expect("DATABASE_URL");
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&url)
.await
.expect("connect");
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("repo root (crates/buzz-db parent x2)");
let m = Migrator::new(root.join("migrations").as_path())
.await
.expect("load migrations");
m.run(&db).await.expect("run migrations");
let v: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(version),0) FROM _sqlx_migrations")
.fetch_one(&db)
.await
.expect("version");
println!("MIGRATED_TO={v}");
}
13 changes: 13 additions & 0 deletions crates/buzz-db/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ pub enum DbError {
#[error("invalid data: {0}")]
InvalidData(String),

/// A PATCH carried `expected_revision` that does not match the row's current
/// `revision`. The caller must re-fetch and retry. This is the optimistic
/// concurrency guard from HW-017: a stale write must not silently win.
#[error("task {task_id} revision mismatch: expected {expected}, found {actual}")]
StaleRevision {
/// The task that was being patched.
task_id: uuid::Uuid,
/// The revision the caller expected (the snapshot it read).
expected: i32,
/// The revision the row actually carries.
actual: i32,
},

/// A serving write admitted before the lifecycle transition is still live.
/// This is an ordinary retryable drain condition, not a safety violation.
#[error(
Expand Down
6 changes: 4 additions & 2 deletions crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,10 @@ mod postgres_tests {

// upstream carries 44 (0032-0034 and 0040 adopted from our PRs);
// fork adds 0046_task_system (PR #6425 pending upstream) and
// 0047_agent_machine_homes (AGENT-HOMES-001 PR-3).
assert_eq!(migrations.len(), 46);
// 0047_agent_machine_homes (AGENT-HOMES-001 PR-3), and
// 0050_task_optimistic_concurrency (HW-017 optimistic concurrency guard).
assert_eq!(migrations.len(), 47);
assert_eq!(migrations[46].version, 50);
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(migrations[0]
Expand Down
Loading
Loading