fix: avoid sort_order collisions when inserting content at a specific position

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-06-10 05:20:12 -07:00
parent 4f873fe9de
commit a704c3b29f
6 changed files with 63 additions and 17 deletions

View File

@ -39,10 +39,7 @@ func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, i
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE exam_prep.units SET sort_order = sort_order + 1 WHERE catalog_course_id = $1 AND sort_order >= $2`,
catalogCourseID, target,
); err != nil {
if err := shiftExamPrepUnitsSortOrderForInsert(ctx, tx, catalogCourseID, target); err != nil {
return domain.ExamPrepUnit{}, err
}
u, err := q.ExamPrepCreateUnit(ctx, dbgen.ExamPrepCreateUnitParams{

View File

@ -39,10 +39,7 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE courses SET sort_order = sort_order + 1 WHERE program_id = $1 AND sort_order >= $2`,
programID, target,
); err != nil {
if err := shiftCoursesSortOrderForInsert(ctx, tx, programID, target); err != nil {
return domain.Course{}, err
}
c, err := q.CreateCourse(ctx, dbgen.CreateCourseParams{

View File

@ -41,10 +41,7 @@ func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.C
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE lessons SET sort_order = sort_order + 1 WHERE module_id = $1 AND sort_order >= $2`,
moduleID, target,
); err != nil {
if err := shiftLessonsSortOrderForInsert(ctx, tx, moduleID, target); err != nil {
return domain.Lesson{}, err
}
l, err := q.CreateLesson(ctx, dbgen.CreateLessonParams{

View File

@ -40,10 +40,7 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE modules SET sort_order = sort_order + 1 WHERE course_id = $1 AND sort_order >= $2`,
courseID, target,
); err != nil {
if err := shiftModulesSortOrderForInsert(ctx, tx, courseID, target); err != nil {
return domain.Module{}, err
}
m, err := q.CreateModule(ctx, dbgen.CreateModuleParams{

View File

@ -9,6 +9,64 @@ 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.
func shiftProgramsSortOrderForInsert(ctx context.Context, tx pgx.Tx, fromPos int32) error {
_, 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)
return err
}
func shiftCoursesSortOrderForInsert(ctx context.Context, tx pgx.Tx, programID int64, fromPos int32) error {
_, 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)
return err
}
func shiftModulesSortOrderForInsert(ctx context.Context, tx pgx.Tx, courseID int64, fromPos int32) error {
_, 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)
return err
}
func shiftLessonsSortOrderForInsert(ctx context.Context, tx pgx.Tx, moduleID int64, fromPos int32) error {
_, 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)
return err
}
func shiftExamPrepUnitsSortOrderForInsert(ctx context.Context, tx pgx.Tx, catalogCourseID int64, fromPos int32) error {
_, 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)
return err
}
func repositionProgramSortOrder(ctx context.Context, tx pgx.Tx, id int64, oldPos, newPos int32) error {
if oldPos == newPos {
return nil

View File

@ -40,7 +40,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx, `UPDATE programs SET sort_order = sort_order + 1 WHERE sort_order >= $1`, target); err != nil {
if err := shiftProgramsSortOrderForInsert(ctx, tx, target); err != nil {
return domain.Program{}, err
}
p, err := q.CreateProgram(ctx, dbgen.CreateProgramParams{