gabriel / musehub public
0076_add_unique_constraint_musehub_issues_number.py python
48 lines 1.8 KB
Raw
sha256:4c406127fd42cddb7bd2282519704e86a5ed7129fe2682617e23821e2bddb259 fix: publish_muse_release.sh failed against production on t… Sonnet 5 minor ⚠ breaking 4 days ago
1 """Add UNIQUE constraint on musehub_issues (repo_id, number)
2
3 musehub#184 (critical data-integrity bug): _next_issue_number() computes
4 the next per-repo issue number via SELECT MAX(number) then INSERT max+1,
5 with no locking and no unique constraint. Reproduced directly: firing 10
6 concurrent `muse hub issue create` calls against the same repo produced
7 genuine duplicate numbers (two rows at #197, three rows at #199) -- all
8 rows persisted (nothing was silently lost), but the per-repo number is
9 no longer a reliable unique identifier once two creates race.
10
11 This migration adds the missing UNIQUE constraint so any future race
12 fails loudly (IntegrityError) instead of silently producing a duplicate.
13 The application-level fix (row-locking musehub_repos to serialize number
14 allocation, so races don't even reach this constraint under normal load)
15 lives in musehub/services/musehub_issues.py, committed alongside this
16 migration.
17
18 Prerequisite: verified staging currently has zero duplicate (repo_id,
19 number) pairs before this migration runs (the 12 duplicate test rows
20 created while reproducing this bug were deleted first) -- this migration
21 will fail loudly on apply if any repo still has a duplicate, which is the
22 correct, safe behavior rather than silently dropping rows.
23
24 Revision ID: 0076
25 Revises: 0075
26 """
27 from __future__ import annotations
28
29 from typing import Sequence, Union
30
31 from alembic import op
32
33
34 # revision identifiers, used by Alembic.
35 revision: str = '0076'
36 down_revision: Union[str, None] = '0075'
37 branch_labels: Union[str, Sequence[str], None] = None
38 depends_on: Union[str, Sequence[str], None] = None
39
40
41 def upgrade() -> None:
42 op.create_unique_constraint(
43 'uq_musehub_issues_repo_id_number', 'musehub_issues', ['repo_id', 'number'],
44 )
45
46
47 def downgrade() -> None:
48 op.drop_constraint('uq_musehub_issues_repo_id_number', 'musehub_issues', type_='unique')
File History 1 commit
sha256:4c406127fd42cddb7bd2282519704e86a5ed7129fe2682617e23821e2bddb259 fix: publish_muse_release.sh failed against production on t… Sonnet 5 minor 4 days ago