3x-ui/docs/superpowers/specs/2026-04-03-mariadb-support-design.md
root f5862abc2e feat: add CodeMirror YAML editor for Clash template and fix settings save button bug
- Replace plain textarea with CodeMirror editor (YAML syntax highlighting, line numbers, auto-indent) for Clash subscription template
- Fix confAlerts crash when subClashURI/subURI/subJsonURI is null/undefined (prevented save button from enabling)
- Add yaml.js CodeMirror mode asset
- Include docs and .gitignore cleanup
2026-04-24 16:15:22 +08:00

11 KiB
Raw Blame History

MariaDB Support for 3x-ui

Summary

Add MariaDB as an alternative database backend to SQLite. Users switch between SQLite and MariaDB via the x-ui.sh management script (option 27). Data is automatically migrated during the switch. MariaDB connection credentials are stored in /etc/x-ui/x-ui.json.

Requirements

  • Support both SQLite and MariaDB as database backends
  • Switch via x-ui.sh with interactive prompts for MariaDB credentials (IP, port, username, password, database name)
  • Auto-migrate data when switching between SQLite and MariaDB
  • Keep old database as backup after migration
  • MariaDB has core feature parity (CRUD, migrations, seeders) but skips SQLite-specific features (WAL checkpoint, file export/import)
  • Credentials stored in /etc/x-ui/x-ui.json

Architecture: Approach A — Driver-agnostic InitDB

Refactor database.InitDB() to read config from the JSON settings file, determine the driver type, and open the appropriate GORM connection. The package-level var db *gorm.DB singleton stays unchanged — all callers continue using database.GetDB().


Section 1: Configuration

New settings in web/service/setting.go

Add to defaultValueMap:

Key Default Description
dbType "sqlite" "sqlite" or "mariadb"
dbHost "127.0.0.1" MariaDB host
dbPort "3306" MariaDB port
dbUser "" MariaDB username
dbPassword "" MariaDB password
dbName "3xui" MariaDB database name

Add getter/setter methods: GetDBType(), SetDBType(), GetDBHost(), SetDBHost(), GetDBPort(), SetDBPort(), GetDBUser(), SetDBUser(), GetDBPassword(), SetDBPassword(), GetDBName(), SetDBName().

Config reading before DB init

Problem: settings are stored IN the database, but we need dbType BEFORE opening the DB.

Solution: config/config.go gets a GetDBTypeFromJSON() function that reads /etc/x-ui/x-ui.json directly (falls back to "sqlite" if file doesn't exist or key is missing). This is called before database.InitDB().

New CLI flags in main.go

Add -dbType, -dbHost, -dbPort, -dbUser, -dbPassword, -dbName flags to the setting subcommand. These write directly to the JSON config file (not via the DB) using config.WriteSettingToJSON(key, value).

New config/config.go helper: WriteSettingToJSON(key, value string) — reads the JSON file, updates the key, writes back.


Section 2: Database Layer (database/db.go)

Refactored InitDB()

func InitDB() error {
    dbType := config.GetDBTypeFromJSON()

    switch dbType {
    case "mariadb":
        return initMariaDB()
    default: // "sqlite"
        return initSQLite(config.GetDBPath())
    }
}

initSQLite(path string) error

Existing logic unchanged — opens SQLite with gorm.io/driver/sqlite, runs initModels(), initUser(), runSeeders().

initMariaDB() error

  1. Read host, port, user, password, dbName from JSON config.
  2. Build DSN: user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
  3. Open with gorm.io/driver/mysql.
  4. Run initModels(), initUser(), runSeeders() (same as SQLite).

Adapted functions

  • Checkpoint() — if MariaDB, return nil. If SQLite, existing WAL logic.
  • IsSQLiteDB() — unchanged, only called for SQLite.
  • ValidateSQLiteDB() — unchanged, only called for SQLite.

New dependency

gorm.io/driver/mysql added to go.mod.


Section 3: Data Migration (database/migrate.go)

New file with two functions:

MigrateSQLiteToMariaDB() error

  1. Open SQLite connection from config.GetDBPath().
  2. Open MariaDB connection from JSON settings.
  3. For each table (users, inbounds, outbound_traffics, settings, inbound_client_ips, client_traffics, history_of_seeders):
    • AutoMigrate the model on MariaDB.
    • SELECT * from SQLite → INSERT into MariaDB using GORM raw SQL.
  4. On success: close connections (SQLite file kept as backup).
  5. On failure: return error with context.

MigrateMariaDBToSQLite() error

Reverse of above:

  1. Open MariaDB connection from JSON settings.
  2. Open/create SQLite connection at config.GetDBPath().
  3. For each table: read from MariaDB, write to SQLite.
  4. On success: close connections.
  5. On failure: return error.

Row transfer approach: Use the existing model structs explicitly. For each table, query all rows from source into a []Model slice, then batch-insert into destination. This avoids raw SQL differences between SQLite and MySQL. Example for users:

var users []model.User
srcDB.Find(&users)
dstDB.CreateInBatches(&users, 100)

This pattern repeats for each of the 7 tables.


Section 4: main.go Changes

Updated callers

All database.InitDB(config.GetDBPath()) calls change to database.InitDB():

  • runWebServer() (line 49)
  • resetSetting() (line 134)
  • updateTgbotSetting() (line 221)
  • updateSetting() (line 259)
  • updateCert() (line 318)
  • migrateDb() (line 395)

New migrate-db subcommand

case "migrate-db":
    migrateDbBetweenDrivers()

migrateDbBetweenDrivers():

  1. Read dbType from JSON config.
  2. If dbType == "mariadb": call database.MigrateSQLiteToMariaDB().
  3. If dbType == "sqlite": call database.MigrateMariaDBToSQLite().
  4. Print success/failure message.

New CLI flags

Add to setting subcommand:

  • -dbType string — set database type
  • -dbHost string — set MariaDB host
  • -dbPort string — set MariaDB port
  • -dbUser string — set MariaDB username
  • -dbPassword string — set MariaDB password
  • -dbName string — set MariaDB database name

These call config.WriteSettingToJSON() to write directly to the JSON file. Only the 6 DB-related settings use WriteSettingToJSON() — all other settings (port, username, etc.) continue to use the existing SettingService methods that write through the database.


Section 5: web/service/server.go Changes

GetDb()

Add check at the top:

dbType, _ := s.GetDBType()
if dbType == "mariadb" {
    return nil, common.NewError("Database export is not supported for MariaDB")
}

Existing SQLite logic unchanged.

ImportDB()

Add check at the top:

dbType, _ := s.GetDBType()
if dbType == "mariadb" {
    return common.NewError("Database import is not supported for MariaDB")
}

Existing SQLite logic unchanged.


Section 6: x-ui.sh Changes

New menu option 27

Add to show_menu:

│────────────────────────────────────────────────│
│  ${green}27.${plain} 数据库管理                                │

Add to the case statement:

27)
    check_install && db_menu
    ;;

