---
date: 2026-05-18
type: error
tags: [prisma, migrate, schema-drift, agent-pool, generated-column]
---

# `prisma migrate dev` 在 schema drift 状态下把无关漂移打包进 feature migration

## 现象

在 slot 上做 feature 改动（动 schema 加新字段），跑 `prisma migrate dev --name feat_xxx`，生成的 `migration.sql` 包含本次 feature 完全没碰的表的 ALTER：

```sql
-- feature 改动（本次想做的）
CREATE TYPE "MemoryOwnerScope" AS ENUM (...);
ALTER TABLE agent_memories ADD COLUMN "ownerScope" ...;

-- 莫名其妙混进来的（不是本次改动）
ALTER TABLE "platform_ai_usage"."ai_usage_events" ALTER COLUMN "total_tokens" DROP DEFAULT;
ALTER TABLE "platform_master"."geo_regions" ALTER COLUMN "countries" DROP DEFAULT;
-- 还有 5 条 RenameIndex...
```

更糟：apply 时报错 `column "total_tokens" of relation "ai_usage_events" is a generated column / use ALTER COLUMN ... DROP EXPRESSION`，整个 migration 失败回滚，feature 全没落到 DB，`_prisma_migrations` 表里留下 `applied_steps_count=0 / finished_at=NULL` 的失败行。

## 根因

dev DB 的实际 schema 跟 `prisma/migrations/` 累积到 HEAD 算出来的"理论 schema"**有漂移**。漂移来源（slot 场景下最常见两条）：

1. 前一次用过 `prisma db push --accept-data-loss`（[ERR-20260518-001](ERR-20260518-001-slot-reclaim-drift.md) 的清场步骤）—— db push **不写 migration history**，直推 SQL 到 DB。migration table 还停在合法状态，但 DB 实际 schema 已超前。
2. 上次占用 slot 的 feature 分支的 schema 改动（来源跟本分支不同），release 时只清 git 状态不重置 DB。

`prisma migrate dev` 的 diff 算法是：
```
target_schema = read_schema_files()
actual_schema = introspect_dev_db()
desired_schema = baseline_apply_all_migrations()  # 用 shadow DB
new_migration = diff(actual_schema, target_schema)
                  ▲ 不是 diff(desired, target)！
```

所以 actual ≠ desired 时，`diff(actual, target)` 把"actual 怎么变成 target"的所有 SQL 都吐出来——既包括你的 feature 改动，也包括 actual 偏离 desired 的那些"漂移修正"。`ai_usage_events.total_tokens DROP DEFAULT` 就是历史上某个 db push 把 generated column 的 default 表达式偷偷改了，prisma 想"修复"。

## 解法

**两条路，选哪条看 dev DB 里有没有想保留的数据**：

### 路 A — 干净 dev DB（推荐 slot 场景）

```bash
# 1. reset dev DB 到 migration baseline（清数据 + 重跑所有 migrations）
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION="<user's consent text>" \
  npx dotenv -e .env -- prisma migrate reset --force --skip-seed

# 2. 再跑 migrate dev——此时 actual == desired，diff 干净
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION="..." \
  npx dotenv -e .env -- prisma migrate dev --name feat_xxx

# 3. 重新 seed + 业务初始化
npm run db:seed && npm run init:itadmin
```

### 路 B — 必须保留 dev DB 数据：手工清理 migration SQL

如果数据宝贵（不太可能在 slot 场景），先按路 A 那样生成 migration 拿到 SQL 模板，然后：

```bash
# 1. 删 _prisma_migrations 里的失败行
docker exec ... psql ... -c \
  "DELETE FROM _prisma_migrations WHERE migration_name='<failed-name>';"

# 2. 手工编辑 migration.sql，只保留 feature 相关 SQL，删掉所有"漂移修正"
# 3. 用 migrate deploy（非交互、不再走 diff，直接执行 SQL）apply
npx dotenv -e .env -- prisma migrate deploy
```

⚠️ 警告：路 B 后续 `migrate dev` 仍会再次提示漂移（DB 里那些 db-push 引入的偏差还在）。要彻底修，最终还是要走路 A。

## 工程化保险

- **slot agent-pool 应在 claim 时 force-reset DB + 重 seed**——避免每个新 feature 跑 `migrate dev` 都踩 [ERR-20260518-001](ERR-20260518-001-slot-reclaim-drift.md) + 本条。已在那条提了候选方案（`agent-claim.sh` 自动 reset）。
- **CI 测试库**：`scripts/lib-test-db.sh::reset_test_db_schema` 已经走 `prisma migrate deploy`（不是 db push），不踩这坑。**任何 dev / test / staging 环境的 schema 初始化都应该用 `migrate deploy`，不要用 `db push`**。
- **生产 absolute 禁 db push**（CLAUDE.md 已明令）。

## 教训

1. **`prisma db push` 和 `prisma migrate dev` 在同一个 DB 上混用 = 漂移 + 后续 migration 污染** —— 选一条路走到底。
2. **失败的 migration 在 `_prisma_migrations` 留 ghost 行**（applied_steps_count=0, finished_at=NULL）。下次 `migrate deploy` / `migrate dev` 会再 retry 同一份 SQL，直到 DELETE 掉那行。
3. **generated column 不能 `DROP DEFAULT`** —— 必须 `DROP EXPRESSION`。这是 Postgres 限制，prisma 没区分。如果 schema 里把 generated column 改成普通列，要手工写 SQL。
4. **migrate dev 的 diff 基准是 dev DB 实际 schema，不是 migrations 累积态** —— 这是默认行为不是 bug，但配合 db push 就成陷阱。
5. **AI 自动化下要 set `PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION` 环境变量** —— Prisma 6+ 对 AI agent 加了 reset/destructive 安全 gate，需要用户明示同意后把同意文本作为 env var 传。

## 关联

- [ERR-20260518-001](ERR-20260518-001-slot-reclaim-drift.md)：slot 复用清场触发的初始漂移
- [ERR-20260509-004](ERR-20260509-004.md)：db push 模式 + 缺扩展（同一 db push vs migrate deploy 主题）
- CLAUDE.md「数据库」段：禁修已应用迁移 + 每次提交最多一个迁移文件
