Skip to content

Commit 67e5b82

Browse files
committed
fix(service): atomic upsert for signature_metadata_mapping via insert on conflict
1 parent 6331221 commit 67e5b82

1 file changed

Lines changed: 59 additions & 42 deletions

File tree

service/src/db/signature_metadata_mapping.rs

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ use entity::signature_metadata_mapping;
99
use entity::signature_metadata_mapping::Model;
1010
use sea_orm::ActiveValue::Set;
1111
use sea_orm::prelude::Uuid;
12-
use sea_orm::sea_query::Expr;
13-
use sea_orm::{
14-
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, IntoActiveModel, QueryFilter,
15-
TryIntoModel,
16-
};
12+
use sea_orm::sea_query::{Expr, OnConflict};
13+
use sea_orm::{ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter};
1714

1815
/// Builder input for [`create_or_update_signature_metadata_mapping`]. Exactly one of
1916
/// `company_id`, `game_id`, `platform_id` should be set.
@@ -119,50 +116,70 @@ pub async fn find_signature_metadata_mapping_by_platform_game_company_and_provid
119116
.await
120117
}
121118

122-
/// Upsert a signature metadata mapping. Looks up the existing row by its identifying tuple,
123-
/// updates it in place if found, or inserts a new row otherwise. Touches `updated_at` on every call.
119+
/// `matched_name` and `matched_year` use COALESCE on conflict so a follow-up
120+
/// call that doesn't carry those fields (e.g. a hash-only retry) preserves
121+
/// values written by an earlier hit. Every other column is overwritten.
124122
pub async fn create_or_update_signature_metadata_mapping(
125123
input: SignatureMetadataMappingInput,
126124
db_conn: &DbConn,
127125
) -> Result<Model, DbErr> {
128-
let signature_metadata_mapping =
129-
find_signature_metadata_mapping_by_platform_game_company_and_provider(
130-
input.platform_id,
131-
input.game_id,
132-
input.company_id,
133-
input.provider,
134-
db_conn,
135-
)
136-
.await?;
137-
138-
let mut active_model = if let Some(signature_metadata_mapping) = signature_metadata_mapping {
139-
signature_metadata_mapping.into_active_model()
140-
} else {
141-
signature_metadata_mapping::ActiveModel {
142-
..Default::default()
126+
let target_col = match (input.game_id, input.platform_id, input.company_id) {
127+
(Some(_), None, None) => signature_metadata_mapping::Column::GameId,
128+
(None, Some(_), None) => signature_metadata_mapping::Column::PlatformId,
129+
(None, None, Some(_)) => signature_metadata_mapping::Column::CompanyId,
130+
_ => {
131+
return Err(DbErr::Custom(
132+
"SignatureMetadataMappingInput requires exactly one of game_id, platform_id, or company_id".to_string(),
133+
));
143134
}
144135
};
145136

146-
active_model.platform_id = Set(input.platform_id);
147-
active_model.game_id = Set(input.game_id);
148-
active_model.company_id = Set(input.company_id);
149-
active_model.provider = Set(input.provider);
150-
active_model.provider_id = Set(input.provider_id);
151-
active_model.match_type = Set(input.match_type);
152-
active_model.manual_match_type = Set(input.manual_match_type);
153-
active_model.failed_match_reason = Set(input.failed_match_reason);
154-
active_model.comment = Set(input.comment);
155-
active_model.automatic_match_reason = Set(input.automatic_match_reason);
156-
active_model.manually_matched_by = Set(input.manually_matched_by);
157-
if input.matched_name.is_some() {
158-
active_model.matched_name = Set(input.matched_name);
159-
}
160-
if input.matched_year.is_some() {
161-
active_model.matched_year = Set(input.matched_year);
162-
}
163-
active_model.updated_at = Set(Utc::now().fixed_offset());
137+
let active_model = signature_metadata_mapping::ActiveModel {
138+
platform_id: Set(input.platform_id),
139+
game_id: Set(input.game_id),
140+
company_id: Set(input.company_id),
141+
provider: Set(input.provider),
142+
provider_id: Set(input.provider_id),
143+
match_type: Set(input.match_type),
144+
manual_match_type: Set(input.manual_match_type),
145+
failed_match_reason: Set(input.failed_match_reason),
146+
comment: Set(input.comment),
147+
automatic_match_reason: Set(input.automatic_match_reason),
148+
manually_matched_by: Set(input.manually_matched_by),
149+
matched_name: Set(input.matched_name),
150+
matched_year: Set(input.matched_year),
151+
updated_at: Set(Utc::now().fixed_offset()),
152+
..Default::default()
153+
};
164154

165-
active_model = active_model.save(db_conn).await?;
155+
let on_conflict =
156+
OnConflict::columns([target_col, signature_metadata_mapping::Column::Provider])
157+
.update_columns([
158+
signature_metadata_mapping::Column::ProviderId,
159+
signature_metadata_mapping::Column::MatchType,
160+
signature_metadata_mapping::Column::ManualMatchType,
161+
signature_metadata_mapping::Column::FailedMatchReason,
162+
signature_metadata_mapping::Column::Comment,
163+
signature_metadata_mapping::Column::AutomaticMatchReason,
164+
signature_metadata_mapping::Column::ManuallyMatchedBy,
165+
signature_metadata_mapping::Column::UpdatedAt,
166+
])
167+
.value(
168+
signature_metadata_mapping::Column::MatchedName,
169+
Expr::cust(
170+
"COALESCE(EXCLUDED.matched_name, signature_metadata_mapping.matched_name)",
171+
),
172+
)
173+
.value(
174+
signature_metadata_mapping::Column::MatchedYear,
175+
Expr::cust(
176+
"COALESCE(EXCLUDED.matched_year, signature_metadata_mapping.matched_year)",
177+
),
178+
)
179+
.to_owned();
166180

167-
active_model.try_into_model()
181+
signature_metadata_mapping::Entity::insert(active_model)
182+
.on_conflict(on_conflict)
183+
.exec_with_returning(db_conn)
184+
.await
168185
}

0 commit comments

Comments
 (0)