Update prompt: 请输入选择 [0-27]

db_menu() function

db_menu() {
    # Read current dbType from JSON
    local current_type=$(read_json_dbtype)

    echo -e "
╔────────────────────────────────────────────────╗
${green}数据库管理${plain}${green}0.${plain} 返回主菜单                                │
${green}1.${plain} 查看当前数据库类型(当前: ${current_type}${green}2.${plain} 切换到 MariaDB                             │
${green}3.${plain} 切换到 SQLite                               │
╚────────────────────────────────────────────────╝
"
    read -rp "请输入选择 [0-3]" num
    case "${num}" in
    0) show_menu ;;
    1) db_show_status && db_menu ;;
    2) db_switch_to_mariadb ;;
    3) db_switch_to_sqlite ;;
    *) echo "无效选项" && db_menu ;;
    esac
}

db_switch_to_mariadb()

db_switch_to_mariadb() {
    echo "请输入 MariaDB 连接信息(直接回车使用默认值):"

    read -rp "MariaDB IP默认 127.0.0.1: " db_host
    db_host=${db_host:-127.0.0.1}

    read -rp "MariaDB 端口(默认 3306: " db_port
    db_port=${db_port:-3306}

    read -rp "MariaDB 用户名: " db_user
    if [ -z "$db_user" ]; then
        echo -e "${red}用户名不能为空${plain}"
        db_menu
        return
    fi

    read -rsp "MariaDB 密码: " db_pass
    echo
    if [ -z "$db_pass" ]; then
        echo -e "${red}密码不能为空${plain}"
        db_menu
        return
    fi

    read -rp "数据库名(默认 3xui: " db_name
    db_name=${db_name:-3xui}

    # Write settings to JSON config
    /usr/local/x-ui/x-ui setting -dbType mariadb -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbPassword "$db_pass" -dbName "$db_name"

    # Migrate data
    echo "正在迁移数据从 SQLite 到 MariaDB..."
    /usr/local/x-ui/x-ui migrate-db

    if [ $? -eq 0 ]; then
        echo -e "${green}数据库切换成功,正在重启面板...${plain}"
        restart
    else
        echo -e "${red}数据迁移失败,正在回滚到 SQLite...${plain}"
        /usr/local/x-ui/x-ui setting -dbType sqlite
        restart
    fi
}

db_switch_to_sqlite()

db_switch_to_sqlite() {
    /usr/local/x-ui/x-ui setting -dbType sqlite

    echo "正在迁移数据从 MariaDB 到 SQLite..."
    /usr/local/x-ui/x-ui migrate-db

    if [ $? -eq 0 ]; then
        echo -e "${green}数据库切换成功,正在重启面板...${plain}"
        restart
    else
        echo -e "${red}数据迁移失败${plain}"
    fi
}

Helper functions in x-ui.sh

  • read_json_dbtype() — reads dbType from /etc/x-ui/x-ui.json using grep/sed or Python if available.
  • db_show_status() — displays current DB type and connection info.

Files Changed

File Changes
go.mod Add gorm.io/driver/mysql
config/config.go Add GetDBTypeFromJSON(), WriteSettingToJSON()
database/db.go Refactor InitDB() to be driver-agnostic, add initMariaDB(), adapt Checkpoint()
database/migrate.go New fileMigrateSQLiteToMariaDB(), MigrateMariaDBToSQLite()
main.go Update all InitDB calls, add migrate-db subcommand, add setting CLI flags
web/service/setting.go Add 6 new settings + getter/setter methods
web/service/server.go Guard GetDb()/ImportDB() for MariaDB
x-ui.sh Add option 27, db_menu(), db_switch_to_mariadb(), db_switch_to_sqlite(), helpers

Testing

  1. Fresh install with SQLite (default) — verify panel works as before
  2. Switch to MariaDB via x-ui.sh — verify data migrates and panel starts
  3. Switch back to SQLite — verify data migrates back
  4. Verify MariaDB CRUD operations (create inbound, modify settings, etc.)
  5. Verify GetDb/ImportDB return appropriate errors when using MariaDB
  6. Verify invalid MariaDB credentials show error and rollback to SQLite