---
id: 025
title: DB migration 测试规范 + 覆盖当前所有迁移
status: in_progress
labels: [tech-debt, db, testing, compliance]
assignee: team-lead
created_at: 2026-04-23
---

# #025 DB migration 测试规范

## 背景

2026-04-23 老板发现：`src/xiangqin/db.py` 里有 `_migrate_profile_columns` / `_drop_exposure_tables` 这类迁移函数，当前**完全没测试覆盖**（grep test/ 无匹配）。风险：未来改 migration 破坏老库数据无感知。

## 业界最佳实践（4 种模式）

| 模式 | 做什么 | 适合 |
|---|---|---|
| **1. Schema from scratch** | 空 db → 跑 init_db → assert 所有表/列/索引都在 | **必做**（MVP） |
| **2. Idempotent**（幂等）| 同一 migration 跑 2 次不报错不变更状态 | **必做** |
| **3. Historical snapshot** | 保存每版本 schema 快照 → 模拟老库 → 跑 migration → assert 新列 OK + 老数据不丢 | 推荐（关键 migration） |
| **4. Round-trip** | 老库 → migrate → 读数据应等于老库数据 | 有 DROP / 改列时必做 |

大项目会用 `alembic` / `Django migrations` 自带测试工具。我们 SQLite + 纯 Python 手写 migration 够轻，pytest fixtures 实现这 4 种。

## 本 issue 做什么

### 阶段 1（MVP，立即做）

新建 `test/test_db.py`：
- `test_init_db_empty` 空 db init 所有表都有
- `test_init_db_idempotent` init 2 次 OK
- `test_migrate_profile_columns_idempotent` 迁移函数跑 2 次 OK
- `test_drop_exposure_tables_idempotent` DROP 不存在的表 OK
- `test_user_photos_schema` FEAT-008 新表字段齐
- `test_avatar_photo_id_column_added` profiles 新列 OK

### 阶段 2（Historical snapshot）

在 `test/fixtures/db-snapshots/` 存各历史版本的 .sql：
- `0.3.0-pre-m4.sql`（只有 users / profiles 4 列 / exposure_orders）
- `0.4.0-m4.sql`（加了信封 + M4 profile 字段，还有 exposure 表）
- `0.4.5-current.sql`（dump 当前）

每个阶段 1 个测试：`test_upgrade_from_<version>_preserves_data`。

### 规范（写到 sop 或 brain/features/db/）

**每次改 db schema / migration 必须：**
1. 同 PR 加对应 test（4 种模式里相关的）
2. CHANGELOG 明标向前/向后兼容
3. 通过 release.yml 的 test gate

## 何时执行

- **CI 每次 push / PR**：run `pytest test/test_db.py -q`（已在 ci.yml 覆盖）
- **真发布前**：通过 release.yml test gate（已有）
- **M6 full drill**：用真 OSS backup + restore 到新机验证（已有）

## 相关

- issue `#004` DB schema + migrations 独立目录（长期 refactor，本 issue 是它的先决条件）
- M3 restore drill（已有但只测 integrity 不测 migration）
- 新 FEAT-008 avatar 的 migration 触发的 `avatar_photo_id` 列加入
