diff --git a/internal/repository/exam_prep_units.go b/internal/repository/exam_prep_units.go index 88eb0ae..f924385 100644 --- a/internal/repository/exam_prep_units.go +++ b/internal/repository/exam_prep_units.go @@ -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{ diff --git a/internal/repository/lms_courses.go b/internal/repository/lms_courses.go index b4e2116..0bd41f2 100644 --- a/internal/repository/lms_courses.go +++ b/internal/repository/lms_courses.go @@ -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{ diff --git a/internal/repository/lms_lessons.go b/internal/repository/lms_lessons.go index 62c6a49..0b06271 100644 --- a/internal/repository/lms_lessons.go +++ b/internal/repository/lms_lessons.go @@ -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{ diff --git a/internal/repository/lms_modules.go b/internal/repository/lms_modules.go index 27a0054..2a9ca51 100644 --- a/internal/repository/lms_modules.go +++ b/internal/repository/lms_modules.go @@ -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{ diff --git a/internal/repository/lms_sort_order_shift.go b/internal/repository/lms_sort_order_shift.go index 05d279e..3e236ff 100644 --- a/internal/repository/lms_sort_order_shift.go +++ b/internal/repository/lms_sort_order_shift.go @@ -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 diff --git a/internal/repository/programs.go b/internal/repository/programs.go index e83f8e9..50667b8 100644 --- a/internal/repository/programs.go +++ b/internal/repository/programs.go @@ -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{