---
id: 004
title: DB schema + migrations 独立目录管理
status: open
labels: [tech-debt, db, infra]
assignee: team-lead
created_at: 2026-04-23
---

# #004 DB schema + migrations 独立目录管理

## 背景 / 发现

2026-04-23 发 0.4.0 live 过程中老板问："我们的数据库迁移脚本写在哪里也没看到，可以创建独立的文件夹吗？"

当前状态：
- **schema 定义**：`src/xiangqin/db.py` 里的 `_ensure_schema()` 函数，用 Python 字符串 heredoc 堆 `CREATE TABLE IF NOT EXISTS ...`
- **迁移**：散在 `db.py` 里的私有函数
  - `_drop_exposure_tables()` — M4 砍 exposure 时加的幂等 DROP
  - `_migrate_profile_columns()` — 给 profiles 加 height / education 字段
  - `_migrate_orders_columns()` — （曾经加的，已 clean up）
- **触发时机**：每次 `xiangqin.app:app` 启动时调用 `_ensure_schema()` + 各 `_migrate_*()` 函数

问题：
1. **找不到 migration 历史**：想知道"0.3.1 → 0.4.0 schema 到底变了什么"只能 git log -p src/xiangqin/db.py
2. **没有版本化**：migration 没有 version number，只靠 `IF NOT EXISTS` / `IF EXISTS` 做幂等，多人协作或分支合并时容易漏
3. **回滚不明确**：如果要回滚 schema，没有 "down migration"
4. **schema 文档脱节**：`docs/internal/schema.md`（如果有）是手写的，容易和代码漂移

## 方案选项

### A. 纯文件组织（轻）—— 推荐先做

```
schema/
├── README.md              # 当前 schema 总览 + migration 约定
├── tables.sql             # 所有 CREATE TABLE IF NOT EXISTS（etalon）
└── migrations/
    ├── 0001-initial.sql   # M1 首次建表
    ├── 0002-m4-envelope.sql  # M4 加 messages/envelope_* 表
    ├── 0003-drop-exposure.sql  # M4 砍 exposure_orders/usage
    ├── 0004-profile-m4.sql  # profile 加 height/education/agent_gateway_url 等
    └── index.md           # migration 时间线 + 各 migration 背景
```

`db.py` 改为读 `schema/tables.sql` + `migrations/*.sql`（按文件名顺序执行，sqlite 里加 `schema_migrations` 表记已执行 version）。

优点：
- 文件命名即可看 migration 历史
- SQL 原生（比 Python 更接近 schema 真相）
- 不加新依赖

### B. 接 alembic（重）

引入 alembic (SQLAlchemy migration 工具)：
- `alembic revision` 生成 migration
- `alembic upgrade head` 执行
- `alembic history` 看历史

优点：
- 成熟生态，有完整 down migration 支持
- 自动 diff schema 生成 migration

缺点：
- 引入 SQLAlchemy 依赖（xiangqin 目前是 raw sqlite3）
- 两人团队过度工程

### C. 不动（保持 Python 函数，加注释）

在 `db.py` 顶部加大段注释"迁移历史"，引用各 `_migrate_*` 函数。

优点：0 代码变更
缺点：只是文档化，没解决版本化/回滚问题

## 推荐

**A 方案，分两期做**：
1. **期一**（0.5.0 前）：创建 `schema/` 目录 + 迁移 `tables.sql`；历史 migration 回溯写 `migrations/0001-initial.sql` 等；`db.py` 继续跑现有 Python migration 函数（过渡期并行）
2. **期二**（0.6.0 前）：`db.py` 改为从 `migrations/` 按文件名扫描执行，删老 `_migrate_*` 函数

## 不做

**不跳到 alembic**。xiangqin 是自治单体实验，不想绑 SQLAlchemy 生态。raw sqlite3 + 纯 SQL 文件已经足够透明。

## 触发时机

不阻塞 0.4.0 live（0.4.0 schema 已在 `db.py` 里 hardcode）。下次大 schema change（v0.5.0 真付接入）前必须落。

## 相关

- `src/xiangqin/db.py` 当前实现
- ADR 2026-04-23-m4-mail-model-touchreach.md（M4 schema 设计）
- ADR 2026-04-23-drop-exposure-system.md（M4 DROP）
