"""add ai project control tables Revision ID: 20260421_add_ai_project_control_tables Revises: 20260415_add_semantic_facts Create Date: 2026-04-21 """ from __future__ import annotations from alembic import op import sqlalchemy as sa try: from sqlalchemy.dialects.postgresql import JSONB except Exception: # pragma: no cover JSONB = sa.JSON revision = "20260421_add_ai_project_control_tables" down_revision = "20260415_add_semantic_facts" branch_labels = None depends_on = None def _json_type(bind): return JSONB() if bind.dialect.name == "postgresql" else sa.JSON() def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def _seed_model_catalog(bind) -> None: table = sa.table( "model_catalog_items", sa.column("tenant_id", sa.BigInteger()), sa.column("provider", sa.String()), sa.column("model_key", sa.String()), sa.column("display_name", sa.String()), sa.column("version", sa.String()), sa.column("modalities", _json_type(bind)), sa.column("capability_tags", _json_type(bind)), sa.column("strengths", _json_type(bind)), sa.column("limitations", _json_type(bind)), sa.column("compliance_status", sa.String()), sa.column("filing_status", sa.String()), sa.column("input_cost_per_mtok", sa.Numeric(12, 4)), sa.column("output_cost_per_mtok", sa.Numeric(12, 4)), sa.column("official_docs_url", sa.Text()), sa.column("is_active", sa.Boolean()), ) op.bulk_insert( table, [ { "tenant_id": 1, "provider": "阿里云", "model_key": "qwen-plus", "display_name": "通义千问 Plus", "version": "latest", "modalities": ["text", "image"], "capability_tags": ["general", "rag", "reasoning", "agent", "code"], "strengths": ["中文通用能力强", "RAG 适配稳定", "工具调用成熟"], "limitations": ["不适合视频生成"], "compliance_status": "ready", "filing_status": "filed", "input_cost_per_mtok": 0.8, "output_cost_per_mtok": 2.0, "official_docs_url": "https://help.aliyun.com/zh/model-studio/", "is_active": True, }, { "tenant_id": 1, "provider": "火山引擎", "model_key": "doubao-pro", "display_name": "豆包 Pro", "version": "latest", "modalities": ["text", "image"], "capability_tags": ["general", "rag", "reasoning", "multimodal"], "strengths": ["长文本稳", "中文体验好", "成本适中"], "limitations": ["代码生成弱于专用模型"], "compliance_status": "ready", "filing_status": "filed", "input_cost_per_mtok": 0.8, "output_cost_per_mtok": 2.0, "official_docs_url": "https://www.volcengine.com/product/doubao", "is_active": True, }, { "tenant_id": 1, "provider": "百度智能云", "model_key": "ernie-4.0-turbo", "display_name": "文心一言 4.0 Turbo", "version": "latest", "modalities": ["text", "image"], "capability_tags": ["general", "rag", "reasoning", "multimodal"], "strengths": ["企业落地常见", "多模态均衡"], "limitations": ["综合成本偏高"], "compliance_status": "ready", "filing_status": "filed", "input_cost_per_mtok": 1.2, "output_cost_per_mtok": 3.2, "official_docs_url": "https://cloud.baidu.com/product/wenxinworkshop", "is_active": True, }, { "tenant_id": 1, "provider": "DeepSeek", "model_key": "deepseek-chat", "display_name": "DeepSeek Chat", "version": "latest", "modalities": ["text"], "capability_tags": ["general", "reasoning", "code", "agent"], "strengths": ["代码与推理性价比高", "适合技术团队"], "limitations": ["需要留意合规和交付边界"], "compliance_status": "review_required", "filing_status": "unknown", "input_cost_per_mtok": 0.27, "output_cost_per_mtok": 1.1, "official_docs_url": "https://platform.deepseek.com/", "is_active": True, }, { "tenant_id": 1, "provider": "Moonshot AI", "model_key": "kimi-k2", "display_name": "Kimi K2", "version": "latest", "modalities": ["text"], "capability_tags": ["general", "long_context", "rag", "reasoning"], "strengths": ["长上下文强", "资料理解稳定"], "limitations": ["对多模态场景支持有限"], "compliance_status": "ready", "filing_status": "filed", "input_cost_per_mtok": 1.0, "output_cost_per_mtok": 3.0, "official_docs_url": "https://platform.moonshot.cn/", "is_active": True, }, ], ) def upgrade() -> None: bind = op.get_bind() json_type = _json_type(bind) if not _table_exists(bind, "model_catalog_items"): op.create_table( "model_catalog_items", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("provider", sa.String(length=120), nullable=False), sa.Column("model_key", sa.String(length=120), nullable=False), sa.Column("display_name", sa.String(length=255), nullable=False), sa.Column("version", sa.String(length=120), nullable=False), sa.Column("modalities", json_type, nullable=False), sa.Column("capability_tags", json_type, nullable=False), sa.Column("strengths", json_type, nullable=False), sa.Column("limitations", json_type, nullable=False), sa.Column("compliance_status", sa.String(length=50), nullable=False, server_default="review_required"), sa.Column("filing_status", sa.String(length=50), nullable=False, server_default="unknown"), sa.Column("input_cost_per_mtok", sa.DECIMAL(12, 4), nullable=True), sa.Column("output_cost_per_mtok", sa.DECIMAL(12, 4), nullable=True), sa.Column("official_docs_url", sa.Text(), nullable=True), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.UniqueConstraint( "tenant_id", "provider", "model_key", "version", name="uq_model_catalog_items_tenant_provider_key_version", ), ) op.create_index("ix_model_catalog_items_tenant_id", "model_catalog_items", ["tenant_id"], unique=False) op.create_index("ix_model_catalog_items_provider", "model_catalog_items", ["provider"], unique=False) op.create_index("ix_model_catalog_items_model_key", "model_catalog_items", ["model_key"], unique=False) op.create_index("ix_model_catalog_items_is_active", "model_catalog_items", ["is_active"], unique=False) _seed_model_catalog(bind) table_specs = { "project_model_bindings": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("model_catalog_item_id", sa.BigInteger(), sa.ForeignKey("model_catalog_items.id"), nullable=False), sa.Column("selected_by_user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), sa.Column("selection_reason", json_type, nullable=False), sa.Column("estimated_daily_tokens", sa.Integer(), nullable=False, server_default="0"), sa.Column("estimated_monthly_cost_usd", sa.DECIMAL(12, 2), nullable=True), sa.Column("active_from", sa.TIMESTAMP(), nullable=True), sa.Column("archived_at", sa.TIMESTAMP(), nullable=True), ], "model_recommendation_snapshots": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("recommendation_scope", sa.String(length=32), nullable=False, server_default="charter"), sa.Column("snapshot_input", json_type, nullable=False), sa.Column("recommendations", json_type, nullable=False), sa.Column("top_model_catalog_item_id", sa.BigInteger(), sa.ForeignKey("model_catalog_items.id"), nullable=True), sa.Column("generated_by", sa.String(length=32), nullable=False, server_default="rule_engine"), ], "model_usage_events": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True), sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"), sa.Column("event_key", sa.String(length=255), nullable=True), sa.Column("input_tokens", sa.Integer(), nullable=False, server_default="0"), sa.Column("output_tokens", sa.Integer(), nullable=False, server_default="0"), sa.Column("cost_usd", sa.DECIMAL(12, 4), nullable=False, server_default="0"), sa.Column("event_payload", json_type, nullable=False), sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True), sa.UniqueConstraint("tenant_id", "event_key", name="uq_model_usage_events_tenant_event_key"), ], "model_eval_runs": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"), sa.Column("success_criteria", json_type, nullable=False), sa.Column("test_cases", json_type, nullable=False), sa.Column("summary", json_type, nullable=False), sa.Column("score", sa.DECIMAL(5, 2), nullable=True), ], "prompt_packages": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=False), sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=True), sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True), sa.Column("package_kind", sa.String(length=32), nullable=False, server_default="development"), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), sa.Column("package_text", sa.Text(), nullable=False), sa.Column("package_payload", json_type, nullable=False), sa.Column("generated_from", json_type, nullable=False), ], "dev_workspace_bindings": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=False), sa.Column("workspace_key", sa.String(length=255), nullable=False), sa.Column("repo_root", sa.Text(), nullable=True), sa.Column("editor_type", sa.String(length=32), nullable=False, server_default="vscode"), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), sa.Column("last_seen_at", sa.TIMESTAMP(), nullable=True), sa.UniqueConstraint("tenant_id", "workspace_key", name="uq_dev_workspace_bindings_tenant_workspace_key"), ], "repo_bindings": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("provider", sa.String(length=32), nullable=False, server_default="github"), sa.Column("external_repo_id", sa.String(length=255), nullable=True), sa.Column("owner", sa.String(length=255), nullable=False), sa.Column("repo_name", sa.String(length=255), nullable=False), sa.Column("default_branch", sa.String(length=255), nullable=False, server_default="main"), sa.Column("installation_id", sa.String(length=255), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), sa.Column("webhook_secret_hint", sa.String(length=255), nullable=True), sa.UniqueConstraint("tenant_id", "provider", "owner", "repo_name", name="uq_repo_bindings_tenant_provider_repo"), ], "task_code_links": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=False), sa.Column("repo_binding_id", sa.BigInteger(), sa.ForeignKey("repo_bindings.id"), nullable=False), sa.Column("branch_name", sa.String(length=255), nullable=True), sa.Column("pr_number", sa.Integer(), nullable=True), sa.Column("last_commit_sha", sa.String(length=255), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="linked"), sa.UniqueConstraint( "tenant_id", "task_id", "repo_binding_id", "branch_name", name="uq_task_code_links_tenant_task_repo_branch", ), ], "progress_events": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("task_id", sa.BigInteger(), sa.ForeignKey("tasks.id"), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True), sa.Column("repo_binding_id", sa.BigInteger(), sa.ForeignKey("repo_bindings.id"), nullable=True), sa.Column("workspace_binding_id", sa.BigInteger(), sa.ForeignKey("dev_workspace_bindings.id"), nullable=True), sa.Column("event_type", sa.String(length=32), nullable=False), sa.Column("source", sa.String(length=32), nullable=False, server_default="manual"), sa.Column("derived_task_status", sa.String(length=32), nullable=True), sa.Column("summary", sa.Text(), nullable=True), sa.Column("payload", json_type, nullable=False), sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True), ], "dev_sessions": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=False), sa.Column("workspace_binding_id", sa.BigInteger(), sa.ForeignKey("dev_workspace_bindings.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), sa.Column("started_at", sa.TIMESTAMP(), nullable=True), sa.Column("heartbeat_at", sa.TIMESTAMP(), nullable=True), sa.Column("ended_at", sa.TIMESTAMP(), nullable=True), sa.Column("session_payload", json_type, nullable=False), ], "release_validation_runs": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("release_node_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"), sa.Column("score", sa.DECIMAL(5, 2), nullable=True), sa.Column("key_metrics", json_type, nullable=False), sa.Column("risk_items", json_type, nullable=False), sa.Column("rollback_plan", json_type, nullable=False), sa.Column("validation_summary", json_type, nullable=False), ], "feedback_ingests": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("ingested_by_user_id", sa.BigInteger(), sa.ForeignKey("users.id"), nullable=True), sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"), sa.Column("title", sa.String(length=255), nullable=True), sa.Column("content", sa.Text(), nullable=False), sa.Column("metrics", json_type, nullable=False), sa.Column("samples", json_type, nullable=False), sa.Column("status", sa.String(length=32), nullable=False, server_default="active"), ], "iteration_recommendations": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("feedback_ingest_id", sa.BigInteger(), sa.ForeignKey("feedback_ingests.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="draft"), sa.Column("priority", sa.String(length=32), nullable=False, server_default="medium"), sa.Column("recommendation_summary", json_type, nullable=False), sa.Column("proposed_charter_patch", json_type, nullable=False), sa.Column("proposed_tasks", json_type, nullable=False), ], "revenue_events": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("source_type", sa.String(length=32), nullable=False, server_default="manual"), sa.Column("currency", sa.String(length=16), nullable=False, server_default="USD"), sa.Column("amount_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"), sa.Column("external_ref", sa.String(length=255), nullable=True), sa.Column("event_payload", json_type, nullable=False), sa.Column("occurred_at", sa.TIMESTAMP(), nullable=True), ], "roi_snapshots": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("range_key", sa.String(length=32), nullable=False, server_default="monthly"), sa.Column("window_start", sa.TIMESTAMP(), nullable=True), sa.Column("window_end", sa.TIMESTAMP(), nullable=True), sa.Column("total_revenue_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"), sa.Column("total_cost_usd", sa.DECIMAL(12, 2), nullable=False, server_default="0"), sa.Column("roi_ratio", sa.DECIMAL(12, 4), nullable=True), sa.Column("snapshot_payload", json_type, nullable=False), ], "compliance_check_runs": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("charter_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("release_node_id", sa.BigInteger(), sa.ForeignKey("truth_nodes.id"), nullable=True), sa.Column("project_model_binding_id", sa.BigInteger(), sa.ForeignKey("project_model_bindings.id"), nullable=True), sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"), sa.Column("score", sa.DECIMAL(5, 2), nullable=True), sa.Column("checklist", json_type, nullable=False), sa.Column("issues", json_type, nullable=False), sa.Column("summary", json_type, nullable=False), ], "evidence_packs": [ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.BigInteger(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("project_id", sa.BigInteger(), sa.ForeignKey("projects.id"), nullable=False), sa.Column("compliance_check_run_id", sa.BigInteger(), sa.ForeignKey("compliance_check_runs.id"), nullable=True), sa.Column("package_kind", sa.String(length=32), nullable=False, server_default="compliance_release"), sa.Column("status", sa.String(length=32), nullable=False, server_default="draft"), sa.Column("content", json_type, nullable=False), sa.Column("export_payload", json_type, nullable=False), ], } for table_name, columns in table_specs.items(): if _table_exists(bind, table_name): continue op.create_table(table_name, *columns) op.create_index(f"ix_{table_name}_tenant_id", table_name, ["tenant_id"], unique=False) extra_indexes = { "project_model_bindings": ["project_id", "charter_id", "model_catalog_item_id", "status"], "model_recommendation_snapshots": ["project_id", "charter_id"], "model_usage_events": ["project_id", "project_model_binding_id", "source_type", "occurred_at"], "model_eval_runs": ["project_id", "charter_id", "project_model_binding_id", "status"], "prompt_packages": ["project_id", "charter_id", "task_id", "project_model_binding_id", "status"], "dev_workspace_bindings": ["project_id", "user_id", "status"], "repo_bindings": ["project_id", "provider", "status"], "task_code_links": ["task_id", "repo_binding_id", "pr_number", "status"], "progress_events": ["task_id", "project_id", "user_id", "repo_binding_id", "workspace_binding_id", "event_type", "source", "occurred_at"], "dev_sessions": ["project_id", "user_id", "workspace_binding_id", "status"], "release_validation_runs": ["project_id", "charter_id", "release_node_id", "project_model_binding_id", "status"], "feedback_ingests": ["project_id", "charter_id", "ingested_by_user_id", "source_type", "status"], "iteration_recommendations": ["project_id", "charter_id", "feedback_ingest_id", "status", "priority"], "revenue_events": ["project_id", "source_type", "external_ref", "occurred_at"], "roi_snapshots": ["project_id", "range_key"], "compliance_check_runs": ["project_id", "charter_id", "release_node_id", "project_model_binding_id", "status"], "evidence_packs": ["project_id", "compliance_check_run_id", "package_kind", "status"], } for table_name, columns in extra_indexes.items(): for column_name in columns: op.create_index(f"ix_{table_name}_{column_name}", table_name, [column_name], unique=False) def downgrade() -> None: bind = op.get_bind() tables = [ "evidence_packs", "compliance_check_runs", "roi_snapshots", "revenue_events", "iteration_recommendations", "feedback_ingests", "release_validation_runs", "dev_sessions", "progress_events", "task_code_links", "repo_bindings", "dev_workspace_bindings", "prompt_packages", "model_eval_runs", "model_usage_events", "model_recommendation_snapshots", "project_model_bindings", "model_catalog_items", ] for table_name in tables: if not _table_exists(bind, table_name): continue for index in sa.inspect(bind).get_indexes(table_name): op.drop_index(index["name"], table_name=table_name) op.drop_table(table_name)