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:
Yared Yemane 2026-04-29 03:36:45 -07:00
parent 8430b82687
commit 60290e5c34

View File

@ -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(
&current.ID,
&current.ProgramID,
&current.CourseID,
&current.Name,
&current.Description,
&current.Icon,
&current.SortOrder,
&current.CreatedAt,
&current.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
} }