From 26366c65a642e562c4c46f8ef50e6fe48bc34eb7 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 30 Aug 2026 16:46:35 +0800 Subject: [PATCH] fix(database): migrate versioned schemas before create_all --- app/startup/initializers/database.py | 11 ++++++++-- tests/test_database_migration_startup.py | 27 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/startup/initializers/database.py b/app/startup/initializers/database.py index 7bdbaf516..a4b977961 100644 --- a/app/startup/initializers/database.py +++ b/app/startup/initializers/database.py @@ -90,7 +90,7 @@ def _validate_migration_lineage( def prepare_database(*, before_alembic: Callable[[], None] | None = None) -> None: - """在建表或迁移前完成版本校验及可选备份。""" + """在建表或迁移前完成版本校验、可选备份与安全的结构升级。""" engine = get_engine() alembic_cfg = _build_alembic_config(engine) has_existing_database, current_heads, target_heads = _migration_state( @@ -101,6 +101,7 @@ def prepare_database(*, before_alembic: Callable[[], None] | None = None) -> Non has_existing_database and set(current_heads) != set(target_heads) ) + migrate_before_create = requires_migration and bool(current_heads) if ( requires_migration and get_runtime_setting('DB_BACKUP_ENABLE') @@ -114,11 +115,17 @@ def prepare_database(*, before_alembic: Callable[[], None] | None = None) -> Non ) build_database_governance().create_backup() + if migrate_before_create: + # 已标记的旧结构必须先沿 Alembic 链升级;当前元数据可能包含依赖新列的外键, + # 提前 create_all 会在 PostgreSQL 上因父表仍是旧结构而失败。 + update_db(alembic_cfg) + init_db() if before_alembic: # 首次初始化需要先建立用户表,再把管理员密码交给 Alembic 基础迁移消费。 before_alembic() - update_db(alembic_cfg) + if not migrate_before_create: + update_db(alembic_cfg) def verify_database_revision() -> None: diff --git a/tests/test_database_migration_startup.py b/tests/test_database_migration_startup.py index 4d6c55d16..bf3bd5fc1 100644 --- a/tests/test_database_migration_startup.py +++ b/tests/test_database_migration_startup.py @@ -101,15 +101,40 @@ def test_prepare_database_creates_backup_before_schema_changes(monkeypatch) -> N assert calls == [ "backup", + "alembic", "create_all", "before_alembic", - "alembic", ] assert logged_messages == [ "数据库需要从版本 old 升级到 head,正在创建迁移前备份" ] +def test_prepare_database_keeps_create_all_first_for_unversioned_database( + monkeypatch, +) -> None: + """未标记旧库仍需先补齐基础表,再从 Alembic base 执行数据迁移。""" + calls: list[str] = [] + monkeypatch.setattr(db_init, "get_engine", lambda: object()) + monkeypatch.setattr(db_init, "_build_alembic_config", lambda _engine: object()) + monkeypatch.setattr( + db_init, + "_migration_state", + lambda *_: (True, (), ("head",)), + ) + monkeypatch.setattr(db_init.settings, "DB_BACKUP_ENABLE", False) + monkeypatch.setattr(db_init, "init_db", lambda: calls.append("create_all")) + monkeypatch.setattr( + db_init, + "update_db", + lambda _config: calls.append("alembic"), + ) + + db_init.prepare_database(before_alembic=lambda: calls.append("before_alembic")) + + assert calls == ["create_all", "before_alembic", "alembic"] + + @pytest.mark.parametrize( ("has_existing_database", "current_heads", "backup_enabled", "upgrade_enabled"), (