初始化项目
This commit is contained in:
96
migrations/001_initial_schema.go
Normal file
96
migrations/001_initial_schema.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterMigration(
|
||||
1,
|
||||
"Initial schema",
|
||||
migrateInitialSchemaUp,
|
||||
migrateInitialSchemaDown,
|
||||
)
|
||||
}
|
||||
|
||||
func migrateInitialSchemaUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS media_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
path TEXT NOT NULL,
|
||||
library_id INTEGER NOT NULL)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
artist_id INTEGER,
|
||||
FOREIGN KEY (artist_id) REFERENCES artists(id),
|
||||
UNIQUE(title, artist_id)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS songs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
album_id INTEGER,
|
||||
artist_id INTEGER,
|
||||
duration INTEGER,
|
||||
media_file_id INTEGER UNIQUE,
|
||||
FOREIGN KEY (album_id) REFERENCES albums(id),
|
||||
FOREIGN KEY (artist_id) REFERENCES artists(id),
|
||||
FOREIGN KEY (media_file_id) REFERENCES media_files(id)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateInitialSchemaDown(tx *sql.Tx) error {
|
||||
// Drop tables in reverse order to avoid foreign key constraints
|
||||
tables := []string{
|
||||
"songs",
|
||||
"albums",
|
||||
"artists",
|
||||
"media_files",
|
||||
"libraries",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
_, err := tx.Exec("DROP TABLE IF EXISTS " + table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
143
migrations/README.md
Normal file
143
migrations/README.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Database Migration System
|
||||
|
||||
This package provides a simple database migration system for Butterfliu. It allows you to:
|
||||
|
||||
- Create new migrations
|
||||
- Apply pending migrations
|
||||
- Roll back migrations
|
||||
- Check migration status
|
||||
|
||||
## Usage
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To run migrations, use the following command:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go up
|
||||
```
|
||||
|
||||
This will apply all pending migrations.
|
||||
|
||||
**Important:** Migrations must be run manually before starting the application. The application no longer automatically applies migrations on startup.
|
||||
|
||||
### Rolling Back Migrations
|
||||
|
||||
To roll back migrations to a specific version, use:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go down <version>
|
||||
```
|
||||
|
||||
For example, to roll back to version 1:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go down 1
|
||||
```
|
||||
|
||||
To roll back all migrations:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go reset
|
||||
```
|
||||
|
||||
### Checking Migration Status
|
||||
|
||||
To check the status of all migrations:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go status
|
||||
```
|
||||
|
||||
### Refreshing Migrations
|
||||
|
||||
To roll back all migrations and then apply them again:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go refresh
|
||||
```
|
||||
|
||||
### Creating a New Migration
|
||||
|
||||
To create a new migration:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go create <name>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go create add_user_table
|
||||
```
|
||||
|
||||
This will create a new migration file in the `migrations` directory.
|
||||
|
||||
## Auto-Migration
|
||||
|
||||
By default, auto-migration is **enabled**. The application will automatically apply all pending migrations on startup.
|
||||
|
||||
To disable auto-migration, set the `AUTO_MIGRATE` environment variable to `false`:
|
||||
|
||||
```bash
|
||||
# Disable auto-migration
|
||||
export AUTO_MIGRATE=false
|
||||
|
||||
# Then run the application
|
||||
go run main.go
|
||||
```
|
||||
|
||||
**Recommended usage:**
|
||||
- **Development:** Auto-migration is enabled by default for convenience
|
||||
- **Production:** Set `AUTO_MIGRATE=false` and run migrations manually using `go run cmd/migrate/main.go up` before deploying
|
||||
|
||||
## Migration File Structure
|
||||
|
||||
Each migration file should implement two functions:
|
||||
|
||||
- `Up`: Applies the migration
|
||||
- `Down`: Rolls back the migration
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterMigration(
|
||||
3,
|
||||
"Add user table",
|
||||
migrateAddUserTableUp,
|
||||
migrateAddUserTableDown,
|
||||
)
|
||||
}
|
||||
|
||||
func migrateAddUserTableUp(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func migrateAddUserTableDown(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`DROP TABLE IF EXISTS users`)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always provide both `Up` and `Down` functions
|
||||
2. Make migrations idempotent when possible
|
||||
3. Use transactions to ensure atomicity
|
||||
4. Keep migrations small and focused
|
||||
5. Test migrations before applying them to production
|
||||
205
migrations/cli.go
Normal file
205
migrations/cli.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"butterfliu/config"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// RunMigrationCLI runs the migration command-line interface
|
||||
func RunMigrationCLI() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
|
||||
// Load configuration
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
// Database file path - use configuration
|
||||
dbPath := cfg.DatabasePath
|
||||
log.Printf("Using database: %s", dbPath)
|
||||
|
||||
// Open database connection
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening database %s: %v", dbPath, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Test connection
|
||||
if err = db.Ping(); err != nil {
|
||||
log.Fatalf("Error connecting to database %s: %v", dbPath, err)
|
||||
}
|
||||
|
||||
// Initialize migration table
|
||||
if err = InitMigrationTable(db); err != nil {
|
||||
log.Fatalf("Error initializing migration table: %v", err)
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "up":
|
||||
// Run all pending migrations
|
||||
if err := MigrateUp(db); err != nil {
|
||||
log.Fatalf("Error applying migrations: %v", err)
|
||||
}
|
||||
fmt.Println("All migrations applied successfully")
|
||||
|
||||
case "down":
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Error: Missing target version")
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
targetVersion, err := strconv.Atoi(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing target version: %v", err)
|
||||
}
|
||||
|
||||
// Roll back migrations to target version
|
||||
if err := MigrateDown(db, targetVersion); err != nil {
|
||||
log.Fatalf("Error rolling back migrations: %v", err)
|
||||
}
|
||||
fmt.Printf("Migrations rolled back to version %d successfully\n", targetVersion)
|
||||
|
||||
case "reset":
|
||||
// Roll back all migrations
|
||||
if err := MigrateDown(db, 0); err != nil {
|
||||
log.Fatalf("Error rolling back migrations: %v", err)
|
||||
}
|
||||
fmt.Println("All migrations rolled back successfully")
|
||||
|
||||
case "refresh":
|
||||
// Roll back all migrations and then apply them again
|
||||
if err := MigrateDown(db, 0); err != nil {
|
||||
log.Fatalf("Error rolling back migrations: %v", err)
|
||||
}
|
||||
if err := MigrateUp(db); err != nil {
|
||||
log.Fatalf("Error applying migrations: %v", err)
|
||||
}
|
||||
fmt.Println("All migrations refreshed successfully")
|
||||
|
||||
case "status":
|
||||
// Show migration status
|
||||
migrations, err := ListMigrations(db)
|
||||
if err != nil {
|
||||
log.Fatalf("Error listing migrations: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Migration Status:")
|
||||
fmt.Println("=================")
|
||||
for _, migration := range migrations {
|
||||
status := "Pending"
|
||||
appliedAt := ""
|
||||
if applied, ok := migration["applied"].(bool); ok && applied {
|
||||
status = "Applied"
|
||||
appliedAt = migration["applied_at"].(string)
|
||||
}
|
||||
fmt.Printf("%d: %s - %s %s\n", migration["version"], migration["description"], status, appliedAt)
|
||||
}
|
||||
|
||||
case "create":
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Error: Missing migration name")
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := os.Args[2]
|
||||
if err := CreateMigration(name); err != nil {
|
||||
log.Fatalf("Error creating migration: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("Error: Unknown command '%s'\n", command)
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Usage: go run cmd/migrate/main.go <command> [args]")
|
||||
fmt.Println("")
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" up Apply all pending migrations")
|
||||
fmt.Println(" down <version> Roll back migrations to specified version")
|
||||
fmt.Println(" reset Roll back all migrations")
|
||||
fmt.Println(" refresh Roll back all migrations and apply them again")
|
||||
fmt.Println(" status Show migration status")
|
||||
fmt.Println(" create <name> Create a new migration")
|
||||
}
|
||||
|
||||
func CreateMigration(name string) error {
|
||||
// Get next version number
|
||||
allMigrations := getMigrations()
|
||||
nextVersion := 1
|
||||
for _, migration := range allMigrations {
|
||||
if migration.Version >= nextVersion {
|
||||
nextVersion = migration.Version + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Create migration file - use absolute path from current working directory
|
||||
filename := fmt.Sprintf("%03d_%s.go", nextVersion, name)
|
||||
// Get the absolute path to the migrations directory
|
||||
migrationDir, err := filepath.Abs("migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get migrations directory path: %v", err)
|
||||
}
|
||||
migrationPath := filepath.Join(migrationDir, filename)
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(migrationPath); err == nil {
|
||||
return fmt.Errorf("migration file already exists: %s", migrationPath)
|
||||
}
|
||||
|
||||
// Create migration file content
|
||||
content := fmt.Sprintf(`package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterMigration(
|
||||
%d,
|
||||
"%s",
|
||||
migrate%sUp,
|
||||
migrate%sDown,
|
||||
)
|
||||
}
|
||||
|
||||
func migrate%sUp(tx *sql.Tx) error {
|
||||
// TODO: Implement migration up
|
||||
_, err := tx.Exec(`+"`"+`
|
||||
-- Add your SQL here
|
||||
`+"`"+`)
|
||||
return err
|
||||
}
|
||||
|
||||
func migrate%sDown(tx *sql.Tx) error {
|
||||
// TODO: Implement migration down
|
||||
_, err := tx.Exec(`+"`"+`
|
||||
-- Add your SQL here
|
||||
`+"`"+`)
|
||||
return err
|
||||
}
|
||||
`, nextVersion, name, name, name, name, name)
|
||||
|
||||
// Write file
|
||||
if err := os.WriteFile(migrationPath, []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Created migration file: %s\n", migrationPath)
|
||||
return nil
|
||||
}
|
||||
218
migrations/migrations.go
Normal file
218
migrations/migrations.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Migration represents a database migration
|
||||
type Migration struct {
|
||||
Version int
|
||||
Description string
|
||||
Up func(*sql.Tx) error
|
||||
Down func(*sql.Tx) error
|
||||
}
|
||||
|
||||
var (
|
||||
migrations = []Migration{}
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterMigration adds a migration to the list of available migrations
|
||||
func RegisterMigration(version int, description string, up, down func(*sql.Tx) error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
migrations = append(migrations, Migration{
|
||||
Version: version,
|
||||
Description: description,
|
||||
Up: up,
|
||||
Down: down,
|
||||
})
|
||||
|
||||
// Sort migrations by version
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].Version < migrations[j].Version
|
||||
})
|
||||
}
|
||||
|
||||
// getMigrations returns a copy of the migrations list (thread-safe)
|
||||
func getMigrations() []Migration {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
result := make([]Migration, len(migrations))
|
||||
copy(result, migrations)
|
||||
return result
|
||||
}
|
||||
|
||||
// InitMigrationTable creates the schema_migrations table if it doesn't exist
|
||||
func InitMigrationTable(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCurrentVersion returns the current database schema version
|
||||
func GetCurrentVersion(db *sql.DB) (int, error) {
|
||||
var version int
|
||||
err := db.QueryRow(`
|
||||
SELECT COALESCE(MAX(version), 0) FROM schema_migrations
|
||||
`).Scan(&version)
|
||||
return version, err
|
||||
}
|
||||
|
||||
// MigrateUp applies all pending migrations
|
||||
func MigrateUp(db *sql.DB) error {
|
||||
// Get current version
|
||||
currentVersion, err := GetCurrentVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply pending migrations
|
||||
allMigrations := getMigrations()
|
||||
for _, migration := range allMigrations {
|
||||
if migration.Version > currentVersion {
|
||||
log.Printf("Applying migration %d: %s", migration.Version, migration.Description)
|
||||
|
||||
// Begin transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply migration
|
||||
if err := migration.Up(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("error applying migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Record migration
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)",
|
||||
migration.Version, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("error recording migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("error committing migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully applied migration %d", migration.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateDown rolls back migrations to the specified version
|
||||
// If targetVersion is 0, rolls back all migrations
|
||||
func MigrateDown(db *sql.DB, targetVersion int) error {
|
||||
// Get current version
|
||||
currentVersion, err := GetCurrentVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get a copy of migrations and sort in reverse order
|
||||
allMigrations := getMigrations()
|
||||
sort.Slice(allMigrations, func(i, j int) bool {
|
||||
return allMigrations[i].Version > allMigrations[j].Version
|
||||
})
|
||||
|
||||
// Roll back migrations
|
||||
for _, migration := range allMigrations {
|
||||
if migration.Version <= currentVersion && migration.Version > targetVersion {
|
||||
log.Printf("Rolling back migration %d: %s", migration.Version, migration.Description)
|
||||
|
||||
// Begin transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply down migration
|
||||
if err := migration.Down(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("error rolling back migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Remove migration record
|
||||
_, err = tx.Exec(
|
||||
"DELETE FROM schema_migrations WHERE version = ?",
|
||||
migration.Version,
|
||||
)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("error removing migration record %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("error committing rollback of migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully rolled back migration %d", migration.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListMigrations returns a list of all available migrations and their status
|
||||
func ListMigrations(db *sql.DB) ([]map[string]interface{}, error) {
|
||||
// Get current version (unused but kept for reference)
|
||||
_, err := GetCurrentVersion(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get applied migrations with timestamps
|
||||
rows, err := db.Query("SELECT version, applied_at FROM schema_migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
appliedMigrations := make(map[int]time.Time)
|
||||
for rows.Next() {
|
||||
var version int
|
||||
var appliedAt time.Time
|
||||
if err := rows.Scan(&version, &appliedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appliedMigrations[version] = appliedAt
|
||||
}
|
||||
|
||||
// Create result
|
||||
var result []map[string]interface{}
|
||||
allMigrations := getMigrations()
|
||||
for _, migration := range allMigrations {
|
||||
appliedAt, applied := appliedMigrations[migration.Version]
|
||||
appliedAtStr := ""
|
||||
if applied {
|
||||
appliedAtStr = appliedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
result = append(result, map[string]interface{}{
|
||||
"version": migration.Version,
|
||||
"description": migration.Description,
|
||||
"applied": applied,
|
||||
"applied_at": appliedAtStr,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user