MHSanaei/3x-ui · critical

upgrade panel schema for %T: %w

Error message

upgrade panel schema for %T: %w

What it means

Thrown by PrepareSQLiteForMigration when AutoMigrate(m) fails while upgrading the old SQLite backup to the current panel schema (so the row copy can read newer tables/columns), and the error is not an ignorable duplicate-column condition. This runs on the SQLite source copy before migration starts; a failure aborts safely with nothing written to Postgres.

Source

Thrown at internal/database/migrate_data.go:341

func PrepareSQLiteForMigration(dbPath string) error {
	gdb, err := gorm.Open(sqlite.Open(dbPath+"?_busy_timeout=10000"), &gorm.Config{Logger: logger.Discard})
	if err != nil {
		return err
	}
	sqlDB, err := gdb.DB()
	if err != nil {
		return err
	}
	defer sqlDB.Close()

	for _, table := range []string{"users", "settings", "inbounds"} {
		if !sqliteTableExists(sqlDB, table) {
			return fmt.Errorf("not a 3x-ui panel database: required table %q is missing", table)
		}
	}
	for _, m := range migrationModels() {
		if err := gdb.AutoMigrate(m); err != nil && !isIgnorableDuplicateColumnErr(gdb, err, m) {
			return fmt.Errorf("upgrade panel schema for %T: %w", m, err)
		}
	}
	return nil
}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Note the model %T in the message, open the backup with sqlite3, and compare that table's PRAGMA table_info against the model's expected columns/types.
  2. If the backup is several versions behind, first restore it into a temporary panel of an intermediate version, let it upgrade, back it up again, then migrate.
  3. Close other processes holding the file and retry.
  4. Keep an untouched copy of the backup — AutoMigrate mutates the source file, so always prepare a copy, never the only original.
Defensive patterns

Strategy: try-catch

Validate before calling

// snapshot the backup before PrepareSQLiteForMigration mutates it
cpSrc := dbPath + ".work"
if err := copyFile(dbPath, cpSrc); err != nil { return err }
if err := PrepareSQLiteForMigration(cpSrc); err != nil { /* original untouched */ }

Try / catch

err := PrepareSQLiteForMigration(cpPath)
if err != nil && strings.Contains(err.Error(), "upgrade panel schema") {
    // restore into an intermediate panel version first, re-backup, then retry with a fresh copy
}

Prevention

When it happens

Trigger: A backup from a panel version whose column types conflict with current models (e.g. an id or timestamp column stored with an incompatible type/affinity); a schema hand-edited at runtime; the file locked by another process; a duplicate-column error not matching isIgnorableDuplicateColumnErr's accepted shapes.

Common situations: Migrating backups skipped across many major versions (very old panel → current); DBs previously touched by forks or manual ALTERs; running the preparation while the file is open in a SQLite browser.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/8b40380cef4d1118. Report an issue: GitHub.