This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
|
||||
| 步骤 / Step | 文件 / File | 目标 / Goal | 改 schema? | 依赖 / Depends on |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **1** | [`step-1-alembic-foundation.md`](./step-1-alembic-foundation.md) | Alembic 迁移地基(封装 + V1 baseline + 自动认领),**不改 schema** | 否 / No | — |
|
||||
| **1** | [`step-1-alembic-foundation.md`](./step-1-alembic-foundation.md) | Alembic 迁移地基(V1 baseline + 独立幂等迁移命令 + 启动只校验/fail-close),**不改 schema** | 否 / No | — |
|
||||
| **2** | [`step-2-llm-integration.md`](./step-2-llm-integration.md) | LLM 接入:`app_settings` 表 + 客户端 + 配置页 | 是 / Yes (V2) | 步骤 1 / Step 1 |
|
||||
| **3** | [`step-3-ai-search.md`](./step-3-ai-search.md) | 基础 AI 搜索:常驻按钮 + 查询词扩展 | 否 / No | 步骤 2 / Step 2 |
|
||||
|
||||
|
||||
@@ -70,10 +70,13 @@
|
||||
└───────────────┘ └──────┬───────┘ └───────────────┘
|
||||
│
|
||||
┌───────────────────▼─────────────────────────┐
|
||||
│ app/migrate.py(封装层) │
|
||||
│ run_migrations(url): 自动 stamp / upgrade │
|
||||
│ app/migrate.py │
|
||||
│ 启动 / boot: verify_schema_is_current() 只读 │
|
||||
│ └─ 与 head 不一致 → fail-close,拒绝启动 │
|
||||
│ 命令 / CLI `python -m app.migrate`(幂等): │
|
||||
│ └─ 空库建库 / 认领老库 / upgrade(见 §3) │
|
||||
└───────────────────┬─────────────────────────┘
|
||||
│ command.upgrade / stamp
|
||||
│ command.upgrade / stamp(仅迁移命令 / migration command only)
|
||||
┌───────────────────▼─────────────────────────┐
|
||||
│ Alembic (alembic.ini + migrations/) │
|
||||
│ V1 baseline → V2(app_settings) → … │
|
||||
@@ -112,32 +115,60 @@ All databases converge to the same `head`. The `V1 baseline` must equal **today'
|
||||
> `stamp` 只向 `alembic_version` 写一条版本记录,**不执行任何 DDL、不修改数据**。这是安全认领已有库的关键。
|
||||
> `stamp` only writes a row into `alembic_version`; it runs **no DDL and touches no data**. This is the key to safely adopting an existing DB.
|
||||
|
||||
### 3.3 自动认领逻辑 / Auto-adoption (in `app/migrate.py`)
|
||||
### 3.3 运行时机:校验与迁移分离 / Migrations Run Separately from Startup
|
||||
|
||||
`init_db()` 启动时调用 `run_migrations(url)`,内部用 SQLAlchemy inspector 判断:
|
||||
At startup `init_db()` calls `run_migrations(url)`, which inspects the DB:
|
||||
**关键决策:迁移不在应用启动时发生。** 启动只做**只读校验**,迁移由一个独立、显式的命令/步骤执行。
|
||||
**Key decision: migrations do not happen at app startup.** Startup only **verifies** (read-only); migrating is an explicit, separate step.
|
||||
|
||||
- **启动校验(fail-close)/ Startup check (fail-closed):** `app/db.py::init_db()` 调 `app/migrate.py::verify_schema_is_current(url)`,比较 DB 当前 revision 与 `head`:
|
||||
- 一致 → 正常启动 / match → start normally。
|
||||
- 不一致(含空库、未认领的老库)→ **fail-close**:输出清晰日志、拒绝提供服务、提示先跑迁移步骤;**不执行任何 DDL、不碰数据**。
|
||||
Mismatch (incl. empty or un-adopted DBs) → **fail closed**: clear log, refuse to serve, no DDL, no data change.
|
||||
- **迁移命令 / The migration command:** 独立、显式、**幂等**的 `python -m app.migrate`(逻辑在 `app/migrate.py`)。已在 `head` 则空操作并退出 0,便于每次部署都安全重跑。
|
||||
A separate, explicit, **idempotent** `python -m app.migrate`. No-op (exit 0) when already at `head`, so it is safe to re-run on every deploy.
|
||||
- 退休手写列同步 / Retire the hand-rolled sync:`_sync_sqlite_image_columns()` 删除,schema 由 Alembic 单一接管。
|
||||
`_sync_sqlite_image_columns()` is removed; Alembic is the sole owner of schema.
|
||||
|
||||
为什么 / Why:避免"启动副作用式迁移"、避免多实例并发迁移竞态;当 code 与 DB 不一致时,**宁可不启动也不带病运行**。
|
||||
Avoids surprise startup migrations and concurrent-migration races; on a code/DB mismatch it refuses to run rather than run wrong.
|
||||
|
||||
### 3.4 迁移命令的三种情况 / The Migration Command's Three Cases
|
||||
|
||||
`python -m app.migrate` 用 SQLAlchemy inspector 判定,分三种:
|
||||
`python -m app.migrate` inspects the DB and branches three ways:
|
||||
|
||||
| 库的状态 / DB state | 动作 / Action |
|
||||
| --- | --- |
|
||||
| 有 `alembic_version` / has `alembic_version` | `upgrade head` |
|
||||
| 无 `alembic_version` 但有 `boxes` 表(=老生产库)/ no `alembic_version` but `boxes` exists | `stamp V1` → `upgrade head` |
|
||||
| 全空 / empty | `upgrade head`(从 V1 建起 / build from V1) |
|
||||
| **空库 / empty** | `upgrade head`(建库并升到最新 / create & upgrade to head) |
|
||||
| **老库且与 baseline 一致 / existing, matches baseline(2a)** | `stamp V1` → `upgrade head`(认领后升级 / adopt then upgrade) |
|
||||
| **老库但与 baseline 不一致 / existing, mismatched(2b)** | **fail-close,不做任何改动 / fail closed, no changes** |
|
||||
|
||||
这样**生产机重新部署零手动迁移命令**,老数据安全。
|
||||
So redeploying the production box needs **zero manual migration commands**; existing data is safe.
|
||||
> **一致性比对的基准是 baseline(V1),不是 head。** 未认领的老库结构停在 V1(不含 `app_settings` 等后续内容),若拿 head 去比会把合法老库误判为不一致。
|
||||
> The match is compared against the **baseline (V1)**, not `head` — an un-adopted DB sits at V1 and would wrongly look "mismatched" if compared against head.
|
||||
>
|
||||
> ⚠️ SQLite 的 autogenerate 比对存在假阳性(类型亲和、索引命名等),可能让 2b 误 fail。实现上需用**容忍性比对**或允许**人工确认覆盖**(见 §3.6 验证)。
|
||||
> SQLite autogenerate has false positives; 2b should use a tolerant comparison or allow a documented manual override (see §3.6).
|
||||
|
||||
### 3.4 封装层 / The Wrapper (`app/migrate.py`)
|
||||
### 3.5 部署形态:Compose db-migration 闸门 / Deployment Shape: a Compose Gate(未来 / future)
|
||||
|
||||
应用其余部分只调用 `run_migrations(database_url)`,不直接接触 Alembic API。封装内部:
|
||||
The rest of the app only calls `run_migrations(database_url)`; Alembic stays encapsulated. Internally it:
|
||||
意图:用一个一次性 `db-migration` 服务跑迁移命令,**成功才放行 App**。本轮可先只交付命令本身,Compose 接线随后。
|
||||
Intent: a one-shot `db-migration` service runs the command and **the app starts only on its success**. The command ships this round; the Compose wiring can follow.
|
||||
|
||||
- 以编程方式构造 Alembic `Config`(`script_location` 指向打包进镜像的 `migrations/`,`sqlalchemy.url` 用传入的 URL)。
|
||||
Builds an Alembic `Config` programmatically (`script_location` → bundled `migrations/`, `sqlalchemy.url` → the passed URL).
|
||||
- 调 `command.stamp(...)` / `command.upgrade(...)`。
|
||||
- 由 `init_db()` 调用,取代原来的 `create_all()` + `_sync_sqlite_image_columns()`(后者删除)。
|
||||
Called by `init_db()`, replacing `create_all()` + `_sync_sqlite_image_columns()` (the latter is removed).
|
||||
```yaml
|
||||
services:
|
||||
db-migration:
|
||||
image: <same image>
|
||||
command: python -m app.migrate # 成功 exit 0;2b/失败 exit ≠0
|
||||
web:
|
||||
depends_on:
|
||||
db-migration:
|
||||
condition: service_completed_successfully
|
||||
```
|
||||
|
||||
### 3.5 Alembic 配置要点 / Alembic config notes
|
||||
迁移失败(含 2b 不一致)→ App 永不启动。
|
||||
A failed migration (incl. a 2b mismatch) → the app never starts.
|
||||
|
||||
### 3.6 Alembic 配置要点 / Alembic config notes
|
||||
|
||||
- `migrations/env.py`:`target_metadata = Base.metadata`;DB URL 从 `get_settings().database_url` 动态读取(不写死在 `alembic.ini`);对 SQLite 设 `render_as_batch=True`(便于未来改列/删列走 batch 模式)。
|
||||
`target_metadata = Base.metadata`; URL read dynamically from settings; `render_as_batch=True` for SQLite.
|
||||
@@ -267,10 +298,12 @@ AI off/unconfigured → no button (or a hint to `/settings`); on failure → a f
|
||||
|
||||
## 8. 测试策略 / Testing Strategy
|
||||
|
||||
- **迁移在测试中真实执行 / Migrations run in tests:** 临时 SQLite 上 `upgrade head`,schema 来自迁移本身——单一事实来源,且为迁移提供覆盖。
|
||||
`upgrade head` on a tmp SQLite; schema comes from migrations — single source of truth plus migration coverage.
|
||||
- **认领逻辑测试 / Adoption test:** 构造一个"有 `boxes` 数据但无 `alembic_version`"的库,跑 `run_migrations`,断言数据保留且版本到达 head。
|
||||
Build a "has `boxes` data, no `alembic_version`" DB, run `run_migrations`, assert data preserved and version at head.
|
||||
- **迁移在测试中真实执行 / Migrations run in tests:** fixture 先在临时 SQLite 上跑迁移命令(建库 → `upgrade head`),再 `create_app()`(启动校验随之通过)。schema 来自迁移本身——单一事实来源 + 迁移覆盖。
|
||||
The fixture runs the migration command on a tmp SQLite first, then `create_app()` (whose startup check then passes).
|
||||
- **认领逻辑测试 / Adoption test(2a):** 构造"有 `boxes` 数据但无 `alembic_version`"的库 → 跑迁移命令 → 断言数据保留、版本到达 head。
|
||||
Build a "has `boxes` data, no `alembic_version`" DB → run the migration command → assert data preserved and version at head.
|
||||
- **fail-close 测试 / Fail-closed tests:** ① DB 未到 head 时 `create_app()` 启动应 fail-close;② 2b 不一致时迁移命令应 fail-close 且不改动。
|
||||
① `create_app()` fails closed when the DB is not at head; ② the migration command fails closed (and changes nothing) on a 2b mismatch.
|
||||
- **LLM 全程 mock / Mock the LLM:** 打桩 `expand_query` / `test_connection`(或底层 httpx),CI 不联网。
|
||||
- **新增用例 / New cases:** 配置增删改 + Key 脱敏;测试连接(mock);AI 搜索扩展命中;各降级路径(未配置/失败)。
|
||||
|
||||
@@ -293,7 +326,7 @@ AI off/unconfigured → no button (or a hint to `/settings`); on failure → a f
|
||||
| --- | --- | --- |
|
||||
| D1 | 先引入 Alembic 再做功能 / Alembic before features | 配置表与未来列都依赖可靠迁移;退休手写列同步。 |
|
||||
| D2 | V1 baseline 严格等于现状,新东西放 V2+ / baseline = current schema only | 使 `stamp` 认领老库为真、安全。 |
|
||||
| D3 | 自动 stamp/upgrade / auto-adopt | 生产机零手动迁移;契合自托管"开箱即用"。 |
|
||||
| D3 | 迁移与启动分离:启动只校验 + fail-close,迁移走独立幂等命令(`python -m app.migrate`)/ 未来 Compose `db-migration` 闸门 / migrations separated from startup | 避免启动副作用式迁移与并发竞态;schema 不一致宁可不启动也不带病运行;迁移成功才放行 App。 |
|
||||
| D4 | 配置用 KV 表 / KV settings table | 后续配置项多,避免反复给已有表加列。 |
|
||||
| D5 | API Key 明文落库 / plaintext key | 业主威胁模型下可接受;备份至自有 OneDrive。 |
|
||||
| D6 | 复用 httpx,手搓 OpenAI 调用 / reuse httpx | 不引入 `openai` SDK,依赖最小。 |
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
## 目标 / Goal
|
||||
|
||||
引入 Alembic 并**安全接管现有生产库**,schema 一点不改,所有现有测试保持绿。
|
||||
Introduce Alembic and **safely adopt the existing prod DB**, with zero schema change; all existing tests stay green.
|
||||
引入 Alembic 并**安全接管现有生产库**,schema 一点不改,所有现有测试保持绿。**迁移与应用启动分离**:启动只做只读校验 + fail-close,实际迁移由独立、幂等命令 `python -m app.migrate` 执行。
|
||||
Introduce Alembic and **safely adopt the existing prod DB** with zero schema change; all tests stay green. **Migration is separated from startup**: boot only verifies (read-only) and fails closed; the actual migrating is done by a separate idempotent command `python -m app.migrate`.
|
||||
|
||||
---
|
||||
|
||||
@@ -17,8 +17,8 @@ Introduce Alembic and **safely adopt the existing prod DB**, with zero schema ch
|
||||
|
||||
- **当前没有 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.
|
||||
- `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. **This step turns it into a read-only check** (no table creation/migration at boot).
|
||||
- `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`)。
|
||||
@@ -29,10 +29,11 @@ Introduce Alembic and **safely adopt the existing prod DB**, with zero schema ch
|
||||
- 所有数据库最终收敛到同一个 `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`.
|
||||
- 以下动作**由迁移命令执行,不在应用启动时** / done by the **migration command**, not at boot:
|
||||
- 老库且与 baseline 一致:`stamp V1`(只写版本号,**不建表、不碰数据**)→ `upgrade head`。
|
||||
Existing DB matching baseline: `stamp V1` (no DDL, no data change) → `upgrade head`.
|
||||
- 老库但与 baseline 不一致:**fail-close,不做任何改动**。Mismatched existing DB → fail closed.
|
||||
- 新库:跑 `V1`(真正建表)→ `upgrade head`。Fresh DB: run `V1` → `upgrade head`.
|
||||
|
||||
---
|
||||
|
||||
@@ -48,39 +49,54 @@ Introduce Alembic and **safely adopt the existing prod DB**, with zero schema ch
|
||||
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()`.
|
||||
- [ ] 新增 `app/migrate.py`,承担两个职责 / two responsibilities:
|
||||
- **(A) 迁移命令入口 `python -m app.migrate`(幂等 / idempotent)**:编程方式构造 Alembic `Config`(`script_location` → 打包进镜像的 `migrations/`,`sqlalchemy.url` = 解析出的 URL),用 SQLAlchemy inspector 分情况:
|
||||
- 空库 / empty → `command.upgrade(cfg, "head")`
|
||||
- 老库且与 **baseline(V1)** 一致 → `command.stamp(cfg, "<V1 rev>")` → `command.upgrade(cfg, "head")`
|
||||
- 老库但与 baseline 不一致 → **fail-close**:非零退出 + 清晰日志 + **不做任何改动**
|
||||
- 已在 `head` → 空操作、退出 0
|
||||
- `<V1 rev>` 指 **baseline 这个具体 revision**(`down_revision=None` 的那条),不是 `head`。
|
||||
- "与 baseline 一致"的判定**对照 baseline(V1) 的预期 schema**(不是 head);SQLite 假差异需容忍或允许人工确认覆盖。
|
||||
- **(B) 启动校验 `verify_schema_is_current(url)`(只读 / read-only)**:比较 DB 当前 revision 与 `head`;不一致返回失败/抛错,**绝不改动 DB**。
|
||||
- [ ] 改 `app/db.py::init_db()`:改为调 `verify_schema_is_current(resolved_url)` —— **一致才放行;不一致 fail-close**(清晰日志,提示先跑 `python -m app.migrate`)。不再在启动时建表/迁移。**删除** `_sync_sqlite_image_columns()`。保留 `configure_database()` / engine 装配。
|
||||
`init_db()` now only verifies and **fails closed** on mismatch (pointing the user to `python -m app.migrate`); remove `_sync_sqlite_image_columns()`.
|
||||
- [ ] `tests/conftest.py`:fixture 改为**先跑迁移命令**把临时库带到 `head`,再 `create_app()`(这样启动校验通过)。
|
||||
Fixture runs the migration first, then `create_app()`.
|
||||
- [ ] `Dockerfile`:加 `COPY alembic.ini .` 与 `COPY migrations ./migrations`(否则容器内无迁移脚本)。
|
||||
- [ ] CI(可选 / optional):`.github/workflows/test.yml` 加一步 `alembic check`,防止 model 与迁移漂移。
|
||||
- [ ] Compose `db-migration` 闸门(可后续 / can be deferred):加一个一次性服务跑 `python -m app.migrate`,`web` 经 `depends_on: condition: service_completed_successfully` 等它成功(见设计 §3.5)。
|
||||
Add a one-shot `db-migration` service gating `web` (design §3.5); may be deferred.
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件 / Files
|
||||
|
||||
`requirements.txt`、`alembic.ini`(新)、`migrations/**`(新)、`app/migrate.py`(新)、`app/db.py`、`Dockerfile`、`tests/`、(可选)`.github/workflows/test.yml`。
|
||||
`requirements.txt`、`alembic.ini`(新)、`migrations/**`(新)、`app/migrate.py`(新)、`app/db.py`、`tests/conftest.py`、`Dockerfile`、(可选)`.github/workflows/test.yml`、(可后续)`docker-compose.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`。
|
||||
- [ ] 现有 ~83 个测试全绿(fixture 先跑迁移、再起 App,启动校验通过)。
|
||||
All existing ~83 tests pass (fixture migrates first, then starts the app).
|
||||
- [ ] **认领老库(2a)**:构造"有 `boxes` 数据、无 `alembic_version`"的库(可先用 `create_all` 造)→ 跑迁移命令 → 断言数据保留、版本到达 `head`、未重复建表报错。
|
||||
Adoption (2a): migrate an un-stamped populated DB → data preserved, version at `head`.
|
||||
- [ ] **全新库**:空 URL → 跑迁移命令 → 三张表存在、版本到 `head`。
|
||||
Fresh DB: empty URL → migrate → tables exist, version at `head`.
|
||||
- [ ] **fail-close(启动)**:DB 未到 `head` 时 `create_app()` / `init_db()` 启动应 fail-close(抛错/拒绝服务)、不改动 DB。
|
||||
Startup fails closed when the DB is not at `head`; DB unchanged.
|
||||
- [ ] **fail-close(2b)**:构造与 baseline 不一致的老库 → 跑迁移命令 → 断言非零退出、DB 不变。
|
||||
Migration command fails closed on a 2b mismatch; DB unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 验收 / Acceptance
|
||||
|
||||
- 全新库:从 V1 建表,应用正常起。Fresh DB builds from V1; app starts.
|
||||
- 模拟老库:自动 `stamp` + `upgrade`,**数据无损**。Existing-like DB auto-adopts; data intact.
|
||||
- 迁移命令:空库建到 `head`;老库一致则认领并到 `head`;老库不一致则 **fail-close 不改动**;已在 `head` 则幂等空操作。
|
||||
Migration command: empty→head; matching existing→adopt+head; mismatch→fail closed; already-at-head→no-op.
|
||||
- 启动校验:DB 未到 `head` 时**拒绝启动**并输出清晰日志;到 `head` 才正常起。
|
||||
Startup refuses to boot (clear log) unless the DB is at `head`.
|
||||
- 模拟老库认领后**数据无损**。Adopted existing-like DB keeps data intact.
|
||||
- 全部测试绿;schema 与本步骤前**逐列一致**(本步不改业务 schema)。
|
||||
All tests green; schema identical to before (no business-schema change).
|
||||
|
||||
@@ -90,6 +106,8 @@ Introduce Alembic and **safely adopt the existing prod DB**, with zero schema ch
|
||||
|
||||
- **baseline 与现状有偏差 → `stamp` 失真。** 缓解:`alembic check` 对生产副本校验 + 人眼复核 SQLite 假差异。
|
||||
Baseline drift → `alembic check` against a prod copy + manual eyeball.
|
||||
- **2b 一致性比对假阳性 → 合法老库被误 fail-close。** 缓解:比对基准用 baseline(V1) 而非 head;容忍已知 SQLite 噪声,或提供"人工确认覆盖"的开关。
|
||||
2b false positives wrongly fail a legit DB → compare against baseline (not head); tolerate known SQLite noise or offer a manual-confirm override.
|
||||
- **容器内找不到迁移脚本。** 缓解:确认 `Dockerfile` 已 `COPY` `alembic.ini` 与 `migrations/`;`script_location` 用绝对/相对镜像 WORKDIR(`/app`) 正确解析。
|
||||
Migrations missing in image → ensure they're `COPY`-ed and `script_location` resolves under `/app`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 步骤 2 · LLM 接入 / Step 2 · LLM Integration
|
||||
|
||||
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §4;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
|
||||
> **前置 / Prerequisite:** [步骤 1](./step-1-alembic-foundation.md) 已合入(Alembic 已就位——**schema 变更一律通过新建迁移完成**)。Step 1 merged; Alembic is in place — **all schema changes go through a new migration**.
|
||||
> **前置 / Prerequisite:** [步骤 1](./step-1-alembic-foundation.md) 已合入(Alembic 已就位——**schema 变更一律通过新建迁移完成,并经迁移命令 `python -m app.migrate` / `db-migration` 步骤生效,非应用启动时**)。Step 1 merged; Alembic is in place — **schema changes go through a new migration, applied by the migration command, not at app startup**.
|
||||
> **产出 / Output:** 一个可独立合入的 PR。
|
||||
|
||||
---
|
||||
|
||||
@@ -208,16 +208,18 @@ The service worker only claims clients — **no caching, no offline** yet.
|
||||
|
||||
---
|
||||
|
||||
## 10. 数据库初始化与迁移 / DB Init & Migrations (`app/db.py`)
|
||||
## 10. 数据库初始化与迁移 / DB Init & Migrations (`app/migrate.py` + `app/db.py`)
|
||||
|
||||
- 懒加载 engine;`init_db()` 在 FastAPI `lifespan` 启动时调用,执行 `Base.metadata.create_all`。
|
||||
Lazy engine; `init_db()` runs at FastAPI startup (lifespan) and does `create_all`.
|
||||
- **Alembic 接管 schema**:迁移系统由 Alembic 管理(`alembic.ini` + `migrations/`),V1 baseline 等于当前三表 schema。
|
||||
Alembic owns schema creation and changes (`alembic.ini` + `migrations/`); V1 baseline equals the current three-table schema.
|
||||
- **迁移与启动分离 / Migrations separated from startup**:
|
||||
- `init_db()`(`app/db.py`)在 FastAPI lifespan 启动时调用 `verify_schema_is_current()`,只做**只读校验**——检查 DB 是否在 `head`,不一致则 **fail-close**(拒绝启动、不执行任何 DDL)。
|
||||
`init_db()` calls `verify_schema_is_current()` at startup — read-only check, fails closed on mismatch, no DDL.
|
||||
- 实际迁移由独立幂等命令 `python -m app.migrate`(`app/migrate.py`)执行:空库建表、老库认领(stamp V1 → upgrade head)、已在 head 则空操作。老库 schema 不匹配则 fail-close 不改动。
|
||||
Actual migration via standalone idempotent command `python -m app.migrate`: fresh DB → create, matching existing → adopt, already-at-head → no-op, mismatch → fail closed.
|
||||
- SQLite 连接开启 `PRAGMA foreign_keys=ON`。
|
||||
- **轻量「迁移」/ Ad-hoc migration**:`_sync_sqlite_image_columns()` 在启动时用 `PRAGMA table_info` 检测,并对缺失的图片列做 `ALTER TABLE ADD COLUMN`。这是项目**唯一**的迁移机制,专门为「后加图片功能」补列而写。
|
||||
The only migration mechanism is a hand-written check that adds the four image columns if missing.
|
||||
|
||||
> ⚠️ **没有 Alembic / 没有通用迁移**。新增任何**非图片**字段到已有库,需要扩展这段逻辑或手动 `ALTER`,否则旧库不会自动获得新列。这是下一轮改动需要特别注意的点(见 §14)。
|
||||
> No Alembic / general migrations — adding new non-image columns to an existing DB needs manual handling.
|
||||
- 手写列同步 `_sync_sqlite_image_columns()` 已退休删除。
|
||||
The hand-rolled `_sync_sqlite_image_columns()` has been retired and removed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user