Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions model/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
246 changes: 246 additions & 0 deletions model/schema_repair.go
Original file line number Diff line number Diff line change
@@ -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
}