跳转至

数据库迁移与版本管理

本页描述 Chameleon 当前的 Alembic 迁移链路、执行方式与团队协作规范。

总览

项目使用 Alembic 管理 PostgreSQL 模式演进:

  • 配置入口:alembic.ini
  • 迁移环境:ai_service/migrations/env.py
  • 版本脚本目录:ai_service/migrations/versions/

env.py 会按顺序加载项目根目录的 .env.env.local,再从环境变量读取 DATABASE_URL 作为迁移连接字符串。

运行时迁移策略

默认采用“开发自动,staging/production 手动”策略:

  • APP_ENV=development:默认 DB_MIGRATION_MODE=auto,应用启动前自动执行 uv run alembic upgrade head
  • APP_ENV=staging/production:默认 DB_MIGRATION_MODE=manual,应用启动时不会自动迁移。
  • 可通过环境变量覆盖默认值:DB_MIGRATION_MODE=auto|manual

当前仓库的 Dokploy 发布链路会将 staging/production 的 DB_MIGRATION_MODE 写为 auto,因此迁移在 ai-service 容器启动阶段执行,而不是在 GitHub Actions 中单独执行。

空库(brand-new database)行为说明:

  • 当前 revision 链主要用于兼容历史库增量演进;在核心业务表不存在时会安全 no-op。
  • 随后应用启动事件会执行 init_database()Base.metadata.create_all)创建当前模型所需全量表结构。
  • 因此首次部署到全新 PostgreSQL 时,alembic upgrade head 不应再因缺表而失败。
  • 每个 revision 都必须显式声明 EMPTY_DATABASE_POLICY
  • fresh_safe:可直接安全运行在空库上。
  • legacy_compatible_noop:历史兼容迁移,缺少锚点表时必须安全短路。
  • legacy_compatible_noop 类型的 migration,统一通过 ai_service/migrations/helpers.py 中的 MigrationSchemaGuard 做表/列/索引存在性判断, 不再重复手写本地 _has_table() / _has_column() 逻辑。

生产发布建议:

  1. 确保部署环境配置 DB_MIGRATION_MODE=auto,并保证容器内 DATABASE_URL 可连通目标数据库。
  2. 对高风险迁移优先采用单实例滚动或维护窗口发布,避免多个实例并发启动同时触发迁移。

当前迁移链

截至当前仓库版本,主迁移链在 b1e350f626b1 之后再次分叉,并已在 fa12c4d6e8b9 重新收敛为单一 head。当前末端链路如下:

flowchart LR
    V1[d9b7a1c4e5f6]
    V2[aa91c2d4e6f7]
    V3[b1e350f626b1]
    V4[7d1e3c9b4a6f]
    V5[2c6d9a4e8f11]
    V6[e8c4a1b2d3f6]
    V7[2a6f4c8d9b1e]
    V8[fa12c4d6e8b9]

    V1 --> V3
    V2 --> V3
    V3 --> V4 --> V5 --> V8
    V3 --> V6 --> V7 --> V8

如需确认运行时的实际 head,以 uv run alembic heads 输出为准。

关键节点

Revision down_revision 主要变更
58d932c51f0c None Alembic 基线 revision
69fb951503de f1a9c3e8b472, f6a9b2c3d4e5 合并 skill_runtimeagent_versions 双分支
b1e350f626b1 aa91c2d4e6f7, d9b7a1c4e5f6 合并 grounding config 与 MCP/token statistics 双分支
e1f6c2b7a9d4 2a6f4c8d9b1e, 2c6d9a4e8f11 合并 encrypted_api_key_json 与 agent usage limit 双分支
fa12c4d6e8b9 e1f6c2b7a9d4 新增 Agent public access control 与 Fusion Agent API key support,恢复单一 head
8c4d2e1f9b7a c3b8a1d4e5f6 新增 agent message type / response 配置
2f7c5e1d9a4b 8c4d2e1f9b7a 新增 orchestrator_key 并回填历史 chat agent
b4d0f1a2c3e5 a7f4c2d9e1b3 新增 hide_rag_source_filename,同步到 agents / agent_versions 并默认保留原始 provenance

常用命令

先确保环境变量可用,尤其是 DATABASE_URLPOSTGRES_* 组合配置。

# 查看仓库中的 head revision
uv run alembic heads

