"""issues + systems: task_kind=issue, systems, record_systems, arose_from_id Revision ID: 0065 Revises: 0064 Create Date: 2026-06-14 Adds the corrective-work 'issue' task_kind (same-change CHECK expand per the 'new CHECK-enum values need a same-change migration' rule), a per-project self-describing System entity, a many-to-many record<->system join (any note/task/issue), and an issue->originating-task provenance FK. """ from alembic import op import sqlalchemy as sa revision = "0065" down_revision = "0064" branch_labels = None depends_on = None def upgrade() -> None: # 1. task_kind gains 'issue' (corrective work). DROP+ADD the CHECK in the # same change that introduces the value. op.drop_constraint("notes_task_kind_check", "notes", type_="check") op.create_check_constraint( "notes_task_kind_check", "notes", "task_kind IN ('work','plan','issue')", ) # 2. Provenance: an issue can point back at the task/feature it arose from. # Distinct from parent_id (sub-task hierarchy). op.add_column("notes", sa.Column("arose_from_id", sa.Integer(), nullable=True)) op.create_foreign_key( "fk_notes_arose_from_id", "notes", "notes", ["arose_from_id"], ["id"], ondelete="SET NULL", ) op.create_index("ix_notes_arose_from_id", "notes", ["arose_from_id"]) # 3. systems: per-project, reusable, self-describing subsystem/area. op.create_table( "systems", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "project_id", sa.Integer(), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, ), sa.Column("name", sa.Text(), nullable=False, server_default=""), sa.Column("description", sa.Text(), nullable=True), sa.Column("color", sa.Text(), nullable=True), sa.Column("status", sa.Text(), nullable=False, server_default="active"), sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.Column("deleted_batch_id", sa.Text(), nullable=True), ) op.create_index("ix_systems_project_id", "systems", ["project_id"]) op.create_check_constraint( "systems_status_check", "systems", "status IN ('active','archived')", ) # 4. record_systems: M2M join — any note/task/issue <-> system. op.create_table( "record_systems", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "note_id", sa.Integer(), sa.ForeignKey("notes.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "system_id", sa.Integer(), sa.ForeignKey("systems.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.UniqueConstraint("note_id", "system_id", name="uq_record_systems_note_system"), ) op.create_index("ix_record_systems_note_id", "record_systems", ["note_id"]) op.create_index("ix_record_systems_system_id", "record_systems", ["system_id"]) def downgrade() -> None: op.drop_table("record_systems") op.drop_table("systems") op.drop_index("ix_notes_arose_from_id", table_name="notes") op.drop_constraint("fk_notes_arose_from_id", "notes", type_="foreignkey") op.drop_column("notes", "arose_from_id") op.drop_constraint("notes_task_kind_check", "notes", type_="check") op.create_check_constraint( "notes_task_kind_check", "notes", "task_kind IN ('work','plan')", )