Summary
Removing an issue from a cycle and then adding it back throws a 500 and the issue can never be re-added to that cycle.
Where
CycleIssue has a gorm.DeletedAt so RemoveCycleIssue (apps/api/internal/store/cycle.go) is a soft delete, but cycle_issues has a plain UNIQUE (cycle_id, issue_id) that ignores deleted_at (migration 000001). AddCycleIssue does a plain Create.
Repro
- Add issue X to cycle C.
- Remove X from C (row is soft-deleted, physical row survives).
- Add X to C again. The INSERT collides with the surviving row, Postgres returns a unique violation, the request 500s.
X can now never be re-added to C. The same failure hits CompleteCycle's transfer of incomplete issues when the target cycle previously held that issue.
Note
The codebase already handles this correctly elsewhere. PageStore.MoveTreeToProject hard-deletes project_pages for exactly this reason, and the workspace/project member upserts revive with Unscoped(). Cycle issues were missed. Every other join table (issue_assignees, issue_labels, module_issues, etc.) has no DeletedAt and hard-deletes, so they're fine.
Fix
Hard-delete cycle links, or upsert-revive the soft-deleted row, or make the constraint partial (WHERE deleted_at IS NULL).
Summary
Removing an issue from a cycle and then adding it back throws a 500 and the issue can never be re-added to that cycle.
Where
CycleIssuehas agorm.DeletedAtsoRemoveCycleIssue(apps/api/internal/store/cycle.go) is a soft delete, butcycle_issueshas a plainUNIQUE (cycle_id, issue_id)that ignoresdeleted_at(migration000001).AddCycleIssuedoes a plainCreate.Repro
X can now never be re-added to C. The same failure hits
CompleteCycle's transfer of incomplete issues when the target cycle previously held that issue.Note
The codebase already handles this correctly elsewhere.
PageStore.MoveTreeToProjecthard-deletesproject_pagesfor exactly this reason, and the workspace/project member upserts revive withUnscoped(). Cycle issues were missed. Every other join table (issue_assignees,issue_labels,module_issues, etc.) has noDeletedAtand hard-deletes, so they're fine.Fix
Hard-delete cycle links, or upsert-revive the soft-deleted row, or make the constraint partial (
WHERE deleted_at IS NULL).