Files
multica/server/migrations/536_issue_duplicate_of.up.sql
b7cdd76ad8 MUL-7349: feat(server): add the duplicate-mark column and keep it correct (1/2) (#8691)
* feat(server): add the duplicate-mark column and keep it correct (MUL-7349)

Expand step for marking an issue as a duplicate of another. A duplicate
is an ordinary cancelled issue that records which issue it duplicates;
there is no duplicate status. This release adds the column and the code
that maintains it, and nothing writes a pointer yet.

- A mark survives only a write that leaves an already cancelled issue
  cancelled. Every other write drops it, so reopening removes a mark and
  cancelling again does not bring one back. Both status-writing queries
  carry the rule, which covers the UI, batch updates and background
  writers.
- Deleting an issue clears the pointers of its duplicates in the same
  transaction, the way deleting a parent detaches its children.

Shipping this before marking is what makes marking safe to roll back: the
release a rollback lands on already clears the pointer. Tests seed a mark
the way the next release will write it.

Co-authored-by: multica-agent <[email protected]>

* fix(server): compute issue updates from the locked row (MUL-7349)

UpdateIssue built its next_* values in a CTE that read the row before
the UPDATE took its row lock. A write that waited there behind another
transaction then resumed with the values it had read before the wait and
wrote them back, so an ordinary priority or status edit could restore a
duplicate mark the other transaction had just cleared — by reopening the
issue or by deleting its original — and did_change/did_activity were
decided against the pre-wait row as well.

Lock the row in the CTE instead, so every computed value comes from the
row the statement is about to write. Tests hold the clearing transaction
open and wait for the update to block on the row lock, so the
interleaving is deterministic; both fail without this change.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-23 14:59:47 +08:00

26 lines
1.4 KiB
SQL

-- Record which issue a cancelled issue duplicates (MUL-7349).
--
-- A duplicate is an ordinary cancelled issue with a pointer to the issue it
-- duplicates; there is no duplicate status. The pointer only lives while the
-- issue is cancelled: a write that leaves an already cancelled issue cancelled
-- keeps it, and every other write drops it. Reopening an issue is therefore how
-- a mark is removed, and cancelling it again does not bring the mark back.
--
-- This is the expand half of the feature: the column ships with the code that
-- maintains it, and nothing writes a pointer yet. A later release adds marking.
-- That order is what makes marking safe to roll back — the release it rolls
-- back to already clears the pointer on reopen and on delete, so no issue is
-- left carrying a mark the product no longer honours.
--
-- No foreign key (repository rule). Deleting the original clears the pointers
-- to it in the same transaction, the way deleting a parent detaches children.
--
-- A nullable column with no default is a catalog-only change, so this does not
-- rewrite the table. Bound lock acquisition so the ALTER fails fast and retries
-- on the next run instead of queueing an ACCESS EXCLUSIVE lock in front of
-- every issue query.
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '10s';
ALTER TABLE issue ADD COLUMN IF NOT EXISTS duplicate_of_issue_id UUID;