-- 门诊经理批次分配 —— 建 plan_assignments + followup_plans 六列归因
--
-- 【为什么这一份可以用普通 DDL,不必 CONCURRENTLY】
--   20260728020000 那次踩过:Prisma 把整份 migration.sql 用**一次 simple query** 发给 PG,
--   PG 对「一个查询串里多条语句」隐式开事务块,而 CONCURRENTLY 禁止在事务块内跑 →
--   ERROR 25001,且 _prisma_migrations 留一条卡死记录(P3018)**堵死整条迁移流水线**。
--   分界线是**语句条数,不是 Prisma 版本**。
--   本次不需要它:唯一要在存量表上建的索引是 followup_plans(assignment_id),
--   该表生产约 25 万行(秒级),不是 persona_features(7.66M / 5.4GB)也不是
--   patient_transactions(12–24M)。patient_facts 1300 万行本次**一个字都不动**。
--
-- 【为什么加六列不会重写 25 万行】
--   六列**全部可空、全部无默认值** → PG 11+ 纯元数据操作(不 rewrite、不 full scan)。
--   一旦给任何一列加 DEFAULT 或 NOT NULL,这个前提当场失效。
--   ⚠️ 存量**不回填**:NULL 即语义正确(= 上线前的自助认领单)。
--   25 万行 UPDATE 会真正重写表 + 膨胀 + 触发 autovacuum,收益为零。
--
-- 【锁窗口】⚠️ 整份文件跑在**一个隐式事务**里 → 下面 ALTER 拿到的 ACCESS EXCLUSIVE
--   会一直持有到 COMMIT,真实阻塞窗口 ≈ 整份迁移时长(本地实测 1–3 秒)。
--   **后人别往这份文件里追加慢语句**(回填、大表建索引),那会把阻塞窗口线性放大。
--   首句 lock_timeout 是为了 R3:PG 锁队列近似 FIFO,一个**等待中**的 ACCESS EXCLUSIVE
--   会挡住排在它后面的所有 SELECT —— 部署时若正好有慢查询压在 followup_plans 上,
--   召回工作台会在 ALTER 真正拿到锁**之前**就整体停摆。设 10s 是把
--   「全站排队几分钟」换成「迁移快速失败、重试即可」。
--
-- 【失败后怎么办】整份 SQL 在一个事务里,**不可能出现「表建了列没加」的半吊子 schema**。
--   npx prisma migrate resolve --rolled-back 20260802090000_add_plan_assignments
--   然后避开 DW 08:00(沪) 落库后的增量 cron 窗口重试。

SET LOCAL lock_timeout = '10s';

-- ── plan_assignments ─────────────────────────────────────────
-- ⚠️ id 不给 DB 默认值:本仓 uuid 一律由 Prisma 客户端生成(@default(uuid())),
--    与 plan_event_logs 等既有表一致。写 DEFAULT gen_random_uuid() 会造成永久 schema drift。
CREATE TABLE "plan_assignments" (
    "id"         UUID         NOT NULL,
    "host_id"    UUID         NOT NULL,
    "tenant_id"  TEXT         NOT NULL,
    "clinic_id"  TEXT,
    "created_by" TEXT         NOT NULL,
    "request_id" TEXT         NOT NULL,
    "criteria"   JSONB        NOT NULL,
    "attributes" JSONB,
    "expires_at" TIMESTAMPTZ(3) NOT NULL,
    "status"     TEXT         NOT NULL DEFAULT 'confirmed',
    "revoked_at" TIMESTAMPTZ(3),
    "revoked_by" TEXT,
    "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMPTZ(3) NOT NULL,

    CONSTRAINT "plan_assignments_pkey" PRIMARY KEY ("id")
);

-- 幂等键:同一 requestId 重复提交回查已有批次,不产生第二个批次
CREATE UNIQUE INDEX "plan_assignments_host_id_tenant_id_request_id_key"
    ON "plan_assignments"("host_id", "tenant_id", "request_id");

-- 主管看「我分的那些批」
CREATE INDEX "plan_assignments_host_id_tenant_id_clinic_id_created_at_idx"
    ON "plan_assignments"("host_id", "tenant_id", "clinic_id", "created_at");
CREATE INDEX "plan_assignments_created_by_created_at_idx"
    ON "plan_assignments"("created_by", "created_at");

ALTER TABLE "plan_assignments"
    ADD CONSTRAINT "plan_assignments_host_id_fkey"
    FOREIGN KEY ("host_id") REFERENCES "hosts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- ── followup_plans 六列归因 ──────────────────────────────────
-- ⭐ **一条 ALTER 带六个 ADD COLUMN**(一次拿锁,不是六次抢锁)。
--   拆成六条 ALTER 会让上面说的锁队列风险乘以六。
ALTER TABLE "followup_plans"
    ADD COLUMN "assignment_id"         UUID,
    ADD COLUMN "assignment_expires_at" TIMESTAMPTZ(3),
    ADD COLUMN "assigned_by"           TEXT,
    ADD COLUMN "release_reason"        TEXT,
    ADD COLUMN "release_note"          TEXT,
    ADD COLUMN "assign_strategy"       TEXT;

CREATE INDEX "followup_plans_assignment_id_idx" ON "followup_plans"("assignment_id");

-- ⭐ ON DELETE RESTRICT 是**刻意的**:批次是审计对象,删批次会让 N 行归因永久失真。
--   NOT VALID + 单独 VALIDATE:后者只拿 SHARE UPDATE EXCLUSIVE(不挡读写),
--   避免建约束时对 25 万行做一次带 ACCESS EXCLUSIVE 的全表校验。
--   存量全是 NULL,校验必然通过。
ALTER TABLE "followup_plans"
    ADD CONSTRAINT "followup_plans_assignment_id_fkey"
    FOREIGN KEY ("assignment_id") REFERENCES "plan_assignments"("id")
    ON DELETE RESTRICT ON UPDATE CASCADE
    NOT VALID;

ALTER TABLE "followup_plans" VALIDATE CONSTRAINT "followup_plans_assignment_id_fkey";
