From a4792206f7a34ded7673ab180979eeecd1d0ffb6 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 10 Jun 2026 05:26:17 -0700 Subject: [PATCH] fix: use two-phase bump when shifting sort_order on content insert Co-authored-by: Cursor --- internal/repository/lms_sort_order_shift.go | 86 +++++++++++++-------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/internal/repository/lms_sort_order_shift.go b/internal/repository/lms_sort_order_shift.go index 3e236ff..95c2ff1 100644 --- a/internal/repository/lms_sort_order_shift.go +++ b/internal/repository/lms_sort_order_shift.go @@ -9,61 +9,83 @@ import ( // Sequential siblings (programs global, courses per program, modules per course, lessons per module) use unique sort_order. // These helpers move id from oldPos to newPos without collisions by temporarily assigning sort_order = -id then shifting intermediates. -// shift*SortOrderForInsert makes room at fromPos by incrementing existing rows at/after that position. -// Rows are updated highest-first so the unique (parent, sort_order) index is never violated mid-shift. +// sortOrderInsertShiftBump moves affected rows into a high temporary range, then back to +// (old + 1). A single UPDATE ... SET sort_order = sort_order + 1 can violate unique +// (parent, sort_order) indexes because PostgreSQL does not apply row updates in sort order. +const sortOrderInsertShiftBump = int32(1_000_000) func shiftProgramsSortOrderForInsert(ctx context.Context, tx pgx.Tx, fromPos int32) error { + bump := sortOrderInsertShiftBump + if _, err := tx.Exec(ctx, ` +UPDATE programs +SET sort_order = sort_order + $2 +WHERE sort_order >= $1`, fromPos, bump); err != nil { + return err + } _, err := tx.Exec(ctx, ` -UPDATE programs AS t -SET sort_order = t.sort_order + 1 -FROM ( - SELECT id FROM programs WHERE sort_order >= $1 ORDER BY sort_order DESC -) AS s -WHERE t.id = s.id`, fromPos) +UPDATE programs +SET sort_order = sort_order - $2 +WHERE sort_order >= $1`, bump, bump-1) return err } func shiftCoursesSortOrderForInsert(ctx context.Context, tx pgx.Tx, programID int64, fromPos int32) error { + bump := sortOrderInsertShiftBump + if _, err := tx.Exec(ctx, ` +UPDATE courses +SET sort_order = sort_order + $3 +WHERE program_id = $1 AND sort_order >= $2`, programID, fromPos, bump); err != nil { + return err + } _, err := tx.Exec(ctx, ` -UPDATE courses AS t -SET sort_order = t.sort_order + 1 -FROM ( - SELECT id FROM courses WHERE program_id = $1 AND sort_order >= $2 ORDER BY sort_order DESC -) AS s -WHERE t.id = s.id`, programID, fromPos) +UPDATE courses +SET sort_order = sort_order - $3 +WHERE program_id = $1 AND sort_order >= $2`, programID, bump, bump-1) return err } func shiftModulesSortOrderForInsert(ctx context.Context, tx pgx.Tx, courseID int64, fromPos int32) error { + bump := sortOrderInsertShiftBump + if _, err := tx.Exec(ctx, ` +UPDATE modules +SET sort_order = sort_order + $3 +WHERE course_id = $1 AND sort_order >= $2`, courseID, fromPos, bump); err != nil { + return err + } _, err := tx.Exec(ctx, ` -UPDATE modules AS t -SET sort_order = t.sort_order + 1 -FROM ( - SELECT id FROM modules WHERE course_id = $1 AND sort_order >= $2 ORDER BY sort_order DESC -) AS s -WHERE t.id = s.id`, courseID, fromPos) +UPDATE modules +SET sort_order = sort_order - $3 +WHERE course_id = $1 AND sort_order >= $2`, courseID, bump, bump-1) return err } func shiftLessonsSortOrderForInsert(ctx context.Context, tx pgx.Tx, moduleID int64, fromPos int32) error { + bump := sortOrderInsertShiftBump + if _, err := tx.Exec(ctx, ` +UPDATE lessons +SET sort_order = sort_order + $3 +WHERE module_id = $1 AND sort_order >= $2`, moduleID, fromPos, bump); err != nil { + return err + } _, err := tx.Exec(ctx, ` -UPDATE lessons AS t -SET sort_order = t.sort_order + 1 -FROM ( - SELECT id FROM lessons WHERE module_id = $1 AND sort_order >= $2 ORDER BY sort_order DESC -) AS s -WHERE t.id = s.id`, moduleID, fromPos) +UPDATE lessons +SET sort_order = sort_order - $3 +WHERE module_id = $1 AND sort_order >= $2`, moduleID, bump, bump-1) return err } func shiftExamPrepUnitsSortOrderForInsert(ctx context.Context, tx pgx.Tx, catalogCourseID int64, fromPos int32) error { + bump := sortOrderInsertShiftBump + if _, err := tx.Exec(ctx, ` +UPDATE exam_prep.units +SET sort_order = sort_order + $3 +WHERE catalog_course_id = $1 AND sort_order >= $2`, catalogCourseID, fromPos, bump); err != nil { + return err + } _, err := tx.Exec(ctx, ` -UPDATE exam_prep.units AS t -SET sort_order = t.sort_order + 1 -FROM ( - SELECT id FROM exam_prep.units WHERE catalog_course_id = $1 AND sort_order >= $2 ORDER BY sort_order DESC -) AS s -WHERE t.id = s.id`, catalogCourseID, fromPos) +UPDATE exam_prep.units +SET sort_order = sort_order - $3 +WHERE catalog_course_id = $1 AND sort_order >= $2`, catalogCourseID, bump, bump-1) return err }