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:
@@ -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
|
||||
|
||||
@@ -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` 进入已登录态
|
||||
|
||||
Reference in New Issue
Block a user