From 2997d7e4f991784a8c13251db123d4586f47b1a5 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Tue, 17 Mar 2026 14:44:16 -0400 Subject: [PATCH] Updated name to be unique. Hopefully not a problem --- src/error.rs | 3 + .../m20260317_000007_unique_artist_album.rs | 101 ++++++++++++++++++ src/migration/mod.rs | 2 + src/queries/albums.rs | 17 ++- src/queries/artists.rs | 15 ++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/migration/m20260317_000007_unique_artist_album.rs diff --git a/src/error.rs b/src/error.rs index 1b9cb2b..09eca0f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,9 @@ pub enum DbError { #[error("serialization error: {0}")] Serialization(#[from] serde_json::Error), + + #[error("{0}")] + Other(String), } pub type DbResult = Result; diff --git a/src/migration/m20260317_000007_unique_artist_album.rs b/src/migration/m20260317_000007_unique_artist_album.rs new file mode 100644 index 0000000..6dd8244 --- /dev/null +++ b/src/migration/m20260317_000007_unique_artist_album.rs @@ -0,0 +1,101 @@ +use sea_orm_migration::prelude::*; + +use super::m20260317_000001_create_artists::Artists; +use super::m20260317_000002_create_albums::Albums; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Drop the old non-unique index on artists.name + manager + .drop_index( + Index::drop() + .name("idx_artists_name") + .table(Artists::Table) + .to_owned(), + ) + .await?; + + // Create a unique index on artists.name + manager + .create_index( + Index::create() + .name("idx_artists_name_unique") + .table(Artists::Table) + .col(Artists::Name) + .unique() + .to_owned(), + ) + .await?; + + // Drop the old non-unique index on albums.name + manager + .drop_index( + Index::drop() + .name("idx_albums_name") + .table(Albums::Table) + .to_owned(), + ) + .await?; + + // Create a unique composite index on albums(name, album_artist) + manager + .create_index( + Index::create() + .name("idx_albums_name_artist_unique") + .table(Albums::Table) + .col(Albums::Name) + .col(Albums::AlbumArtist) + .unique() + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("idx_albums_name_artist_unique") + .table(Albums::Table) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_albums_name") + .table(Albums::Table) + .col(Albums::Name) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .name("idx_artists_name_unique") + .table(Artists::Table) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_artists_name") + .table(Artists::Table) + .col(Artists::Name) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 046f0f8..c130c33 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -6,6 +6,7 @@ mod m20260317_000003_create_tracks; mod m20260317_000004_create_wanted_items; mod m20260317_000005_create_download_queue; mod m20260317_000006_create_search_cache; +mod m20260317_000007_unique_artist_album; pub struct Migrator; @@ -19,6 +20,7 @@ impl MigratorTrait for Migrator { Box::new(m20260317_000004_create_wanted_items::Migration), Box::new(m20260317_000005_create_download_queue::Migration), Box::new(m20260317_000006_create_search_cache::Migration), + Box::new(m20260317_000007_unique_artist_album::Migration), ] } } diff --git a/src/queries/albums.rs b/src/queries/albums.rs index bcb296f..a5de1b0 100644 --- a/src/queries/albums.rs +++ b/src/queries/albums.rs @@ -24,6 +24,7 @@ pub async fn upsert( return Ok(existing); } + // Try to insert — catch unique constraint race let active = ActiveModel { name: Set(name.to_string()), album_artist: Set(album_artist.to_string()), @@ -31,7 +32,21 @@ pub async fn upsert( artist_id: Set(artist_id), ..Default::default() }; - Ok(active.insert(db).await?) + match active.insert(db).await { + Ok(inserted) => Ok(inserted), + Err(DbErr::Exec(RuntimeErr::SqlxError(sqlx_err))) + if sqlx_err.to_string().contains("UNIQUE constraint failed") => + { + find_by_name_and_artist(db, name, album_artist) + .await? + .ok_or_else(|| { + DbError::Other(format!( + "album '{name}' by '{album_artist}' vanished after conflict" + )) + }) + } + Err(e) => Err(e.into()), + } } pub async fn get_by_id(db: &DatabaseConnection, id: i32) -> DbResult { diff --git a/src/queries/artists.rs b/src/queries/artists.rs index c97f6f4..4fc2868 100644 --- a/src/queries/artists.rs +++ b/src/queries/artists.rs @@ -26,6 +26,8 @@ pub async fn upsert(db: &DatabaseConnection, name: &str, musicbrainz_id: Option< return Ok(existing); } + // Try to insert — if we race with another task, catch the unique constraint + // violation and fall back to a lookup. let now = Utc::now().naive_utc(); let active = ActiveModel { name: Set(name.to_string()), @@ -35,7 +37,18 @@ pub async fn upsert(db: &DatabaseConnection, name: &str, musicbrainz_id: Option< similar_artists: Set("[]".to_string()), ..Default::default() }; - Ok(active.insert(db).await?) + match active.insert(db).await { + Ok(inserted) => Ok(inserted), + Err(DbErr::Exec(RuntimeErr::SqlxError(sqlx_err))) + if sqlx_err.to_string().contains("UNIQUE constraint failed") => + { + // Lost the race — another task inserted first, just look it up + find_by_name(db, name) + .await? + .ok_or_else(|| DbError::Other(format!("artist '{name}' vanished after conflict"))) + } + Err(e) => Err(e.into()), + } } pub async fn get_by_id(db: &DatabaseConnection, id: i32) -> DbResult {