Compare commits
1 Commits
305ddff278
...
2997d7e4f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2997d7e4f9 |
@@ -13,6 +13,9 @@ pub enum DbError {
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type DbResult<T> = Result<T, DbError>;
|
||||
|
||||
101
src/migration/m20260317_000007_unique_artist_album.rs
Normal file
101
src/migration/m20260317_000007_unique_artist_album.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Album> {
|
||||
|
||||
@@ -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<Artist> {
|
||||
|
||||
Reference in New Issue
Block a user