104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
"""Phase 7S.3 email-to-engagement mapping
|
|
|
|
Revision ID: 20260612_phase_7s3_email_mapping
|
|
Revises: 20260611_phase_7s2_imap_incoming
|
|
Create Date: 2026-06-12
|
|
|
|
SQLite-safe revision:
|
|
- Adds reference id columns as plain Integer columns instead of adding
|
|
ForeignKey constraints through ALTER TABLE, because SQLite cannot ALTER
|
|
constraints without batch copy/move migration.
|
|
- SQLAlchemy model relationships can still use these id columns for lookups.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
revision = "20260612_phase_7s3_email_mapping"
|
|
down_revision = "20260611_phase_7s2_imap_incoming"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _inspector():
|
|
return sa.inspect(op.get_bind())
|
|
|
|
|
|
def _has_table(table_name: str) -> bool:
|
|
return table_name in _inspector().get_table_names()
|
|
|
|
|
|
def _has_column(table_name: str, column_name: str) -> bool:
|
|
if not _has_table(table_name):
|
|
return False
|
|
return column_name in {col["name"] for col in _inspector().get_columns(table_name)}
|
|
|
|
|
|
def _create_index_if_missing(table_name: str, index_name: str, columns: list[str]) -> None:
|
|
if not _has_table(table_name):
|
|
return
|
|
existing = {idx["name"] for idx in _inspector().get_indexes(table_name)}
|
|
if index_name not in existing:
|
|
op.create_index(index_name, table_name, columns)
|
|
|
|
|
|
def _add_column_if_missing(table_name: str, column: sa.Column) -> None:
|
|
if _has_table(table_name) and not _has_column(table_name, column.name):
|
|
op.add_column(table_name, column)
|
|
|
|
|
|
def upgrade() -> None:
|
|
if not _has_table("email_incoming_messages"):
|
|
return
|
|
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("tracking_code", sa.String(length=80), nullable=True),
|
|
)
|
|
|
|
# SQLite does not support ALTER TABLE ADD CONSTRAINT for ForeignKey.
|
|
# Keep these as nullable integer reference columns. Application code and
|
|
# SQLAlchemy models can still resolve the linked records safely.
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("matched_engagement_id", sa.Integer(), nullable=True),
|
|
)
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("matched_task_id", sa.Integer(), nullable=True),
|
|
)
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("matched_invoice_id", sa.Integer(), nullable=True),
|
|
)
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("mapping_status", sa.String(length=30), nullable=False, server_default="UNMAPPED"),
|
|
)
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("mapping_notes", sa.Text(), nullable=True),
|
|
)
|
|
_add_column_if_missing(
|
|
"email_incoming_messages",
|
|
sa.Column("mapped_at_utc", sa.DateTime(timezone=True), nullable=True),
|
|
)
|
|
|
|
for name, cols in {
|
|
"ix_email_incoming_messages_tracking_code": ["tracking_code"],
|
|
"ix_email_incoming_messages_matched_engagement_id": ["matched_engagement_id"],
|
|
"ix_email_incoming_messages_matched_task_id": ["matched_task_id"],
|
|
"ix_email_incoming_messages_matched_invoice_id": ["matched_invoice_id"],
|
|
"ix_email_incoming_messages_mapping_status": ["mapping_status"],
|
|
"ix_email_incoming_messages_mapped_at_utc": ["mapped_at_utc"],
|
|
}.items():
|
|
_create_index_if_missing("email_incoming_messages", name, cols)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Keep defensive because SQLite cannot reliably drop columns/constraints
|
|
# in all local development environments without a batch table rewrite.
|
|
pass
|