fix(app): guard force-column migration against downgrade-then-reupgrade

Review hardening for Task 8 (test-server workflow: testers install older
APKs over a newer DB):

- Old APK opening a v2 database hits drift's downgrade path, which leaves
  the force column in place but writes user_version back to 1. Reinstalling
  the new APK then re-runs onUpgrade(1,2) and addColumn crashes with
  "duplicate column name: force", failing the DB open.
- onUpgrade now checks PRAGMA table_info(upload_tasks) and only adds the
  force column when absent; the normal v1 -> v2 path is unchanged.
- Migration test simulates the downgrade-reupgrade cycle on a file-backed
  database (v2 open -> user_version forced back to 1 -> re-upgrade),
  asserting the open succeeds, legacy rows survive, and the column stays
  writable. RED before the guard (SqliteException: duplicate column name).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 17:27:52 +08:00
parent 29f3bde9c2
commit 46f9d93fd3
2 changed files with 105 additions and 40 deletions
@@ -141,7 +141,20 @@ class UploadTaskDb extends _$UploadTaskDb {
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
if (from == 1) {
await m.addColumn(uploadTasks, uploadTasks.force);
// 降级-重升级防护:旧 APK 覆盖安装打开 v2 库时,drift 降级
// 路径不改表、只把 user_version 写回 1(force 列留存);随后
// 新 APK 重升级再次走到这里——无条件 addColumn 会撞
// 「duplicate column name: force」致库打开失败。先探测列
// 存在性,缺列才补(正常 v1 → v2 路径列必缺省,行为不变)。
final columns = await m.database
.customSelect('PRAGMA table_info(upload_tasks)')
.get();
final forceExists = columns.any(
(row) => row.read<String>('name') == 'force',
);
if (!forceExists) {
await m.addColumn(uploadTasks, uploadTasks.force);
}
}
},
);