73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""phase54 — link related and duplicate issues
|
|
|
|
Creates `issue_links`: one row per connection between two issues, displayed on
|
|
both of them. See the IssueLink docstring in app/models/issue.py for why the
|
|
direction is stored once rather than mirrored.
|
|
|
|
Purely additive. Nothing reads the table until a person creates a link, so an
|
|
empty table is exactly today's behaviour and there is nothing to backfill.
|
|
|
|
Both FKs use ON DELETE CASCADE so deleting an issue takes its links with it from
|
|
either end — a surviving link pointing at a deleted issue would render a dead
|
|
row on the other issue's page. The ORM cascade on Issue.links_from/links_to
|
|
covers the application path; this covers a direct SQL delete.
|
|
|
|
Table-existence check — safe to re-run.
|
|
|
|
Revision ID: phase54_issue_links
|
|
Revises: phase53_followup_assignee
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
# Revision ids must be <= 32 chars — alembic_version.version_num is VARCHAR(32).
|
|
revision = 'phase54_issue_links'
|
|
down_revision = 'phase53_followup_assignee'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _table_exists(conn, name):
|
|
return conn.execute(sa.text("""
|
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t
|
|
"""), {'t': name}).scalar() > 0
|
|
|
|
|
|
def upgrade():
|
|
conn = op.get_bind()
|
|
if _table_exists(conn, 'issue_links'):
|
|
return
|
|
|
|
op.execute(sa.text("""
|
|
CREATE TABLE issue_links (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
issue_id INT NOT NULL,
|
|
linked_issue_id INT NOT NULL,
|
|
link_type ENUM('duplicate','related') NOT NULL DEFAULT 'related',
|
|
created_by INT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
|
|
CONSTRAINT uq_issue_link UNIQUE (issue_id, linked_issue_id),
|
|
|
|
-- Names match what SQLAlchemy's index=True generates, so the
|
|
-- schema this migration builds and the one db.create_all() builds
|
|
-- are identical down to the index names.
|
|
INDEX ix_issue_links_issue_id (issue_id),
|
|
INDEX ix_issue_links_linked_issue_id (linked_issue_id),
|
|
|
|
CONSTRAINT fk_issue_links_issue
|
|
FOREIGN KEY (issue_id) REFERENCES issues (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_issue_links_linked
|
|
FOREIGN KEY (linked_issue_id) REFERENCES issues (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_issue_links_creator
|
|
FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
"""))
|
|
|
|
|
|
def downgrade():
|
|
conn = op.get_bind()
|
|
if _table_exists(conn, 'issue_links'):
|
|
op.execute(sa.text('DROP TABLE issue_links'))
|