Swap module sort_order on conflict during update.
When updating a module sort_order to an occupied position in the same course, perform an atomic swap in a transaction instead of failing with a unique constraint error. Made-with: Cursor
This commit is contained in:
parent
8430b82687
commit
60290e5c34
|
|
@ -93,13 +93,78 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||||
|
q, tx, err := s.BeginTx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
var current dbgen.Module
|
||||||
|
err = tx.QueryRow(ctx, `
|
||||||
|
SELECT id, program_id, course_id, name, description, icon, sort_order, created_at, updated_at
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
|
`, id).Scan(
|
||||||
|
¤t.ID,
|
||||||
|
¤t.ProgramID,
|
||||||
|
¤t.CourseID,
|
||||||
|
¤t.Name,
|
||||||
|
¤t.Description,
|
||||||
|
¤t.Icon,
|
||||||
|
¤t.SortOrder,
|
||||||
|
¤t.CreatedAt,
|
||||||
|
¤t.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.SortOrder != nil {
|
||||||
|
targetSort := int32(*input.SortOrder)
|
||||||
|
if targetSort != current.SortOrder {
|
||||||
|
var conflictID int64
|
||||||
|
err = tx.QueryRow(ctx, `
|
||||||
|
SELECT id
|
||||||
|
FROM modules
|
||||||
|
WHERE course_id = $1
|
||||||
|
AND sort_order = $2
|
||||||
|
AND id <> $3
|
||||||
|
FOR UPDATE
|
||||||
|
`, current.CourseID, targetSort, id).Scan(&conflictID)
|
||||||
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
var tempSort int32
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
SELECT COALESCE(MIN(sort_order), 0) - 1
|
||||||
|
FROM modules
|
||||||
|
WHERE course_id = $1
|
||||||
|
`, current.CourseID).Scan(&tempSort); err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, tempSort, id); err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, current.SortOrder, conflictID); err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var nameText pgtype.Text
|
var nameText pgtype.Text
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||||
} else {
|
} else {
|
||||||
nameText = pgtype.Text{Valid: false}
|
nameText = pgtype.Text{Valid: false}
|
||||||
}
|
}
|
||||||
m, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
m, err := q.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: nameText,
|
Name: nameText,
|
||||||
Description: optionalTextUpdate(input.Description),
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
|
@ -112,6 +177,10 @@ func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateM
|
||||||
}
|
}
|
||||||
return domain.Module{}, err
|
return domain.Module{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
return moduleToDomain(m), nil
|
return moduleToDomain(m), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user