fix: skip startup sql migrations when not needed or no permissions

This commit is contained in:
Mikhail Chusavitin
2026-02-06 11:56:55 +03:00
parent 726dccb07c
commit 1744e6a3b8
2 changed files with 108 additions and 21 deletions

View File

@@ -2,6 +2,7 @@ package models
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
@@ -9,6 +10,7 @@ import (
"strings"
"time"
mysqlDriver "github.com/go-sql-driver/mysql"
"gorm.io/gorm"
)
@@ -22,6 +24,30 @@ func (SQLSchemaMigration) TableName() string {
return "qt_schema_migrations"
}
// NeedsSQLMigrations reports whether at least one SQL migration from migrationsDir
// is not yet recorded in qt_schema_migrations.
func NeedsSQLMigrations(db *gorm.DB, migrationsDir string) (bool, error) {
files, err := listSQLMigrationFiles(migrationsDir)
if err != nil {
return false, err
}
if len(files) == 0 {
return false, nil
}
// If tracking table does not exist yet, migrations are required.
if !db.Migrator().HasTable(&SQLSchemaMigration{}) {
return true, nil
}
var count int64
if err := db.Model(&SQLSchemaMigration{}).Where("filename IN ?", files).Count(&count).Error; err != nil {
return false, fmt.Errorf("check applied migrations: %w", err)
}
return count < int64(len(files)), nil
}
// RunSQLMigrations applies SQL files from migrationsDir once and records them in qt_schema_migrations.
// Local SQLite-only scripts are skipped automatically.
func RunSQLMigrations(db *gorm.DB, migrationsDir string) error {
@@ -29,27 +55,11 @@ func RunSQLMigrations(db *gorm.DB, migrationsDir string) error {
return fmt.Errorf("migrate qt_schema_migrations table: %w", err)
}
entries, err := os.ReadDir(migrationsDir)
files, err := listSQLMigrationFiles(migrationsDir)
if err != nil {
return fmt.Errorf("read migrations dir %s: %w", migrationsDir, err)
return err
}
files := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(strings.ToLower(name), ".sql") {
continue
}
if isSQLiteOnlyMigration(name) {
continue
}
files = append(files, name)
}
sort.Strings(files)
for _, filename := range files {
var count int64
if err := db.Model(&SQLSchemaMigration{}).Where("filename = ?", filename).Count(&count).Error; err != nil {
@@ -84,6 +94,37 @@ func RunSQLMigrations(db *gorm.DB, migrationsDir string) error {
return nil
}
// IsMigrationPermissionError returns true if err indicates insufficient privileges
// to create/alter/read migration metadata or target schema objects.
func IsMigrationPermissionError(err error) bool {
if err == nil {
return false
}
var mysqlErr *mysqlDriver.MySQLError
if errors.As(err, &mysqlErr) {
switch mysqlErr.Number {
case 1044, 1045, 1142, 1143, 1227:
return true
}
}
lower := strings.ToLower(err.Error())
patterns := []string{
"command denied to user",
"access denied for user",
"permission denied",
"insufficient privilege",
"sqlstate 42000",
}
for _, pattern := range patterns {
if strings.Contains(lower, pattern) {
return true
}
}
return false
}
func ensureSQLMigrationsTable(db *gorm.DB) error {
stmt := `
CREATE TABLE IF NOT EXISTS qt_schema_migrations (
@@ -157,3 +198,30 @@ func splitSQLStatements(script string) []string {
}
return stmts
}
func listSQLMigrationFiles(migrationsDir string) ([]string, error) {
entries, err := os.ReadDir(migrationsDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("read migrations dir %s: %w", migrationsDir, err)
}
files := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(strings.ToLower(name), ".sql") {
continue
}
if isSQLiteOnlyMigration(name) {
continue
}
files = append(files, name)
}
sort.Strings(files)
return files, nil
}