diff --git a/model/main.go b/model/main.go index f37cb667cd4..b171173cb57 100644 --- a/model/main.go +++ b/model/main.go @@ -248,6 +248,12 @@ func InitLogDB() (err error) { } func migrateDB() error { + // Repair missing columns in older schemas before AutoMigrate runs. + // AutoMigrate can fail when it introspects tables that reference columns + // not yet present; pre-creating them here avoids that race condition. + if err := repairSchema(); err != nil { + return err + } // Migrate price_amount column from float/double to decimal for existing tables migrateSubscriptionPlanPriceAmount() // Migrate model_limits column from varchar to text for existing tables diff --git a/model/schema_repair.go b/model/schema_repair.go new file mode 100644 index 00000000000..92b25e8f091 --- /dev/null +++ b/model/schema_repair.go @@ -0,0 +1,246 @@ +package model + +import ( + "fmt" + + "github.com/QuantumNous/new-api/common" +) + +// repairSchema explicitly adds columns that may be missing from older database +// schemas before AutoMigrate runs. AutoMigrate can fail when it tries to +// introspect existing tables that reference columns not yet present, so we +// pre-create them here with safe existence checks. +// +// This function is idempotent — it checks whether each column already exists +// before issuing any ALTER TABLE statement, so it is safe to run on every +// startup. +func repairSchema() error { + if common.UsingSQLite { + return repairSchemaSQLite() + } + if common.UsingPostgreSQL { + return repairSchemaPostgres() + } + if common.UsingMySQL { + return repairSchemaMySQL() + } + return nil +} + +// columnExistsPostgres returns true when the given column is present in the +// table (using the current schema). +func columnExistsPostgres(table, column string) (bool, error) { + var count int + err := DB.Raw( + `SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = ? + AND column_name = ?`, + table, column, + ).Scan(&count).Error + return count > 0, err +} + +// columnExistsMySQL returns true when the given column is present in the table. +func columnExistsMySQL(table, column string) (bool, error) { + var count int + err := DB.Raw( + `SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = ? + AND column_name = ?`, + table, column, + ).Scan(&count).Error + return count > 0, err +} + +// addColumnIfMissingPostgres issues ALTER TABLE … ADD COLUMN only when the +// column does not already exist. +func addColumnIfMissingPostgres(table, column, ddl string) error { + exists, err := columnExistsPostgres(table, column) + if err != nil { + return fmt.Errorf("repairSchema: failed to check %s.%s: %w", table, column, err) + } + if exists { + return nil + } + sql := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN %s %s`, table, column, ddl) + if err := DB.Exec(sql).Error; err != nil { + return fmt.Errorf("repairSchema: failed to add %s.%s: %w", table, column, err) + } + common.SysLog(fmt.Sprintf("repairSchema: added missing column %s.%s", table, column)) + return nil +} + +// addColumnIfMissingMySQL issues ALTER TABLE … ADD COLUMN only when the +// column does not already exist. +func addColumnIfMissingMySQL(table, column, ddl string) error { + exists, err := columnExistsMySQL(table, column) + if err != nil { + return fmt.Errorf("repairSchema: failed to check %s.%s: %w", table, column, err) + } + if exists { + return nil + } + sql := fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN %s %s", table, column, ddl) + if err := DB.Exec(sql).Error; err != nil { + return fmt.Errorf("repairSchema: failed to add %s.%s: %w", table, column, err) + } + common.SysLog(fmt.Sprintf("repairSchema: added missing column %s.%s", table, column)) + return nil +} + +// repairSchemaPostgres handles PostgreSQL-specific column repairs. +func repairSchemaPostgres() error { + type colDef struct { + table string + column string + ddl string // everything after the column name in ADD COLUMN + } + + repairs := []colDef{ + // prefill_groups.type — varchar(32), NOT NULL with a safe default so + // existing rows are not rejected. + { + table: "prefill_groups", + column: "type", + ddl: "varchar(32) NOT NULL DEFAULT ''", + }, + // tokens.type — integer column (mirrors older schema versions). + { + table: "tokens", + column: "type", + ddl: "integer NOT NULL DEFAULT 0", + }, + // tokens.models — text column (mirrors older schema versions). + { + table: "tokens", + column: "models", + ddl: "text NOT NULL DEFAULT ''", + }, + // channels.deleted_at — nullable timestamp used by GORM soft-delete. + { + table: "channels", + column: "deleted_at", + ddl: "timestamp with time zone", + }, + } + + for _, r := range repairs { + if !DB.Migrator().HasTable(r.table) { + // Table does not exist yet; AutoMigrate will create it from scratch. + continue + } + if err := addColumnIfMissingPostgres(r.table, r.column, r.ddl); err != nil { + return err + } + } + return nil +} + +// repairSchemaMySQL handles MySQL-specific column repairs. +func repairSchemaMySQL() error { + type colDef struct { + table string + column string + ddl string + } + + repairs := []colDef{ + { + table: "prefill_groups", + column: "type", + ddl: "varchar(32) NOT NULL DEFAULT ''", + }, + { + table: "tokens", + column: "type", + ddl: "int NOT NULL DEFAULT 0", + }, + { + table: "tokens", + column: "models", + ddl: "text NOT NULL DEFAULT ''", + }, + { + table: "channels", + column: "deleted_at", + ddl: "datetime(3)", + }, + } + + for _, r := range repairs { + if !DB.Migrator().HasTable(r.table) { + continue + } + if err := addColumnIfMissingMySQL(r.table, r.column, r.ddl); err != nil { + return err + } + } + return nil +} + +// repairSchemaSQLite handles SQLite-specific column repairs. +// SQLite's ALTER TABLE only supports ADD COLUMN, and it does not support +// information_schema, so we use PRAGMA table_info instead. +func repairSchemaSQLite() error { + type colDef struct { + table string + column string + ddl string // full column definition for ADD COLUMN + } + + repairs := []colDef{ + { + table: "prefill_groups", + column: "type", + ddl: "`type` varchar(32) NOT NULL DEFAULT ''", + }, + { + table: "tokens", + column: "type", + ddl: "`type` integer NOT NULL DEFAULT 0", + }, + { + table: "tokens", + column: "models", + ddl: "`models` text NOT NULL DEFAULT ''", + }, + { + table: "channels", + column: "deleted_at", + ddl: "`deleted_at` datetime", + }, + } + + for _, r := range repairs { + if !DB.Migrator().HasTable(r.table) { + continue + } + + // Query existing columns via PRAGMA. + var cols []struct { + Name string `gorm:"column:name"` + } + if err := DB.Raw("PRAGMA table_info(`" + r.table + "`)").Scan(&cols).Error; err != nil { + return fmt.Errorf("repairSchema: failed to inspect %s: %w", r.table, err) + } + found := false + for _, c := range cols { + if c.Name == r.column { + found = true + break + } + } + if found { + continue + } + + sql := fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN %s", r.table, r.ddl) + if err := DB.Exec(sql).Error; err != nil { + return fmt.Errorf("repairSchema: failed to add %s.%s: %w", r.table, r.column, err) + } + common.SysLog(fmt.Sprintf("repairSchema: added missing column %s.%s", r.table, r.column)) + } + return nil +}