Files
OpenFlare/docs/superpowers/plans/2026-08-28-migration-split-plan.md
T
ryan 530a9dd3ee refactor(migration): delete pkg/migrator, move SQL to per-plugin embed
BREAKING: pkg/migrator/ deleted entirely. Migration SQL files are now
owned by each plugin in its own migrations/ directory.

Architecture:
- Delete pkg/migrator/ (26 global SQL files + ClickHouse migration)
- Move global SQL to per-plugin migrations/ with go:embed + Register()
- Rewrite cmd/app.go gooseEngine: uses Inject[DBService] for DB, iterates
  all plugin-registered MigrationEntry, runs goose.Up per entry
- core.MigrationEngine.Migrate signature changed: *Context instead of
  context.Context, so engine can resolve services via IoC

Per-plugin migration ownership:
  auth/             → w_access_tokens, w_auth_sources, w_external_accounts
  user/             → w_users (seed system user)
  admin/            → w_system_configs, w_templates (seeds)
  upload/           → w_uploads, w_upload_stats
  message_gateway/  → w_push_*, w_message_*
  risk_control/     → w_user_access_logs (PG + ClickHouse)
  driver_asynq_cron/  → w_schedules
  driver_asynq_worker/ → w_task_executions

Dependencies:
- cmd/banner.go: removed migration report display (migrations are automatic)
- cmd/reset_passwd.go: removed PreRun migrator.Migrate() call
- go.mod: clickhouse-go kept (used by plugins/infra/database/clickhouse.go)
2026-08-28 12:19:25 +08:00

