Make auto-migrations tolerant to duplicate DDL
This commit is contained in:
@@ -2,14 +2,18 @@ package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// EnsureSchema applies all "up" migrations when the database has no tables.
|
||||
func EnsureSchema(db *sql.DB, dir string) error {
|
||||
if err := ensureSchemaMigrations(db); err != nil {
|
||||
columnName, err := ensureSchemaMigrations(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,14 +32,14 @@ func EnsureSchema(db *sql.DB, dir string) error {
|
||||
if err := execStatements(db, migration.SQL); err != nil {
|
||||
return fmt.Errorf("apply %s: %w", migration.Name, err)
|
||||
}
|
||||
if err := recordMigration(db, migration.Name); err != nil {
|
||||
if err := recordMigration(db, columnName, migration.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
applied, err := loadAppliedMigrations(db)
|
||||
applied, err := loadAppliedMigrations(db, columnName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -46,7 +50,7 @@ func EnsureSchema(db *sql.DB, dir string) error {
|
||||
if err := execStatements(db, migration.SQL); err != nil {
|
||||
return fmt.Errorf("apply %s: %w", migration.Name, err)
|
||||
}
|
||||
if err := recordMigration(db, migration.Name); err != nil {
|
||||
if err := recordMigration(db, columnName, migration.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -70,23 +74,71 @@ func execStatements(db *sql.DB, sqlText string) error {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &mysqlErr) && (mysqlErr.Number == 1060 || mysqlErr.Number == 1061) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureSchemaMigrations(db *sql.DB) error {
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
func ensureSchemaMigrations(db *sql.DB) (string, error) {
|
||||
column, err := schemaMigrationsColumn(db)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if column != "" {
|
||||
return column, nil
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP NOT NULL
|
||||
)`)
|
||||
return err
|
||||
)`); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func loadAppliedMigrations(db *sql.DB) (map[string]bool, error) {
|
||||
rows, err := db.Query(`SELECT name FROM schema_migrations`)
|
||||
return "name", nil
|
||||
}
|
||||
|
||||
func schemaMigrationsColumn(db *sql.DB) (string, error) {
|
||||
var nameCount int
|
||||
if err := db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'schema_migrations'
|
||||
AND column_name = 'name'
|
||||
`).Scan(&nameCount); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if nameCount > 0 {
|
||||
return "name", nil
|
||||
}
|
||||
|
||||
var versionCount int
|
||||
if err := db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'schema_migrations'
|
||||
AND column_name = 'version'
|
||||
`).Scan(&versionCount); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if versionCount > 0 {
|
||||
return "version", nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func loadAppliedMigrations(db *sql.DB, column string) (map[string]bool, error) {
|
||||
query := fmt.Sprintf("SELECT %s FROM schema_migrations", column)
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -106,7 +158,8 @@ func loadAppliedMigrations(db *sql.DB) (map[string]bool, error) {
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
func recordMigration(db *sql.DB, name string) error {
|
||||
_, err := db.Exec(`INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?)`, name, time.Now().UTC())
|
||||
func recordMigration(db *sql.DB, column, name string) error {
|
||||
query := fmt.Sprintf("INSERT INTO schema_migrations (%s, applied_at) VALUES (?, ?)", column)
|
||||
_, err := db.Exec(query, name, time.Now().UTC())
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user