c42cc2ddb6
Add docs/: a bilingual repository brief, plus docs/design/ with the high-level design (Alembic migration foundation, LLM integration, basic AI search) and a self-contained per-step implementation plan (step 1-3).
102 lines
6.7 KiB
Markdown
102 lines
6.7 KiB
Markdown
# 步骤 1 · Alembic 迁移地基 / Step 1 · Migration Foundation
|
||
|
||
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §3;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
|
||
> **前置 / Prerequisite:** 无(第一步)/ none.
|
||
> **产出 / Output:** 一个可独立合入的 PR;**不改任何业务 schema**。A mergeable PR with **zero business-schema change**.
|
||
|
||
---
|
||
|
||
## 目标 / Goal
|
||
|
||
引入 Alembic 并**安全接管现有生产库**,schema 一点不改,所有现有测试保持绿。
|
||
Introduce Alembic and **safely adopt the existing prod DB**, with zero schema change; all existing tests stay green.
|
||
|
||
---
|
||
|
||
## 必要背景 / Essential Context(仅凭本文件即可执行 / enough to execute from this file)
|
||
|
||
- **当前没有 Alembic。** 唯一的"迁移"是 `app/db.py::_sync_sqlite_image_columns()`(启动时缺图片列就 `ALTER TABLE ADD COLUMN`)。
|
||
No Alembic today; the only "migration" is the hand-rolled image-column sync in `app/db.py`.
|
||
- `app/db.py::init_db()` 在 FastAPI lifespan 启动时被 `create_app()` 调用,现在执行 `Base.metadata.create_all()` + `_sync_sqlite_image_columns()`。相关符号:`Base`、`engine`、`SessionLocal`、`configure_database()`。
|
||
`init_db()` runs at lifespan startup and currently does `create_all()` + the image-column sync.
|
||
- `tests/conftest.py` 的 `client` fixture:`configure_database(tmp_url)` → `create_app()`(触发 `init_db`)。每个测试用临时 SQLite,互不污染。
|
||
- models 在 `app/models.py`:`Box` / `Item` / `SubItem` 三张表;每张含 `image_blob`(BLOB) / `image_mime_type` / `image_width` / `image_height`,以及 `created_at` / `updated_at`。
|
||
- DB URL 来自 `app/config.py::get_settings().database_url`(默认 `sqlite:///./data/app.db`)。
|
||
- **生产库**是当年 `create_all` 建的、**已装上千件数据、没有 `alembic_version` 表**。
|
||
|
||
### 铁律 / The Invariant(不可违背 / non-negotiable)
|
||
|
||
- 所有数据库最终收敛到同一个 `head`。All DBs converge to the same `head`.
|
||
- **V1 baseline 必须严格等于"今天的真实 schema"**(三张表 + 现有图片列 + 索引),**不多一列**。新东西放后续 revision。
|
||
The V1 baseline must equal **today's actual schema exactly** — nothing more.
|
||
- 老库:`stamp V1`(只写版本号,**不建表、不碰数据**)→ `upgrade head`。
|
||
Existing DB: `stamp V1` (writes only the version row, **no DDL, no data change**) → `upgrade head`.
|
||
- 新库:跑 `V1`(真正建表)→ `upgrade head`。
|
||
Fresh DB: run `V1` (creates tables) → `upgrade head`.
|
||
|
||
---
|
||
|
||
## 任务 / Tasks
|
||
|
||
- [ ] `requirements.txt` 增加 `alembic`(钉一个明确版本 / pin a version)。
|
||
- [ ] 初始化 Alembic 工程:`alembic.ini` + `migrations/`(含 `env.py`、`versions/`)。
|
||
- [ ] 配置 `migrations/env.py`:
|
||
- `target_metadata = app.db.Base.metadata`(确保导入 `app.models` 以注册三张表)。
|
||
- `sqlalchemy.url` **从 `app.config.get_settings().database_url` 动态读取**,不写死在 `alembic.ini`。
|
||
- 对 SQLite 设 `render_as_batch=True`(为未来改列/删列预留 batch 能力)。
|
||
- [ ] 生成 **V1 baseline 迁移**=当前 models 的完整建表(`boxes`/`items`/`subitems`,含图片列与索引)。做法:对**空库** `--autogenerate`。
|
||
Author V1 by autogenerating against an **empty** DB.
|
||
- [ ] **验证 baseline**:对一份**生产库副本**跑 `alembic check`,确认**无差异**(印证可安全 `stamp`;SQLite 偶有类型亲和/索引命名假差异,人眼复核)。
|
||
Verify with `alembic check` against a **copy of the prod DB** → expect no diff.
|
||
- [ ] 新增封装 `app/migrate.py`,导出 `run_migrations(database_url: str)`:
|
||
- 编程方式构造 Alembic `Config`(`script_location` 指向打包进镜像的 `migrations/`,`sqlalchemy.url` = 传入 URL)。
|
||
- 用 SQLAlchemy inspector 实现自动认领:
|
||
- 有 `alembic_version` → `command.upgrade(cfg, "head")`
|
||
- 无 `alembic_version` 但有 `boxes` 表 → `command.stamp(cfg, "<V1 rev>")` → `command.upgrade(cfg, "head")`
|
||
- 全空 → `command.upgrade(cfg, "head")`
|
||
- [ ] 改 `app/db.py::init_db()`:改为调 `run_migrations(resolved_url)`,**删除** `_sync_sqlite_image_columns()`(Alembic 接管后冗余)。保留 `configure_database()` / engine 装配逻辑。
|
||
`init_db()` calls `run_migrations(...)`; **remove** `_sync_sqlite_image_columns()`.
|
||
- [ ] `Dockerfile`:加 `COPY alembic.ini .` 与 `COPY migrations ./migrations`(否则容器内无迁移脚本)。
|
||
- [ ] CI(可选 / optional):`.github/workflows/test.yml` 加一步 `alembic check`,防止 model 与迁移漂移。
|
||
|
||
---
|
||
|
||
## 涉及文件 / Files
|
||
|
||
`requirements.txt`、`alembic.ini`(新)、`migrations/**`(新)、`app/migrate.py`(新)、`app/db.py`、`Dockerfile`、`tests/`、(可选)`.github/workflows/test.yml`。
|
||
|
||
---
|
||
|
||
## 测试 / Tests
|
||
|
||
- [ ] 现有 ~83 个测试全绿(它们经 `init_db` 现在改走迁移建表)。
|
||
All existing ~83 tests pass (schema now built via migrations through `init_db`).
|
||
- [ ] 新增**认领老库**用例:构造一个"有 `boxes` 数据、无 `alembic_version`"的库(可先用 `create_all` 造),调 `run_migrations` 后断言:数据保留、`alembic_version` 到达 `head`、未重复建表报错。
|
||
New adoption test: a "has `boxes` data, no `alembic_version`" DB → after `run_migrations`, data preserved and version at `head`.
|
||
- [ ] 新增**全新库**用例:空 URL → `run_migrations` 后三张表存在、版本到 `head`。
|
||
|
||
---
|
||
|
||
## 验收 / Acceptance
|
||
|
||
- 全新库:从 V1 建表,应用正常起。Fresh DB builds from V1; app starts.
|
||
- 模拟老库:自动 `stamp` + `upgrade`,**数据无损**。Existing-like DB auto-adopts; data intact.
|
||
- 全部测试绿;schema 与本步骤前**逐列一致**(本步不改业务 schema)。
|
||
All tests green; schema identical to before (no business-schema change).
|
||
|
||
---
|
||
|
||
## 风险与缓解 / Risks & Mitigations
|
||
|
||
- **baseline 与现状有偏差 → `stamp` 失真。** 缓解:`alembic check` 对生产副本校验 + 人眼复核 SQLite 假差异。
|
||
Baseline drift → `alembic check` against a prod copy + manual eyeball.
|
||
- **容器内找不到迁移脚本。** 缓解:确认 `Dockerfile` 已 `COPY` `alembic.ini` 与 `migrations/`;`script_location` 用绝对/相对镜像 WORKDIR(`/app`) 正确解析。
|
||
Migrations missing in image → ensure they're `COPY`-ed and `script_location` resolves under `/app`.
|
||
|
||
---
|
||
|
||
## 相关约定 / Conventions(详见 implementation-plan.md)
|
||
|
||
- 不主动 push/commit,除非业主要求。Don't push/commit unless asked.
|
||
- 实现与设计若有偏差 → 回写设计文档 §3 与仓库简报 `../repository-brief.md` §10。
|