"""M3 知识域:knowledge_doc + 扩宽 item.status(加知识四态)。 三个需要注意的点: 1. **item.status 的 CHECK 再扩一次**。0003 里是"任务 ∪ 工单",知识的 `draft/in_review/published/archived` 会被直接拦下。这里改为三者并集; "哪种 kind 只能用哪组状态"由 `app/domain/*.py` 的状态机保证,不靠数据库 (CHECK 不能跨行判断 kind 与 status 的组合)。 2. **知识的来源写在数据库里**。`source_kind <> 'manual'` 时 `source_item_id` 必填。 这条 CHECK 是 D3 的物理落点:LLM 自动归档出来的知识若没有来源,用户无法核对, 等于往库里塞断言。 3. **downgrade 会删掉所有知识**(kind='knowledge' 的 item 及其扩展),原因与 0003 相同: 旧约束容纳不下知识状态,不回删就回不到 0003。 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = "0004" down_revision: str | None = "0003" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None APP_ROLE = "dadao_app" TENANT_TABLES_0004: tuple[str, ...] = ("knowledge_doc",) TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled") TICKET_STATUSES = ("open", "accepted", "in_progress", "pending_verify", "done", "canceled") KNOWLEDGE_STATUSES = ("draft", "in_review", "published", "archived") def upgrade() -> None: # 1) item.status 放宽到 任务 ∪ 工单 ∪ 知识 op.execute("ALTER TABLE item DROP CONSTRAINT IF EXISTS ck_item_status") statuses = ",".join( f"'{s}'" for s in (*TASK_STATUSES, *TICKET_STATUSES, *KNOWLEDGE_STATUSES) ) op.execute(f"ALTER TABLE item ADD CONSTRAINT ck_item_status CHECK (status IN ({statuses}))") # 2) knowledge_doc op.create_table( "knowledge_doc", sa.Column( "item_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("item.id", ondelete="CASCADE"), primary_key=True, ), sa.Column( "workspace_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "author_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("app_user.id", ondelete="RESTRICT"), nullable=False, ), sa.Column( "source_item_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("item.id", ondelete="SET NULL"), nullable=True, ), sa.Column("source_kind", sa.String(16), nullable=False, server_default="manual"), sa.Column("doc_type", sa.String(16), nullable=False, server_default="note"), sa.Column("status_hint", sa.String(16), nullable=False, server_default="draft"), sa.Column("summary", sa.Text(), nullable=False, server_default=""), sa.Column("generated_by", sa.String(16), nullable=False, server_default="human"), sa.Column("published_at", sa.DateTime(timezone=True), nullable=True), sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True), sa.Column("chunk_count", sa.Integer(), nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False ), sa.Column("version", sa.Integer(), nullable=False, server_default="1"), sa.CheckConstraint( "source_kind = 'manual' OR source_item_id IS NOT NULL", name="ck_knowledge_source_required", ), sa.CheckConstraint("chunk_count >= 0", name="ck_knowledge_chunk_count"), ) op.create_index("ix_knowledge_ws_status", "knowledge_doc", ["workspace_id", "status_hint"]) op.create_index("ix_knowledge_ws_source", "knowledge_doc", ["workspace_id", "source_item_id"]) # 3) RLS + 授权 op.execute("ALTER TABLE knowledge_doc ENABLE ROW LEVEL SECURITY") op.execute( "CREATE POLICY knowledge_doc_isolation ON knowledge_doc " "USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())" ) op.execute( f"GRANT SELECT, INSERT, UPDATE, DELETE ON {', '.join(TENANT_TABLES_0004)} TO {APP_ROLE}" ) def downgrade() -> None: # 旧约束容纳不下知识状态:先清掉知识 item,才能回到 0003 的约束 op.execute("DELETE FROM item WHERE kind = 'knowledge'") op.execute("DROP POLICY IF EXISTS knowledge_doc_isolation ON knowledge_doc") op.drop_index("ix_knowledge_ws_source", table_name="knowledge_doc") op.drop_index("ix_knowledge_ws_status", table_name="knowledge_doc") op.drop_table("knowledge_doc") op.execute("ALTER TABLE item DROP CONSTRAINT IF EXISTS ck_item_status") statuses = ",".join(f"'{s}'" for s in (*TASK_STATUSES, *TICKET_STATUSES)) op.execute(f"ALTER TABLE item ADD CONSTRAINT ck_item_status CHECK (status IN ({statuses}))")