326 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 迁移脚本拆分执行计划
## 背景现状
| 维度 | 实际状态 |
|---|---|
| 总 SQL 文件 | 26 个全局 (`pkg/migrator/goose/`) + 5 个插件 (`plugins/domain/*/migrations/`) + 1 个 ClickHouse |
| 实际运行的迁移 | **仅 26 个全局文件**(通过 `gooseEngine` → `pkg/migrator.Migrate()`) |
| 插件注册的迁移 | 3 个 (`auth`, `user`, `message_gateway`) — 注册了但被 `gooseEngine` 丢弃 |
| 有迁移文件但未注册的插件 | `admin`(2 个文件,0 个调用) |
| 无迁移文件的插件 | `upload`, `risk_control`, `cap`, `driver_asynq_worker`, `driver_asynq_cron` |
| ClickHouse 迁移 | 1 个文件 (`w_user_access_logs`),通过 `pkg/migrator.MigrateClickHouse()` 单独运行 |
## 表所有者映射
以下列表基于"单一所有者原则",每个表精确映射到一个插件:
| 表名 | 所有者插件 | 涉及全局迁移 |
|---|---|---|
| `w_users` | `domain/user` | 20260609 (create), 20260614 (seed system user) |
| `w_access_tokens` | `domain/auth` | 20260609 (create), 20260610 (is_admin), 20260611 (rm last_used_at) |
| `w_auth_sources` | `domain/auth` | 20260609 (create) |
| `w_external_accounts` | `domain/auth` | 20260609 (create) |
| `w_system_configs` | `domain/admin` | 20260609→20260611 (rename+seeds×7), 20260613 (TEXT), 20260816 (log_db) |
| `w_templates` | `domain/admin` | 20260609→20260611 (rename) |
| `w_schedules` | `driver_asynq_cron` | 20260610 (create), 20260611 (identity), 20260614 (update cleanup) |
| `w_task_executions` | `driver_asynq_worker` | 20260609→20260611 (rename) |
| `w_uploads` | `domain/upload` | 20260609→20260611 (rename), 20260613 (access_mode), 20260617 (indexes), 20260618 (drop storage_driver) |
| `w_upload_stats` | `domain/upload` | 20260617 (create+backfill) |
| `w_push_events` | `domain/message_gateway` | 20260614 (create), 20260615 (task_type), 20260616 (cleanup) |
| `w_push_histories` | `domain/message_gateway` | 20260614 (create) |
| `w_push_channels` | `domain/message_gateway` | 20260614 (create) |
| `w_message_channels` | `domain/message_gateway` | 20260816 (create) |
| `w_message_bindings` | `domain/message_gateway` | 20260816 (create) |
| `w_message_pairing_codes` | `domain/message_gateway` | 20260816 (create) |
| `w_user_access_logs` | `domain/risk_control` | 20260816 (create) + ClickHouse |
## 执行步骤(共 8 步)
---
### 步骤 1:创建 Bootstrap 迁移(保留在 `pkg/migrator`)
**文件**:`pkg/migrator/goose/postgres/00001_bootstrap.sql`
将以下全局迁移合并为一个 bootstrap 文件:
- **`202606090001_initial_schema.sql`** → 创建 `users`, `auth_sources`, `external_accounts`, `access_tokens`, `system_configs`, `uploads`, `task_executions`, `templates`(全部无前缀旧名)
- **`202606110003_rename_tables_to_w_prefix.sql`** → 全部重命名为 `w_` 前缀
**合并后,bootstrap 文件直接创建带 `w_` 前缀的表**,不再需要 rename 步骤:
```sql
-- +goose Up
CREATE TABLE IF NOT EXISTS w_users (
id BIGINT PRIMARY KEY,
username VARCHAR(64) UNIQUE,
...
);
CREATE TABLE IF NOT EXISTS w_access_tokens (...);
CREATE TABLE IF NOT EXISTS w_auth_sources (...);
CREATE TABLE IF NOT EXISTS w_external_accounts (...);
CREATE TABLE IF NOT EXISTS w_system_configs (
key VARCHAR(64) PRIMARY KEY,
value TEXT NOT NULL,
...
);
CREATE TABLE IF NOT EXISTS w_uploads (...);
CREATE TABLE IF NOT EXISTS w_task_executions (...);
CREATE TABLE IF NOT EXISTS w_templates (...);
CREATE TABLE IF NOT EXISTS w_schedules (...);
```
> **为什么保留在 `pkg/migrator`**:这些是平台的"初始化基座"——无论哪些插件启用,这些表都存在。将 bootstrap 放到 `pkg/migrator` 之下回避了循环依赖问题(例如 `w_system_configs` 属于 admin,但 bootstrap 时 admin 插件尚未 apply)。
---
### 步骤 2:修复 `gooseEngine` 支持插件迁移
**文件**:`cmd/app.go`
```go
type gooseEngine struct{}
func (e *gooseEngine) Migrate(_ context.Context, entries []core.MigrationEntry) error {
// 1. 先跑 bootstrap(初始化基座)
_ = migrator.Migrate()
// 2. 再跑每个插件注册的迁移
for _, entry := range entries {
gormDB := database.DB(context.Background())
if gormDB == nil {
continue
}
sqlDB, err := gormDB.DB()
if err != nil {
return err
}
goose.SetBaseFS(entry.FS)
if err := goose.SetDialect(gooseDialect()); err != nil {
return err
}
dir := entry.Dir
if dir == "" {
dir = "migrations"
}
if err := goose.Up(sqlDB, dir); err != nil {
return fmt.Errorf("migrate %s: %w", entry.PluginID, err)
}
}
// 3. ClickHouse 迁移
_ = migrator.MigrateClickHouse()
return nil
}
```
依赖项:`gooseDialect()` 从 `pkg/migrator` 导出。
---
### 步骤 3:按表所有者拆分迁移到各插件
| 全局源文件 | 目标插件 | 迁移文件名 |
|---|---|---|
| `202606100002` (access_tokens is_admin) | `domain/auth` | `migrations/00002_add_access_token_is_admin.sql` |
| `202606110001` (drop last_used_at) | `domain/auth` | `migrations/00003_drop_access_token_last_used_at.sql` |
| `202606140003` (system user seed) | `domain/user` | `migrations/00002_seed_system_user.sql` |
| `202606110004` (file_access_whitelist seed) | `domain/admin` | `migrations/00003_seed_file_access_whitelist.sql` |
| `202606110005` (disk_cache configs seed) | `domain/admin` | `migrations/00004_seed_disk_cache_configs.sql` |
| `202606120002` (update_upstream_repo seed) | `domain/admin` | `migrations/00005_seed_upstream_repo_config.sql` |
| `202606130002` (system_configs value TEXT) | `domain/admin` | `migrations/00006_expand_config_value.sql` |
| `202606130003` (storage_config seed) | `domain/admin` | `migrations/00007_seed_storage_config.sql` |
| `202608160002` (log database configs) | `domain/admin` | `migrations/00008_seed_log_db_configs.sql` |
| `202606120001` (login_session_ttl) | `domain/auth` | `migrations/00004_seed_login_session_ttl.sql` |
| `202606130001` (w_uploads access_mode) | `domain/upload` | `migrations/00001_add_access_mode.sql` |
| `202606170001` (upload indexes) | `domain/upload` | `migrations/00002_add_composite_indexes.sql` |
| `202606170002` (upload stats table) | `domain/upload` | `migrations/00003_create_upload_stats.sql` |
| `202606170003` (backfill stats) | `domain/upload` | `migrations/00004_backfill_upload_stats.sql` |
| `202606180001` (drop storage_driver) | `domain/upload` | `migrations/00005_drop_storage_driver.sql` |
| `202606140001` (push tables) | `domain/message_gateway` | `migrations/00002_create_push_tables.sql` |
| `202606140004` (push channels) | `domain/message_gateway` | `migrations/00003_create_push_channels.sql` |
| `202606150001` (push task_type) | `domain/message_gateway` | `migrations/00004_add_push_task_type.sql` |
| `202606160001` (remove push config) | `domain/message_gateway` | `migrations/00005_remove_push_config.sql` |
| `202608160003` (message gateway tables) | `domain/message_gateway` | `migrations/00006_create_message_tables.sql` |
| `202606100001` (schedules) | `driver_asynq_cron` | `migrations/00001_create_schedules.sql` |
| `202606110002` (schedules identity) | `driver_asynq_cron` | `migrations/00002_alter_schedules_identity.sql` |
| `202606140005` (update cleanup schedule) | `driver_asynq_cron` | `migrations/00003_update_cleanup_schedule.sql` |
| `202608160001` (user access logs) | `domain/risk_control/logstore` | `migrations/00001_create_access_logs.sql` |
| `202608160002` (log_database configs) | `domain/admin` | (合并到 admin 步骤 7) |
---
### 步骤 4:补充缺失的 `go:embed` 和 `Register()` 调用
**`plugins/domain/admin/plugin.go`**:
```go
//go:embed migrations/*.sql
var adminMigrations embed.FS
// 在 Apply() 中:
ctx.Migrations().Register("admin", adminMigrations)
```
**`plugins/domain/upload/plugin.go`**:
```go
//go:embed migrations/*.sql
var uploadMigrations embed.FS
// 在 Apply() 中:
ctx.Migrations().Register("upload", uploadMigrations)
```
**`plugins/domain/risk_control/plugin.go`**:
```go
// go:embed 由 logstore 子包自行处理(它已有自己的 moved 文件)
// 在 Apply() 中:
ctx.Migrations().Register("risk_control/logstore", logstoreMigrationFS)
```
**`plugins/drivers/driver_asynq_cron/plugin.go`**:
```go
//go:embed migrations/*.sql
var cronMigrations embed.FS
// 在 Apply() 中:
ctx.Migrations().Register("driver_asynq_cron", cronMigrations)
```
---
### 步骤 5:解决 Admin 插件迁移与 Bootstrap 的冲突
当前 `admin/migrations/00001` 执行 `CREATE TABLE IF NOT EXISTS w_system_configs (...)`,但 bootstrap 已在步骤 1 中创建过这张表。需要:
1. **保持 `IF NOT EXISTS`** 保证幂等性
2. **从 admin migration 中移除 `w_schedules` 和 `w_task_executions` 的 CREATE**(它们在 bootstrap 中创建,属于 driver 插件)
3. **仅保留 admin 自己的表**:`w_system_configs`, `w_templates`
4. Seed 数据使用 `ON CONFLICT DO NOTHING` 避免重复:
当前 admin 的 seed 包含 29 个系统配置,其中约 14 个与全局迁移重复。整理后的 admin seed 应:
```sql
INSERT INTO w_system_configs (...) VALUES
('cap_login_enabled', 'false', ...),
('cap_auto_solve', 'true', ...),
-- ... (所有 29 个配置)
ON CONFLICT (key) DO NOTHING;
```
> 全局迁移中 `202606110004` 到 `202608160002` 的 7 个种子 INSERT 将被迁移到 admin,全部使用 `ON CONFLICT DO NOTHING`。
---
### 步骤 6:清理已迁移的全局文件
拆分完成后,从 `pkg/migrator/goose/postgres/` 中删除以下文件:
```
202606100002_access_token_is_admin.sql
202606100001_create_schedules.sql
202606110001_remove_access_token_last_used_at.sql
202606110002_alter_schedules_id_auto_increment.sql
202606110004_add_file_access_whitelist_config.sql
202606110005_add_disk_cache_configs.sql
202606120001_add_login_session_ttl_config.sql
202606120002_add_update_upstream_repository_config.sql
202606130001_add_upload_access_mode.sql
202606130002_expand_system_config_value.sql
202606130003_add_storage_config.sql
202606140001_create_push_tables.sql
202606140003_add_system_user.sql
202606140004_create_push_channels.sql
202606140005_update_system_cleanup_schedule.sql
202606150001_add_task_type_to_push_events.sql
202606160001_remove_push_config.sql
202606170001_add_upload_composite_indexes.sql
202606170002_create_upload_stats_table.sql
202606170003_backfill_upload_stats.sql
202606180001_drop_upload_storage_driver.sql
202608160001_create_user_access_logs.sql
202608160002_log_database_configs.sql
202608160003_create_message_gateway.sql
```
**保留在 `pkg/migrator/goose/postgres/` 的仅限**:
```
00001_bootstrap.sql (合并后的初始化基座)
```
**注意**:`202606110003_rename_tables_to_w_prefix.sql` 也被合并进 bootstrap。`202606090001_initial_schema.sql` 也被合并掉。
---
### 步骤 7:更新 `pkg/migrator` 导出 `gooseDialect()`
在 `pkg/migrator/migrator.go` 中将 `gooseDialect()` 和 `migrationDir()` 改为导出,供 `cmd/app.go` 的 `gooseEngine.Migrate()` 引用。
---
### 步骤 8:验证 + 提交
```bash
cd /Users/ryan/Code/Go/Wavelet
# 1. 编译验证
go build -mod=mod ./...
go vet ./...
# 2. 架构门禁验证
make code-check
# 3. 验证插件迁移注册完整性
grep -rn 'go:embed.*migrations' plugins/domain/*/plugin.go plugins/drivers/*/plugin.go
grep -rn 'Migrations()\.Register' plugins/domain/*/plugin.go plugins/drivers/*/plugin.go
# → 每个有 migrations/ 目录的插件既要有 go:embed 又要有 Register()
# 4. 验证 admin 插件迁移完整性
grep -rn 'w_schedules\|w_task_executions' plugins/domain/admin/migrations/
# → 不应有(这些属于 driver 插件)
# 5. 提交
git add -A && git commit -m "refactor(migration): split global SQL into per-plugin migrations
- Merge 26 global SQLs into bootstrap + per-plugin migrations
- Fix gooseEngine to iterate plugin-registered MigrationEntry
- Add go:embed + Register() to admin, upload, risk_control, driver_asynq_cron
- Remove 23 migrated SQL files from pkg/migrator/goose/
- Keep only bootstrap in pkg/migrator/goose/
- All CREATE TABLE use IF NOT EXISTS, all INSERT use ON CONFLICT DO NOTHING"
```
---
## 依赖关系图
```
Bootstrap (pkg/migrator)
├── 创建 w_users, w_access_tokens, w_auth_sources, w_external_accounts
├── 创建 w_system_configs, w_templates, w_schedules, w_task_executions
├── 创建 w_uploads, w_upload_stats
└── 创建所有 w_ 前缀表
│
├─ auth/00002 (access_tokens is_admin)
├─ auth/00003 (drop last_used_at)
├─ auth/00004 (login_session_ttl seed)
│
├─ user/00002 (system user seed)
│
├─ admin/00001 (w_system_configs, w_templates) [IF NOT EXISTS]
├─ admin/00002 (29 config seeds + 2 template seeds)
├─ admin/00003–00008 (拆分后的种子迁移)
│
├─ upload/00001–00005 (access_mode → indexes → stats → backfill → drop)
│
├─ message_gateway/00001 (w_message_* tables)
├─ message_gateway/00002–00006 (push tables → channels → task_type → cleanup)
│
├─ driver_asynq_cron/00001–00003 (schedules → identity → cleanup)
│
├─ driver_asynq_worker/00001 (task_executions — 如果有追加操作)
│
└─ risk_control/logstore/00001 (w_user_access_logs)
```
所有步骤执行的迁移顺序由 Goose 的文件名前缀控制。Bootstrap 使用 `00001_`,每个插件的迁移从 `00002_` 开始编号(`00001` 留给插件自身表 CREATE,若插件 bootstrap 已创建则从 `00002` 开始)。