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

2026-02-15 数据库迁移链完整修复

问题来源

远程仓库提交 ecef7a77ad3bf9 中的迁移文件存在以下 Bug:

  1. UniqueViolationErrorCREATE OR REPLACE FUNCTION 在 asyncpg 驱动下触发唯一约束冲突
  2. UndefinedTableError:迁移脚本引用已在后续迁移中删除的表
  3. 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_characters
  • screenplay_locations
  • screenplay_props
  • storyboard_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 从失败到成功
  • 幂等性:多次执行安全
  • 团队协作:拉取代码后不会破坏本地环境

提交信息

Commit20dfc1b
标题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个核心问题:

  1. 解决 asyncpg 下的函数创建冲突
  2. 移除已删除表的引用
  3. 确保所有迁移操作的幂等性

修复结果

  • 团队成员拉取代码后不会遇到迁移失败
  • 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
状态 已验证并推送至远程