# 查看当前数据库版本
uv run alembic current

# 升级到最新版本
uv run alembic upgrade head

# 回滚一个版本
uv run alembic downgrade -1

# 仅同步 alembic_version,不执行真实 DDL
uv run alembic stamp <target_revision>

# 基于模型变更生成迁移脚本
uv run alembic revision --autogenerate -m "describe change"

镜像回滚时的版本错位应急

当部署链路使用 DB_MIGRATION_MODE=auto 时,新镜像会在启动阶段执行 uv run alembic upgrade head。如果之后只回滚镜像、没有同步处理数据库, 就可能出现下面的错位:

  • 数据库中的 alembic_version 已经指向新 revision
  • 回滚后的旧镜像不再包含该 revision 文件
  • 旧镜像启动时无法解析当前数据库版本,报错 Can't locate revision identified by '<new_revision>'

推荐按以下顺序处理。

1. 先恢复服务可用性

如果旧镜像能够兼容“更前进”的数据库 schema,优先将回滚目标环境的 DB_MIGRATION_MODE 临时改为 manual,然后重新拉起旧镜像。

这样旧镜像启动时不会再执行 alembic upgrade head,可先避开 Alembic 版本解析失败。这个方案适合“旧代码通常能容忍新增列,但不能容忍未知 revision” 的场景。

2. 再决定是否同步数据库版本

在可以访问目标数据库的环境中执行:

uv run alembic heads
uv run alembic current

然后根据目标旧镜像的 revision 选择下面两种方式之一。

方案 A:只校准 Alembic 版本号

当旧镜像可以兼容当前 schema,只是 alembic_version 指向了旧镜像不认识的 revision 时,优先使用:

uv run alembic stamp <target_revision>

stamp 只会更新 alembic_version,不会删除列、索引或表,适合生产应急。

方案 B:真实执行数据库回退

只有在确认目标迁移脚本的 downgrade() 可执行、且回退不会误删仍然需要的数据时, 才使用:

uv run alembic downgrade <target_revision>

此方案会执行真实 DDL。若 migration 的 downgrade() 包含 drop_columndrop_table 或数据清理语句,回退后可能无法恢复。

3. 恢复后的运行建议

  • 若只是为了临时恢复旧镜像,建议在问题彻底定位前继续保持 DB_MIGRATION_MODE=manual
  • 若已通过 stampdowngrade 让数据库与旧镜像 revision 对齐,再决定是否恢复 auto
  • 任何需要长期保留的回滚方案,都应明确“镜像回滚是否同时要求数据库回退”

团队协作规范

1. 变更模型必须配套迁移

涉及以下文件变更时,应同时提交 Alembic revision:

  • ai_service/storage/models.py
  • ai_service/storage/model_domains/*.py
  • 与表结构强耦合的服务逻辑或 API 字段

2. 迁移脚本应可读可回滚

  • upgrade()downgrade() 都必须可执行。
  • 对线上敏感变更应拆分为多步小迁移,避免一次性高风险改动。

3. 空库策略必须显式声明

  • 新建 revision 时,必须保留模板里的 EMPTY_DATABASE_POLICY 常量,不允许省略。
  • 如果 migration 依赖旧表、旧列或旧索引,必须改成 EMPTY_DATABASE_POLICY_LEGACY_COMPATIBLE_NOOP,并使用 MigrationSchemaGuard.from_alembic(...) 做 schema guard。
  • tests/integration/test_migration_empty_database_noop.py 会自动扫描所有 revision:
  • 检查是否声明了受支持的 EMPTY_DATABASE_POLICY
  • legacy_compatible_noop revision 执行空库 no-op 回归测试

4. 与文档保持同步

修改数据模型后,请至少同步以下文档:

  • docs/database/schema.md
  • docs/database/migrations.md
  • 若 API 字段变化,同步 docs/guides/rag-knowledge-management.mddocs/api/endpoints.md

变更检查清单

在提交与数据库相关的 PR 前,建议逐项确认:

  • 已生成并提交迁移脚本
  • 本地执行 uv run alembic upgrade head 成功
  • 本地执行 uv run pytest tests/integration/test_migration_empty_database_noop.py -q 成功
  • 关键回滚路径可用 uv run alembic downgrade -1
  • 文档导航与内容已同步