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)
14 KiB
迁移脚本拆分执行计划
背景现状
| 维度 | 实际状态 |
|---|---|
| 总 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 步骤:
-- +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
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:embed migrations/*.sql
var adminMigrations embed.FS
// 在 Apply() 中:
ctx.Migrations().Register("admin", adminMigrations)
plugins/domain/upload/plugin.go:
//go:embed migrations/*.sql
var uploadMigrations embed.FS
// 在 Apply() 中:
ctx.Migrations().Register("upload", uploadMigrations)
plugins/domain/risk_control/plugin.go:
// go:embed 由 logstore 子包自行处理(它已有自己的 moved 文件)
// 在 Apply() 中:
ctx.Migrations().Register("risk_control/logstore", logstoreMigrationFS)
plugins/drivers/driver_asynq_cron/plugin.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 中创建过这张表。需要:
- 保持
IF NOT EXISTS保证幂等性 - 从 admin migration 中移除
w_schedules和w_task_executions的 CREATE(它们在 bootstrap 中创建,属于 driver 插件) - 仅保留 admin 自己的表:
w_system_configs,w_templates - Seed 数据使用
ON CONFLICT DO NOTHING避免重复:
当前 admin 的 seed 包含 29 个系统配置,其中约 14 个与全局迁移重复。整理后的 admin seed 应:
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:验证 + 提交
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 开始)。