You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.5 KiB
7.5 KiB
2026-02-15 数据库迁移链完整修复
问题来源
远程仓库提交 ecef7a7 和 7ad3bf9 中的迁移文件存在以下 Bug:
- UniqueViolationError:
CREATE OR REPLACE FUNCTION在 asyncpg 驱动下触发唯一约束冲突 - UndefinedTableError:迁移脚本引用已在后续迁移中删除的表
- DuplicateColumnError:非幂等的
ADD COLUMN导致重复创建列
根本原因
1. asyncpg 驱动的特殊行为
# ❌ 在 asyncpg 下会报错
CREATE OR REPLACE FUNCTION update_updated_at_column() ...
# 错误信息
UniqueViolationError: duplicate key value violates unique constraint
"pg_proc_proname_args_nsp_index"
原因:asyncpg 在执行 CREATE OR REPLACE FUNCTION 时会先尝试创建新函数,再删除旧函数,导致唯一键冲突。
2. 迁移顺序问题
迁移文件 20260205_1156_72966791b2f1 创建时,以下表还存在:
screenplay_charactersscreenplay_locationsscreenplay_propsstoryboard_resources
但在后续迁移中这些表被删除了:
drop_screenplay_tables.py- 删除 screenplay_* 表20260210_2059_bb1f354cdf48- 删除 storyboard_resources 表
当执行 start_docker.sh -c -b 清库重建时,迁移按时间顺序执行,20260205_1156 尝试操作不存在的表导致失败。
3. 非幂等的列创建
# ❌ 非幂等
op.add_column('screenplays', sa.Column('parsing_status', ...))
# 重复执行时报错
DuplicateColumnError: column "parsing_status" of relation "screenplays" already exists
修复方案
文件 1:20260205_1156_72966791b2f1_fix_time_fields_functions_and_triggers.py
修复 1:函数创建改为 DROP + CREATE
# ✅ 修复后
op.execute("DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;")
op.execute("""
CREATE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
""")
效果:
- 先删除旧函数及其依赖的触发器(CASCADE)
- 再创建新函数
- 避免唯一键冲突
修复 2:移除已删除表的引用
# ✅ 修复后
tables_need_updated_at = [
'ai_usage_logs',
# ...
# 'storyboard_resources', # 已在 bb1f354cdf48 中删除
'user_sessions'
]
tables_with_updated_at = [
# ...
# 'screenplay_characters', # 已删除
# 'screenplay_locations', # 已删除
# 'screenplay_props', # 已删除
# 'storyboard_resources', # 已在 bb1f354cdf48 中删除
]
效果:
- 避免
UndefinedTableError - 迁移可以在空库上干净执行
文件 2:20260205_1440_add_screenplay_parsing_fields.py
修复:改用原生 SQL 确保幂等性
# ✅ 修复后
op.execute("""
ALTER TABLE screenplays
ADD COLUMN IF NOT EXISTS parsing_status SMALLINT NOT NULL DEFAULT 0;
""")
op.execute("""
CREATE INDEX IF NOT EXISTS idx_screenplays_parsing_status
ON screenplays (parsing_status)
WHERE parsing_status IN (1, 2);
""")
效果:
- 多次执行不会报错
- 保证部署安全性
文件 3:20260205_1600_add_missing_column_comments.py
修复:注释掉已删除表的操作
# ✅ 修复后
# ==================== storyboard_resources ====================
# 注意:storyboard_resources 表已在 bb1f354cdf48 中删除
# op.execute("COMMENT ON COLUMN storyboard_resources.storyboard_resource_id IS ...")
# op.execute("COMMENT ON COLUMN storyboard_resources.storyboard_id IS ...")
# ...(其他7个字段的注释也注释掉)
效果:
- 避免
UndefinedColumnError - 历史原因保留注释代码供参考
测试验证
1. 清库重建测试
cd /Users/panta/py_work/jointoai_work/Jointoai/server
./start_docker.sh -c -b
结果:
✅ 容器重建成功
✅ 数据库清空成功
✅ 迁移执行成功(alembic upgrade head)
✅ 应用启动成功
2. 健康检查
curl http://localhost:6170/health
响应:
{
"status": "healthy",
"environment": "development"
}
3. 迁移版本检查
docker exec jointo-server-app alembic current
输出:
cb015d9eec93 (head)
4. 增量迁移测试
在已有数据库上执行 alembic upgrade head:
- ✅ 不会因为函数已存在而报错
- ✅ 不会因为列已存在而报错
- ✅ 不会因为表不存在而报错
修改统计
| 文件 | 修改类型 | 行数 | 说明 |
|---|---|---|---|
| 20260205_1156_72966791b2f1 | 函数创建 | +4 DROP | 4个函数先删除再创建 |
| 20260205_1156_72966791b2f1 | 移除引用 | -4 表 | 移除已删除表的引用 |
| 20260205_1440 | 改用SQL | ~30行 | ADD COLUMN IF NOT EXISTS |
| 20260205_1600 | 注释代码 | ~14行 | 注释掉storyboard_resources |
| 合计 | 66 insertions(+), 68 deletions(-) |
影响范围
无影响
- ✅ 数据库结构:无变化
- ✅ 已有数据:不受影响
- ✅ 应用逻辑:无变更
有改善
- ✅ 迁移稳定性:从不可执行到完全可靠
- ✅ 部署体验:
start_docker.sh -c -b从失败到成功 - ✅ 幂等性:多次执行安全
- ✅ 团队协作:拉取代码后不会破坏本地环境
提交信息
Commit:20dfc1b
标题:fix: 修复数据库迁移链问题(3个迁移文件)
推送:✅ 已推送至 origin/main
最佳实践建议
1. 编写迁移时的注意事项
# ✅ 推荐:显式 DROP + CREATE(asyncpg 友好)
op.execute("DROP FUNCTION IF EXISTS my_func() CASCADE;")
op.execute("CREATE FUNCTION my_func() ...")
# ❌ 避免:CREATE OR REPLACE(asyncpg 有坑)
op.execute("CREATE OR REPLACE FUNCTION my_func() ...")
2. 列/索引操作的幂等性
# ✅ 推荐:使用 IF NOT EXISTS
op.execute("ALTER TABLE t ADD COLUMN IF NOT EXISTS col TEXT;")
op.execute("CREATE INDEX IF NOT EXISTS idx ON t(col);")
# ❌ 避免:非幂等操作
op.add_column('t', sa.Column('col', sa.TEXT()))
op.create_index('idx', 't', ['col'])
3. 表删除后的清理
当删除表时,记得检查:
- 其他迁移是否引用了该表?
- 是否有触发器或函数依赖该表?
- 注释掉或删除相关的历史迁移代码
4. 迁移测试流程
# 1. 清库测试(最严格)
./start_docker.sh -c -b
# 2. 增量测试
docker exec jointo-server-app alembic upgrade head
# 3. 回滚测试
docker exec jointo-server-app alembic downgrade -1
# 4. 应用启动测试
docker logs jointo-server-app | grep "✅"
相关文档
总结
这次修复彻底解决了远程仓库迁移文件的3个核心问题:
- ✅ 解决 asyncpg 下的函数创建冲突
- ✅ 移除已删除表的引用
- ✅ 确保所有迁移操作的幂等性
修复结果:
- 团队成员拉取代码后不会遇到迁移失败
start_docker.sh -c -b可以稳定执行- 数据库迁移链完整且可靠
推荐操作:
# 1. 拉取最新代码
git pull origin main
# 2. 清库重建(可选)
cd server && ./start_docker.sh -c -b
# 3. 或者增量迁移
docker exec jointo-server-app alembic upgrade head
作者:Claude (AI Assistant)
日期:2026-02-15
状态:✅ 已验证并推送至远程