mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-08-30 14:31:58 -04:00
feat(migration): replace sqlx::migrate with standalone Go migrate tool
Schema is now managed by a separate Go CLI at tools/migrate/, which embeds
the SQL files copied from server/initialize/migrate/database/ and tracks
state in the shared schema_migrations table. Rust no longer owns migrations
at startup.
Rust startup:
1. Connect to DB
2. ALWAYS invoke `ppanel-migrate up` (idempotent — no-op if already at latest)
3. Create admin if not present
4. Continue with redis/queue/scheduler
Changes:
- Add tools/migrate/ — Go module wrapping golang-migrate
* migrate/migrate.go: Migrate() + RunUp() (RunUp bypasses the iofs vs
os.ErrNotExist sentinel mismatch that breaks Up() on already-migrated DBs)
* migrate/sql/{postgres,mysql}/: SQL files copied from server
* cmd/migrate/main.go: CLI (up/down/version/force/drop)
- Widen password column from varchar(100) → varchar(255) in
00001_init_schema.up.sql to fit Rust's 112-char PBKDF2 hash
- Rewrite src/migration.rs:
* Remove sqlx::migrate!, run_migrations(), schema_present()
* Add ensure_schema(cfg): unconditionally invokes ppanel-migrate up
- Fix src/db.rs::build_dsn: previously used cfg.config verbatim even when
the default config string was for the wrong dialect (PG got MySQL's
charset=utf8mb4 string and crashed on connect)
- Delete old migrations/{postgres,mysql}/ (no longer used)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// ppanel-migrate is a standalone CLI for managing PPanel database schema.
|
||||
//
|
||||
// It uses the migrate package (copied from server/initialize/migrate) which embeds
|
||||
// the SQL migration files and applies them via golang-migrate. Tracks state in the
|
||||
// schema_migrations table — the same table the Go server uses — so the Rust
|
||||
// ppanel-backend can attach to a database that was already initialised by either tool.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ppanel-migrate -driver=postgres -dsn="postgres://..." up
|
||||
// ppanel-migrate -driver=postgres -dsn="postgres://..." version
|
||||
// ppanel-migrate -driver=postgres -dsn="postgres://..." force 2131
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
gomigrate "github.com/golang-migrate/migrate/v4"
|
||||
|
||||
ppmigrate "github.com/perfect-panel/ppanel-backend/tools/migrate/migrate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
driver = flag.String("driver", "postgres", "Database driver: postgres | mysql")
|
||||
dsn = flag.String("dsn", "", "Database DSN. URL scheme (postgres:// | mysql://) is auto-prepended if absent.")
|
||||
verbose = flag.Bool("v", false, "Verbose logging")
|
||||
)
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "ppanel-migrate: standalone PPanel schema migration tool\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: ppanel-migrate -driver=<postgres|mysql> -dsn=<dsn> <command> [args]\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||
fmt.Fprintf(os.Stderr, " up [N] Apply all (or N) pending migrations\n")
|
||||
fmt.Fprintf(os.Stderr, " down [N] Roll back one (or N) migration\n")
|
||||
fmt.Fprintf(os.Stderr, " version Print current schema version\n")
|
||||
fmt.Fprintf(os.Stderr, " force <version> Mark database at <version> without running migrations\n")
|
||||
fmt.Fprintf(os.Stderr, " drop Drop every object in the database (dangerous)\n\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if *dsn == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if *verbose {
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
}
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
sess := ppmigrate.Migrate(*driver, *dsn)
|
||||
m := sess.Migrate
|
||||
|
||||
switch cmd {
|
||||
case "", "up":
|
||||
if err := ppmigrate.RunUp(sess); err != nil && !errors.Is(err, gomigrate.ErrNoChange) {
|
||||
log.Fatalf("up: %v", err)
|
||||
}
|
||||
reportVersion(m)
|
||||
|
||||
case "down":
|
||||
n := -parseStep(flag.Arg(1))
|
||||
if err := m.Steps(n); err != nil && !errors.Is(err, gomigrate.ErrNoChange) {
|
||||
log.Fatalf("down: %v", err)
|
||||
}
|
||||
reportVersion(m)
|
||||
|
||||
case "version":
|
||||
v, dirty, err := m.Version()
|
||||
if errors.Is(err, gomigrate.ErrNilVersion) {
|
||||
fmt.Println("no schema_migrations row (database is empty / not yet migrated)")
|
||||
os.Exit(0)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("version: %v", err)
|
||||
}
|
||||
fmt.Printf("version=%d dirty=%v\n", v, dirty)
|
||||
|
||||
case "force":
|
||||
v, err := strconv.Atoi(flag.Arg(1))
|
||||
if err != nil {
|
||||
log.Fatalf("force: bad version %q: %v", flag.Arg(1), err)
|
||||
}
|
||||
if err := m.Force(v); err != nil {
|
||||
log.Fatalf("force %d: %v", v, err)
|
||||
}
|
||||
fmt.Printf("forced to version %d\n", v)
|
||||
|
||||
case "drop":
|
||||
log.Println("WARNING: dropping all database objects")
|
||||
if err := m.Drop(); err != nil {
|
||||
log.Fatalf("drop: %v", err)
|
||||
}
|
||||
fmt.Println("dropped")
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func parseStep(s string) int {
|
||||
if s == "" {
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil || n <= 0 {
|
||||
log.Fatalf("step must be a positive integer, got %q", s)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func reportVersion(m *gomigrate.Migrate) {
|
||||
v, dirty, err := m.Version()
|
||||
if errors.Is(err, gomigrate.ErrNilVersion) {
|
||||
fmt.Println("no schema_migrations row (empty database)")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("version: %v", err)
|
||||
}
|
||||
fmt.Printf("schema_migrations: version=%d dirty=%v\n", v, dirty)
|
||||
}
|
||||
Reference in New Issue
Block a user