Files
gochat/backend/internal/database/migrate.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

131 lines
3.6 KiB
Go

package database
import (
"errors"
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
// RunMigrations applies all pending database migrations from the given path.
// dbURL should be a database connection string (PostgreSQL or SQLite).
// migrationsPath should be a file path to the migrations directory (e.g. "migrations").
//
// Returns nil if all migrations were applied successfully, or an error if:
// - The migration source cannot be opened
// - The database connection fails
// - A migration fails to apply
// - The database is in a dirty state (a previous migration partially failed)
func RunMigrations(dbURL string, migrationsPath string) error {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
dbURL,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil {
if errors.Is(err, migrate.ErrNoChange) {
// No pending migrations — this is not an error
return nil
}
// Check for dirty state
version, dirty, dirtyErr := m.Version()
if dirtyErr == nil && dirty {
return fmt.Errorf("database is in dirty state at version %d; run 'migrate force %d' to fix before retrying: %w", version, version, err)
}
return fmt.Errorf("migration failed: %w", err)
}
return nil
}
// MigrateSteps applies N migration steps (positive = up, negative = down).
func MigrateSteps(dbURL string, migrationsPath string, steps int) error {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
dbURL,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
if err := m.Steps(steps); err != nil {
if errors.Is(err, migrate.ErrNoChange) {
return nil
}
return fmt.Errorf("migration steps failed: %w", err)
}
return nil
}
// RollbackMigrations rolls back all migrations (drops all tables).
func RollbackMigrations(dbURL string, migrationsPath string) error {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
dbURL,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
if err := m.Down(); err != nil {
if errors.Is(err, migrate.ErrNoChange) {
return nil
}
return fmt.Errorf("rollback failed: %w", err)
}
return nil
}
// ForceVersion sets the migration version to a specific number, clearing dirty state.
// Use this to recover from a partially-applied migration.
func ForceVersion(dbURL string, migrationsPath string, version int) error {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
dbURL,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
if err := m.Force(version); err != nil {
return fmt.Errorf("force version failed: %w", err)
}
return nil
}
// CurrentVersion returns the current migration version and whether the DB is in a dirty state.
// When no migrations have been applied (version 0), returns (0, false, nil) instead of an error.
func CurrentVersion(dbURL string, migrationsPath string) (uint, bool, error) {
m, err := migrate.New(
fmt.Sprintf("file://%s", migrationsPath),
dbURL,
)
if err != nil {
return 0, false, fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
version, dirty, err := m.Version()
if err != nil {
if errors.Is(err, migrate.ErrNilVersion) {
// No migrations applied yet — version 0, clean state
return 0, false, nil
}
return 0, false, fmt.Errorf("failed to get version: %w", err)
}
return version, dirty, nil
}