refactor(migration): backfill all missing columns in ORM models

This migration updates the database schema by automatically adding all missing columns from the ORM models to the existing tables. It replaces the previous specific addition of 'tts_audio_urls' with a more comprehensive approach that inspects the ORM metadata and synchronizes it with the database schema. The downgrade function has been simplified to a no-op.
This commit is contained in:
penghanyuan
2026-05-17 22:04:54 +02:00
parent 8df6e42a30
commit 6b1d63524e
2 changed files with 94 additions and 27 deletions

View File

@@ -1,51 +1,66 @@
"""补建缺失列segments.tts_audio_urls, conversation_messages.tts_audio_urls
"""补建所有 ORM 模型中存在但数据库缺失的列
0001 用 create_all 建表,对已有表不会 ALTER 追加列。
本迁移补齐 ORM 模型中存在但生产库缺失的 tts_audio_urls 列。
本迁移自动内省 ORM 元数据与实际数据库 schema 的差异,
为每张已有表补齐缺失列(不含 FK 约束,仅补列定义)。
Revision ID: 0020_add_tts_audio_urls_column
Revision ID: 0020_backfill_all_missing_columns
Revises: 0019_backfill_missing_columns
"""
from __future__ import annotations
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0020_add_tts_audio_urls_column"
revision: str = "0020_backfill_all_missing_columns"
down_revision: Union[str, None] = "0019_backfill_missing_columns"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _column_names(table_name: str) -> set[str]:
bind = op.get_bind()
inspector = sa.inspect(bind)
return {column["name"] for column in inspector.get_columns(table_name)}
def _import_all_models() -> None:
from app.features.asset import models as _asset # noqa: F401
from app.features.auth import models as _auth # noqa: F401
from app.features.conversation import models as _conv # noqa: F401
from app.features.evaluation import models as _eval # noqa: F401
from app.features.memory import models as _memory # noqa: F401
from app.features.memoir import models as _memoir # noqa: F401
from app.features.payment import models as _payment # noqa: F401
from app.features.story import models as _story # noqa: F401
from app.features.user import models as _user # noqa: F401
def upgrade() -> None:
seg_cols = _column_names("segments")
if "tts_audio_urls" not in seg_cols:
op.add_column(
"segments",
sa.Column("tts_audio_urls", sa.JSON(), nullable=True),
)
from app.core.db import Base
msg_cols = _column_names("conversation_messages")
if "tts_audio_urls" not in msg_cols:
op.add_column(
"conversation_messages",
sa.Column("tts_audio_urls", sa.JSON(), nullable=True),
)
_import_all_models()
bind = op.get_bind()
inspector = sa.inspect(bind)
existing_tables = set(inspector.get_table_names())
for table in Base.metadata.sorted_tables:
if table.name not in existing_tables:
continue
db_cols = {c["name"] for c in inspector.get_columns(table.name)}
for col in table.columns:
if col.name in db_cols:
continue
kwargs: dict = {"nullable": True}
if col.server_default is not None:
kwargs["server_default"] = col.server_default.arg
new_col = sa.Column(col.name, col.type, **kwargs)
op.add_column(table.name, new_col)
def downgrade() -> None:
msg_cols = _column_names("conversation_messages")
if "tts_audio_urls" in msg_cols:
op.drop_column("conversation_messages", "tts_audio_urls")
seg_cols = _column_names("segments")
if "tts_audio_urls" in seg_cols:
op.drop_column("segments", "tts_audio_urls")
pass

View File

@@ -137,3 +137,55 @@ E2E 不是默认门禁。
- 团队能承担维护成本
引入后也只测关键主路径,不做像素巡检,不做大面积 UI 回归脚本。
### 前置准备
安装 Maestro CLI
```bash
curl -Ls "https://get.maestro.mobile.dev" | bash
```
构建并安装到目标 iOS Simulator / Android Emulator / 真机。不同 flow 对构建方式的要求不同:
| 场景 | 构建命令 | 读取配置 |
|------|---------|---------|
| 普通开发 | `pnpm ios` / `pnpm android` | `.env.development` |
| 需要登录态的 E2E | `pnpm ios:e2e` / `pnpm android:e2e` | `.env.e2e`staging backend + E2E 开关) |
正常 staging 构建继续使用 `.env.staging`,不注入 `EXPO_PUBLIC_E2E`
### 现有 flow
| Flow 文件 | 用途 | 需要后端 | 需要 E2E 构建 |
|-----------|------|---------|--------------|
| `login-smoke.yaml` | 登录页协议拦截 smoke验证 App 启动到未登录态 | 否 | 否 |
| `login-authenticated.yaml` | 快捷登录进入已登录态 | 是 | 是 |
| `post-login-tabs-smoke.yaml` | 登录后 Tab 导航 smoke对话 → 回忆录 → 我的 → 对话) | 是 | 是 |
| `post-login-long-chat.yaml` | 登录后发送大量对话消息,为回忆录生成提供种子数据 | 是 | 是 |
### 运行命令
无后端依赖(普通构建即可):
```bash
pnpm e2e:ios
pnpm e2e:android
```
需要 E2E 构建 + 后端 mock 登录:
```bash
pnpm e2e:auth:ios
pnpm e2e:auth:android
pnpm e2e:post-login:ios
pnpm e2e:long-chat:ios
```
调试时可通过 `pnpm start:e2e` 启动 dev server 并自动加载 `.env.e2e`
### 登录后 E2E 环境要求
- 后端:`APP_ENV` 不能是 `production`,并开启 `MOCK_SMS_LOGIN_ENABLED=1`
- App通过 `pnpm ios:e2e` / `pnpm android:e2e` 构建安装,读取 `.env.e2e`
- 所有需要登录态的 flow 通过点击 `login.e2e.quickLogin.button` 进入已登录态