From 60290e5c3440629afbed571799bb07df3b8aceaa Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 29 Apr 2026 03:36:45 -0700 Subject: [PATCH] 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 --- internal/repository/lms_modules.go | 71 +++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/internal/repository/lms_modules.go b/internal/repository/lms_modules.go index 599d2d9..2a59390 100644 --- a/internal/repository/lms_modules.go +++ b/internal/repository/lms_modules.go @@ -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) { + 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 if input.Name != nil { nameText = pgtype.Text{String: *input.Name, Valid: true} } else { nameText = pgtype.Text{Valid: false} } - m, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{ + m, err := q.UpdateModule(ctx, dbgen.UpdateModuleParams{ ID: id, Name: nameText, Description: optionalTextUpdate(input.Description), @@ -112,6 +177,10 @@ func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateM } return domain.Module{}, err } + + if err := tx.Commit(ctx); err != nil { + return domain.Module{}, err + } return moduleToDomain(m), nil }