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)
This commit is contained in:
ryan
2026-08-28 12:19:25 +08:00
parent c9b702d234
commit 530a9dd3ee
79 changed files with 552 additions and 1534 deletions
+63 -7
View File
@@ -4,12 +4,13 @@
package cmd
import (
"context"
"fmt"
"log"
"time"
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/pkg/config"
"github.com/Rain-kl/Wavelet/pkg/migrator"
"github.com/Rain-kl/Wavelet/plugins/domain/admin"
"github.com/Rain-kl/Wavelet/plugins/domain/auth"
"github.com/Rain-kl/Wavelet/plugins/domain/cap"
@@ -25,6 +26,7 @@ import (
"github.com/Rain-kl/Wavelet/plugins/infra/database"
"github.com/Rain-kl/Wavelet/plugins/infra/logger"
"github.com/Rain-kl/Wavelet/plugins/infra/storage"
"github.com/pressly/goose/v3"
)
// newWaveletApp creates a core.App wired with Wavelet platform infrastructure, domain plugins, and profile drivers.
@@ -56,7 +58,7 @@ func newWaveletApp(profile core.Profile) *core.App {
system.New(),
)
// 3. Bind Goose migration engine (wraps pkg/migrator.Migrate)
// 3. Bind Goose migration engine
app.SetMigrationEngine(&gooseEngine{})
// 4. Mount runtime drivers for each aspect
@@ -69,11 +71,65 @@ func newWaveletApp(profile core.Profile) *core.App {
return app
}
// gooseEngine implements core.MigrationEngine by wrapping pkg/migrator.Migrate.
// It runs all centrally managed Goose SQL migrations during app startup.
// gooseEngine implements core.MigrationEngine by iterating all plugin-registered
// migration entries and applying each plugin's migrations against the shared DB.
//
// Each plugin owns its own `migrations/*.sql` directory, embedded via go:embed
// and registered via ctx.Migrations().Register(pluginID, embedFS). The engine
// resolves DBService from the IoC container and runs every entry in order.
type gooseEngine struct{}
func (e *gooseEngine) Migrate(_ context.Context, _ []core.MigrationEntry) error {
_ = migrator.Migrate()
func (e *gooseEngine) Migrate(ctx *core.Context, entries []core.MigrationEntry) error {
if len(entries) == 0 {
return nil
}
// Resolve DBService from the IoC container.
var dbSvc contracts.DBService
if err := core.Using[contracts.DBService](ctx, func(svc contracts.DBService) {
dbSvc = svc
}); err != nil {
return fmt.Errorf("migration: resolve DBService: %w", err)
}
gormDB := dbSvc.GORM()
if gormDB == nil {
return fmt.Errorf("migration: DBService.GORM() returned nil")
}
sqlDB, err := gormDB.DB()
if err != nil {
return fmt.Errorf("migration: get underlying %s from GORM: %w", dialectName(), err)
}
dialect := dialectName()
for _, entry := range entries {
log.Printf("[migrate] applying %s migrations (%s)", entry.PluginID, entry.Dir)
goose.SetBaseFS(entry.FS)
if err := goose.SetDialect(dialect); err != nil {
return fmt.Errorf("migration %s: set dialect %q: %w", entry.PluginID, dialect, err)
}
dir := entry.Dir
if dir == "" {
dir = "migrations"
}
if err := goose.Up(sqlDB, dir); err != nil {
return fmt.Errorf("migration %s: apply %w", entry.PluginID, err)
}
log.Printf("[migrate] %s migrations applied", entry.PluginID)
}
return nil
}
// dialectName returns the goose dialect based on the configured database engine.
func dialectName() string {
if !config.Config.Database.Enabled {
return "sqlite3"
}
return "postgres"
}
+1 -17
View File
@@ -14,14 +14,11 @@ import (
"github.com/Rain-kl/Wavelet/pkg/buildinfo"
"github.com/Rain-kl/Wavelet/pkg/config"
"github.com/Rain-kl/Wavelet/pkg/migrator"
)
//nolint:unused // startup banner formatting utilities
type startupState struct {
mode string
relationalDB migrator.Report
clickHouseDB migrator.Report
listensForHTTP bool
}
@@ -42,8 +39,6 @@ func formatStartupBanner(state startupState) string {
fmt.Sprintf(" Environment: %s", config.Config.App.Env),
fmt.Sprintf(" Runtime: %s/%s (%s)", runtime.GOOS, runtime.GOARCH, runtime.Version()),
fmt.Sprintf(" Build time: %s", buildTime()),
fmt.Sprintf(" Database: %s", formatMigration(state.relationalDB)),
fmt.Sprintf(" Analytics: %s", formatMigration(state.clickHouseDB)),
}
if state.listensForHTTP {
lines = append(lines, fmt.Sprintf(" Listening: http://%s", config.Config.App.Addr))
@@ -57,15 +52,4 @@ func buildTime() string {
return "development build"
}
return buildinfo.BuildTime
}
func formatMigration(report migrator.Report) string {
if !report.Enabled {
return "disabled"
}
state := "up to date"
if report.Applied {
state = "upgraded"
}
return fmt.Sprintf("%s (version %d, %s)", report.Backend, report.Version, state)
}
}
+2 -12
View File
@@ -9,7 +9,6 @@ import (
"github.com/Rain-kl/Wavelet/pkg/buildinfo"
"github.com/Rain-kl/Wavelet/pkg/config"
"github.com/Rain-kl/Wavelet/pkg/migrator"
)
func TestFormatStartupBanner(t *testing.T) {
@@ -30,14 +29,7 @@ func TestFormatStartupBanner(t *testing.T) {
config.Config.App.Addr = ":3000"
banner := formatStartupBanner(startupState{
mode: "API",
relationalDB: migrator.Report{
Backend: "PostgreSQL",
Enabled: true,
Version: 202607150003,
Applied: true,
},
clickHouseDB: migrator.Report{Backend: "ClickHouse"},
mode: "API",
listensForHTTP: true,
})
@@ -45,8 +37,6 @@ func TestFormatStartupBanner(t *testing.T) {
"Wavelet v3.2.1",
"Environment: production",
"Build time: 2026-07-13T08:00:00Z",
"Database: PostgreSQL (version 202607150003, upgraded)",
"Analytics: disabled",
"Listening: http://:3000",
"Mode: API",
} {
@@ -54,4 +44,4 @@ func TestFormatStartupBanner(t *testing.T) {
t.Errorf("banner missing %q:\n%s", want, banner)
}
}
}
}
+3 -4
View File
@@ -13,7 +13,6 @@ import (
"os"
"strings"
"github.com/Rain-kl/Wavelet/pkg/migrator"
userdomain "github.com/Rain-kl/Wavelet/plugins/domain/user"
"github.com/Rain-kl/Wavelet/plugins/infra/database"
@@ -46,12 +45,12 @@ func generateRandomPassword(length int) (string, error) {
var resetPasswdCmd = &cobra.Command{
Use: "reset-passwd",
Short: "重置指定账号密码",
PreRun: func(_ *cobra.Command, _ []string) {
migrator.Migrate()
},
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
// Ensure database is initialized
database.DB(ctx)
var username string
if usernameFlag != "" {
username = usernameFlag