From ea0c26a151534be06760b3cbf0c9e3656d6203db Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Thu, 19 Mar 2026 13:44:11 -0400 Subject: [PATCH] Implement idempotency for insertion --- src/votes/repository.rs | 132 ++++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 38 deletions(-) diff --git a/src/votes/repository.rs b/src/votes/repository.rs index 8bf5e78..18b7acb 100644 --- a/src/votes/repository.rs +++ b/src/votes/repository.rs @@ -5,11 +5,15 @@ use uuid::Uuid; use crate::votes::model::BestSlugs; pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { - query("insert into votes (slug, voter_id) values ($1, $2)") - .bind(slug) - .bind(voter_id) - .execute(db) - .await?; + query( + r#"insert into votes (slug, voter_id) + values ($1, $2) + on conflict (slug, voter_id) do nothing"#, + ) + .bind(slug) + .bind(voter_id) + .execute(db) + .await?; Ok(()) } @@ -56,26 +60,14 @@ mod postgres_tests { use crate::{ test_helpers::db::test_pool, - votes::repository::{delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote}, + votes::repository::{ + delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote, + }, }; - fn test_votes() -> [(&'static str, Uuid); 9] { - [ - ("blog_post1", Uuid::from_u128(0x1)), - ("blog_post1", Uuid::from_u128(0x2)), - ("blog_post2", Uuid::from_u128(0x3)), - ("blog_post2", Uuid::from_u128(0x4)), - ("blog_post3", Uuid::from_u128(0x5)), - ("blog_post3", Uuid::from_u128(0x6)), - ("blog_post1", Uuid::from_u128(0x7)), - ("blog_post1", Uuid::from_u128(0x8)), - ("blog_post3", Uuid::from_u128(0x9)), - ] - } - - async fn cleanup(db: &PgPool) { - for (slug, voter_id) in test_votes() { - delete_vote(slug, &voter_id, db).await.unwrap() + async fn cleanup(db: &PgPool, votes: &[(String, Uuid)]) { + for (slug, voter_id) in votes { + delete_vote(slug, voter_id, db).await.unwrap() } } @@ -83,8 +75,18 @@ mod postgres_tests { #[ignore] pub async fn postgres_tests() { let db = test_pool().await; - cleanup(&db).await; - let votes = test_votes(); + let votes = vec![ + ("postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x1)), + ("postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x2)), + ("postgres_tests_blog_post2".to_string(), Uuid::from_u128(0x3)), + ("postgres_tests_blog_post2".to_string(), Uuid::from_u128(0x4)), + ("postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x5)), + ("postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x6)), + ("postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x7)), + ("postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x8)), + ("postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x9)), + ]; + cleanup(&db, &votes).await; for (slug, voter_id) in votes.iter() { insert_new_vote(slug, voter_id, &db) @@ -92,25 +94,79 @@ mod postgres_tests { .expect("Insertions to db failed"); } - assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); - assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); - assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 3); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post1", &db) + .await + .unwrap(), + 4 + ); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post2", &db) + .await + .unwrap(), + 2 + ); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post3", &db) + .await + .unwrap(), + 3 + ); let top_2 = get_top_n_slugs(2, &db).await.unwrap(); - assert_eq!(top_2[0].slug, "blog_post1"); - assert_eq!(top_2[1].slug, "blog_post3"); + assert_eq!(top_2[0].slug, "postgres_tests_blog_post1"); + assert_eq!(top_2[1].slug, "postgres_tests_blog_post3"); - delete_vote(votes[4].0, &votes[4].1, &db).await.unwrap(); - delete_vote(votes[5].0, &votes[5].1, &db).await.unwrap(); + delete_vote(&votes[4].0, &votes[4].1, &db).await.unwrap(); + delete_vote(&votes[5].0, &votes[5].1, &db).await.unwrap(); - assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); - assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); - assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 1); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post1", &db) + .await + .unwrap(), + 4 + ); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post2", &db) + .await + .unwrap(), + 2 + ); + assert_eq!( + get_vote_count_for_slug("postgres_tests_blog_post3", &db) + .await + .unwrap(), + 1 + ); let top_2 = get_top_n_slugs(2, &db).await.unwrap(); - assert_eq!(top_2[0].slug, "blog_post1"); - assert_eq!(top_2[1].slug, "blog_post2"); + assert_eq!(top_2[0].slug, "postgres_tests_blog_post1"); + assert_eq!(top_2[1].slug, "postgres_tests_blog_post2"); - cleanup(&db).await; + cleanup(&db, &votes).await; + } + + #[tokio::test] + #[ignore] + pub async fn insert_idempotency_test() { + let db = test_pool().await; + let votes = vec![( + "insert_idempotency_test_blog_post1".to_string(), + Uuid::from_u128(0x1), + )]; + cleanup(&db, &votes).await; + + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + insert_new_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); + + let votes_count = get_vote_count_for_slug(&votes[0].0, &db).await.unwrap(); + + assert_eq!(votes_count, 1); + + cleanup(&db, &votes).await; } }