Files
kra-new/internal/data/business/registration_index.go
T

80 lines
3.2 KiB
Go

package business
import (
"errors"
"fmt"
"strings"
"gorm.io/gorm"
)
// 保留旧 register_time 字符串及其 CAST 语义,用虚拟列索引避免逐行扫描主账号。
// MySQL 可将查询中相同的 CAST 表达式匹配到该索引,既有查询无需更改统计口径。
func migrateRegistrationIndex(db *gorm.DB) error {
if db.Dialector.Name() != "mysql" || !db.Migrator().HasTable("tab_user") {
return nil
}
const column = "sw_register_time_unix"
const index = "idx_sw_user_registration"
if !db.Migrator().HasColumn("tab_user", column) {
if err := db.Exec("ALTER TABLE tab_user ADD COLUMN sw_register_time_unix BIGINT UNSIGNED GENERATED ALWAYS AS (CAST(register_time AS UNSIGNED)) VIRTUAL, ALGORITHM=INPLACE, LOCK=NONE").Error; err != nil {
return err
}
} else {
var expression string
if err := db.Raw("SELECT generation_expression FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_user' AND column_name=?", column).Scan(&expression).Error; err != nil {
return err
}
normalized := strings.NewReplacer("`", "", " ", "", "\n", "", "\t", "").Replace(strings.ToLower(expression))
if normalized != "cast(register_timeasunsigned)" {
return fmt.Errorf("tab_user.%s 的现有定义与注册时间转换不符", column)
}
}
if !db.Migrator().HasIndex("tab_user", index) {
return addRegistrationIndex(db)
}
var columns []string
if err := db.Raw("SELECT column_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='tab_user' AND index_name=? ORDER BY seq_in_index", index).Scan(&columns).Error; err != nil {
return err
}
if strings.Join(columns, ",") != "source_db,puid,sw_register_time_unix,id" {
return fmt.Errorf("tab_user.%s 的现有索引列不符", index)
}
return nil
}
func addRegistrationIndex(db *gorm.DB) error {
return withLegacyRegistrationCast(db, func(conn *gorm.DB) error {
return conn.Exec("ALTER TABLE tab_user ADD INDEX idx_sw_user_registration(source_db,puid,sw_register_time_unix,id), ALGORITHM=INPLACE, LOCK=NONE").Error
})
}
func withLegacyRegistrationCast(db *gorm.DB, migrate func(*gorm.DB) error) error {
// 旧 SELECT CAST 会以警告方式接受尾部空格等历史值。建索引属于 DDL,
// 严格模式会把同一警告升级为错误;仅在固定连接的索引构建期间保持旧转换行为。
return db.Connection(func(conn *gorm.DB) (result error) {
// Keep the bound connection's base handle free of statement errors so
// restoration still executes after a failed ALTER TABLE.
fresh := func() *gorm.DB { return conn.Session(&gorm.Session{NewDB: true}) }
var previous string
if err := fresh().Raw("SELECT @@session.sql_mode").Scan(&previous).Error; err != nil {
return err
}
modes := []string{}
for _, mode := range strings.Split(previous, ",") {
if mode != "STRICT_TRANS_TABLES" && mode != "STRICT_ALL_TABLES" {
modes = append(modes, mode)
}
}
if err := fresh().Exec("SET SESSION sql_mode=?", strings.Join(modes, ",")).Error; err != nil {
return err
}
defer func() {
if err := fresh().Exec("SET SESSION sql_mode=?", previous).Error; err != nil {
result = errors.Join(result, err)
}
}()
return migrate(fresh())
})
}