mod common;
use colored::Colorize;
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use common::{print_banner, print_done, print_header, print_info, print_success, print_warning, setup_articles_schema};
use pgorm::{FromRow, InsertModel, Model, OrmError, UpdateModel, create_pool};
use std::env;
#[derive(Debug, FromRow, Model)]
#[orm(table = "articles")]
#[allow(dead_code)]
struct Article {
#[orm(id)]
id: i64,
title: String,
body: String,
version: i32,
}
#[derive(Debug, InsertModel)]
#[orm(table = "articles", returning = "Article")]
struct NewArticle {
title: String,
body: String,
}
#[derive(Debug, UpdateModel)]
#[orm(table = "articles", model = "Article", returning = "Article")]
#[allow(dead_code)]
struct ArticlePatch {
title: Option<String>,
body: Option<String>,
#[orm(version)]
version: i32,
}
fn create_articles_table(articles: &[Article]) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("ID")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
Cell::new("Title")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
Cell::new("Body")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
Cell::new("Version")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
]);
for a in articles {
table.add_row(vec![
Cell::new(a.id.to_string()).fg(Color::Yellow),
Cell::new(&a.title).fg(Color::White),
Cell::new(&a.body).fg(Color::DarkGrey),
Cell::new(a.version.to_string()).fg(Color::Magenta),
]);
}
table
}
#[tokio::main]
async fn main() -> Result<(), OrmError> {
dotenvy::dotenv().ok();
print_banner("Optimistic Locking Demo - #[orm(version)]");
let database_url =
env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env or environment");
let pool = create_pool(&database_url)?;
let client = pool.get().await?;
print_header("Setup: Creating Table and Test Data");
setup_articles_schema(&client).await?;
let a1 = NewArticle {
title: "Introduction to Rust".to_string(),
body: "Rust is a systems programming language...".to_string(),
}
.insert_returning(&client)
.await?;
let a2 = NewArticle {
title: "PostgreSQL Tips".to_string(),
body: "Use EXPLAIN ANALYZE to debug queries...".to_string(),
}
.insert_returning(&client)
.await?;
print_success(&format!(
"Inserted 2 articles (IDs: {}, {})",
a1.id, a2.id
));
print_info(&format!("Initial version for all articles: {}", a1.version));
println!();
println!("{}", "Initial state:".bold());
let all = Article::select_all(&client).await?;
println!("{}", create_articles_table(&all));
print_header("1. Successful Version-Checked Update");
print_info("Updating article 1 with current version (0)");
print_info("Generated SQL: UPDATE articles SET title=$1, version=version+1 WHERE id=$2 AND version=$3");
let patch = ArticlePatch {
title: Some("Introduction to Rust (2nd Edition)".to_string()),
body: None,
version: a1.version, };
let affected = patch.update_by_id(&client, a1.id).await?;
print_success(&format!("Updated {affected} row(s) - version is now 1"));
println!();
let all = Article::select_all(&client).await?;
println!("{}", create_articles_table(&all));
print_header("2. Version Conflict Detection (StaleRecord)");
print_info("Simulating concurrent modification:");
print_info(" User A reads article (version=1)");
print_info(" User B updates article (version becomes 2)");
print_info(" User A tries to update with stale version (1)");
let user_b_patch = ArticlePatch {
title: Some("Introduction to Rust (3rd Edition)".to_string()),
body: None,
version: 1, };
user_b_patch.update_by_id(&client, a1.id).await?;
print_success("User B: Updated successfully (version 1 -> 2)");
let user_a_patch = ArticlePatch {
title: Some("Introduction to Rust (User A Edition)".to_string()),
body: None,
version: 1, };
match user_a_patch.update_by_id(&client, a1.id).await {
Ok(_) => {
print_warning("Unexpected success - this should have failed!");
}
Err(OrmError::StaleRecord {
table,
expected_version,
..
}) => {
print_warning(&format!(
"User A: StaleRecord error! Table '{}', expected version {}",
table, expected_version
));
print_info("The record was modified by another user. Re-fetch and retry.");
}
Err(e) => return Err(e),
}
println!();
let all = Article::select_all(&client).await?;
println!("{}", create_articles_table(&all));
print_header("3. Update with RETURNING (Get New Version)");
print_info("Using update_by_id_returning to get the updated row back");
let patch = ArticlePatch {
title: None,
body: Some("Rust is a modern systems programming language focused on safety...".to_string()),
version: 2, };
let updated = patch.update_by_id_returning(&client, a1.id).await?;
print_success(&format!(
"Updated and returned: version {} -> {}",
2, updated.version
));
println!();
println!("{}", "Updated article:".bold());
println!("{}", create_articles_table(&[updated]));
print_header("4. Force Update (Skip Version Check)");
print_info("Admin override: using update_by_id_force to bypass version check");
print_info("Version is still incremented, but no WHERE version=N check");
let admin_patch = ArticlePatch {
title: Some("Introduction to Rust (Admin Override)".to_string()),
body: None,
version: 0, };
let affected = admin_patch.update_by_id_force(&client, a1.id).await?;
print_success(&format!("Force updated {affected} row(s)"));
println!();
let all = Article::select_all(&client).await?;
println!("{}", create_articles_table(&all));
print_header("5. Force Update with RETURNING");
print_info("Admin force update that returns the modified row");
let admin_patch = ArticlePatch {
title: Some("PostgreSQL Tips & Tricks".to_string()),
body: None,
version: 0, };
let updated = admin_patch
.update_by_id_force_returning(&client, a2.id)
.await?;
print_success(&format!(
"Force updated and returned article {}: version is now {}",
updated.id, updated.version
));
println!();
println!("{}", "Updated article:".bold());
println!("{}", create_articles_table(&[updated]));
print_header("6. Retry Pattern After StaleRecord");
print_info("Demonstrating the recommended retry pattern");
let target_id = a2.id;
let max_retries = 3;
for attempt in 1..=max_retries {
let current: Article = pgorm::query("SELECT * FROM articles WHERE id = $1")
.bind(target_id)
.fetch_one_as::<Article>(&client)
.await?;
print_info(&format!(
"Attempt {}: fetched version {}",
attempt, current.version
));
let patch = ArticlePatch {
title: Some(format!("PostgreSQL Tips (attempt {})", attempt)),
body: None,
version: current.version,
};
match patch.update_by_id_returning(&client, target_id).await {
Ok(updated) => {
print_success(&format!(
"Success on attempt {}! Version: {} -> {}",
attempt, current.version, updated.version
));
break;
}
Err(OrmError::StaleRecord { .. }) => {
print_warning(&format!(
"Attempt {} failed: version conflict, retrying...",
attempt
));
}
Err(e) => return Err(e),
}
}
print_header("Final State");
let all = Article::select_all(&client).await?;
println!("{}", create_articles_table(&all));
print_header("Summary: Optimistic Locking API");
let mut summary_table = Table::new();
summary_table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("Method")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
Cell::new("Version Check")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
Cell::new("Description")
.add_attribute(Attribute::Bold)
.fg(Color::Cyan),
]);
summary_table.add_row(vec![
Cell::new("update_by_id").fg(Color::Green),
Cell::new("Yes").fg(Color::Green),
Cell::new("Update with version check, returns affected rows"),
]);
summary_table.add_row(vec![
Cell::new("update_by_id_returning").fg(Color::Green),
Cell::new("Yes").fg(Color::Green),
Cell::new("Update with version check, returns updated row"),
]);
summary_table.add_row(vec![
Cell::new("update_by_id_force").fg(Color::Yellow),
Cell::new("No").fg(Color::Red),
Cell::new("Skip version check (admin override)"),
]);
summary_table.add_row(vec![
Cell::new("update_by_id_force_returning").fg(Color::Yellow),
Cell::new("No").fg(Color::Red),
Cell::new("Skip version check, returns updated row"),
]);
summary_table.add_row(vec![
Cell::new("update_by_ids").fg(Color::DarkGrey),
Cell::new("No").fg(Color::Red),
Cell::new("Bulk updates do not support version checking"),
]);
println!();
println!("{summary_table}");
print_done();
Ok(())
}