# 单 PR 多 schema 改动 → 合并迁移为一个文件

**Date**: 2026-05-16
**Surfaced in**: slot-5 session 7 end-of-day（FFAI Agent Phase 1 单 PR 大迭代）

## 现象

CLAUDE.md「每次提交最多包含一个迁移文件：一个功能涉及多个 schema 变更时，合并为一个迁移文件；禁止一个提交中包含多个迁移文件」是硬规则，但 AI 一上来按 PR 节奏每个 PR 一个迁移文件，连推 7 个：

```
20260516085046_add_agent_session_and_message
20260516102805_add_model_routing
20260516103900_add_agent_trajectory_event
20260516104500_add_agent_quota
20260516110000_add_agent_plan_permission_mode
20260516125000_add_agent_artifact
20260516131500_add_pr8_pr10_5
```

用户拍板"不拆 PR"（一次大 PR 合）就 **直接违反单文件规则**——必须合并。

## 为什么会发生

每个 PR 推进时：
1. `prisma db push --skip-generate`（slot DB 同步 schema）+ `prisma generate`（更新 client）
2. 然后**手工**写一个 `migration.sql` 文件，时间戳 + 描述性名字

每个 PR 独立一个迁移文件，逻辑上跟 stacked PR 模式对齐。但被合成一个大 PR 时变成违规。

## 合并流程（已验证）

### 1. 列出本 session 加的迁移

```bash
ls backend/prisma/migrations/ | grep "20260516" | sort
```

### 2. 检查依赖（哪些 ALTER 引用前面的表）

```bash
grep -l "ALTER TABLE.*<existing_table>\|REFERENCES.*<existing_schema>" \
  backend/prisma/migrations/20260516*/migration.sql
```

关键依赖类型：
- `ALTER TABLE x ADD COLUMN` → x 必须先存在（同 session 创建的话依赖前一个迁移）
- `FOREIGN KEY REFERENCES x` → 同上
- enum 类型 → CREATE TYPE 必须在使用前

### 3. 按时间戳序 concat 到一个文件

```bash
cd backend/prisma/migrations
{
  echo "-- <模块名> 合并迁移 —— 说明"
  for d in $(ls -d 20260516*_*  | sort); do
    name=${d#*_}  # 截取 timestamp 后的描述
    echo ""
    echo "-- ==================== $name ===================="
    cat $d/migration.sql
  done
} > /tmp/merged.sql
```

### 4. 删除旧的，建新的

```bash
# 保留最早的时间戳目录名（"项目起点"），换内容
rm -rf 20260516085046_*  # 删旧的（即便是第一个）
rm -rf 20260516{102805,103900,104500,110000,125000,131500}_*
mkdir 20260516085046_<merged_module_name>
cp /tmp/merged.sql 20260516085046_<merged_module_name>/migration.sql
```

### 5. **关键验证**：干净 PG 重放

```bash
# 在 PG 容器里新建临时库，按 schema namespace 重建空 schema，重放迁移
docker exec <pg-container> psql -U <user> -d postgres \
  -c "CREATE DATABASE migration_test;"
docker exec <pg-container> psql -U <user> -d migration_test \
  -c "CREATE SCHEMA <ns>;"
docker cp ./migration.sql <pg-container>:/tmp/migration.sql
docker exec <pg-container> psql -U <user> -d migration_test \
  -f /tmp/migration.sql

# 验证所有表建出（数量对得上）
docker exec <pg-container> psql -U <user> -d migration_test \
  -c "\dt <ns>.*" | grep -c "table"

# 清理
docker exec <pg-container> psql -U <user> -d postgres \
  -c "DROP DATABASE migration_test;"
```

如果重放失败 → 通常是 ALTER TABLE 引用了还没创建的列、或 FK references 不存在的表 → 调整 concat 顺序。

## 决策树：什么时候要合并

| 情况 | 决定 |
|---|---|
| 一次提交一个迁移（stacked PR 节奏）| 不合，保持单文件 |
| 一次大 PR 含多次 schema 改动（用户授权"不拆"）| **必须合**，违反 CLAUDE.md |
| 已经 push 到远端的迁移 | **不要回头合**——下游 / 部署机已 applied，合并破坏 history。只能给新增 schema 加新迁移 |
| 单 dev 在本地连续做多 PR 但还没 push | 可合可不合；推 PR 前合 |

## 反例：不要做的

- **不要** 在已 push / 已被其他人 pull 过的迁移上动手脚（会让别人本机 `prisma migrate dev` 报 history mismatch）
- **不要** 用 `prisma migrate diff --from-empty --to-schema-datamodel`——它会**包含 baseline_squash 已经建的所有表**，产出几千行；要 `--from-migrations` 但需 shadow DB，复杂度高
- **不要** 跨 ns / 跨模块大杂烩——一个 module 的多个 PR 合一起 OK，但不要把"agent 模块" + "approval 模块"也合到一起

## 相关
- `CLAUDE.md` § 数据库（"每次提交最多包含一个迁移文件"）
- `docs/standards/04-database-architecture.md`
- 本次产出：`backend/prisma/migrations/20260516085046_add_ffai_agent_module/migration.sql`（335 行，13 张表）
