fix: validate explicit migrations and preserve large business ids
This commit is contained in:
@@ -55,16 +55,7 @@ func migrateAlertV2(db *gorm.DB) error {
|
||||
if err := migrateAlertV2UniqueKey(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateQueryIndexes(db, []businessQueryIndex{
|
||||
{"sys_alert_record", "idx_alert_owner_status_time", []string{"business_id", "status", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_channel_type_time", []string{"platform_code", "target_promote_id", "rule_type", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_owner_time", []string{"business_id", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_created", []string{"created_at", "id"}},
|
||||
{"sys_alert_operation_log", "idx_alert_log_time", []string{"alert_id", "created_at", "id"}},
|
||||
{"tab_spend", "idx_alert_spend_window", []string{"source_db", "promote_id", "pay_status", "pay_time", "pay_amount"}},
|
||||
{"tab_user", "idx_alert_user_window", []string{"source_db", "promote_id", "sw_register_time_unix", "puid"}},
|
||||
{"tab_promote", "idx_alert_promote_tree", []string{"source_db", "top_promote_id", "id"}},
|
||||
})
|
||||
return migrateQueryIndexes(db, alertV2QueryIndexes)
|
||||
}
|
||||
|
||||
func migrateAlertV2UniqueKey(db *gorm.DB) error {
|
||||
@@ -131,3 +122,14 @@ func migrateAlertV2UniqueKey(db *gorm.DB) error {
|
||||
}
|
||||
return db.Exec("ALTER TABLE sys_alert_record " + strings.Join(clauses, ", ") + ", ALGORITHM=INPLACE, LOCK=NONE").Error
|
||||
}
|
||||
|
||||
var alertV2QueryIndexes = []businessQueryIndex{
|
||||
{"sys_alert_record", "idx_alert_owner_status_time", []string{"business_id", "status", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_channel_type_time", []string{"platform_code", "target_promote_id", "rule_type", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_owner_time", []string{"business_id", "created_at", "id"}},
|
||||
{"sys_alert_record", "idx_alert_created", []string{"created_at", "id"}},
|
||||
{"sys_alert_operation_log", "idx_alert_log_time", []string{"alert_id", "created_at", "id"}},
|
||||
{"tab_spend", "idx_alert_spend_window", []string{"source_db", "promote_id", "pay_status", "pay_time", "pay_amount"}},
|
||||
{"tab_user", "idx_alert_user_window", []string{"source_db", "promote_id", "sw_register_time_unix", "puid"}},
|
||||
{"tab_promote", "idx_alert_promote_tree", []string{"source_db", "top_promote_id", "id"}},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func businessDisplayPaths(db *gorm.DB, owners map[uint]string) (map[uint][]strin
|
||||
for owner, path := range owners {
|
||||
seen := map[uint]bool{}
|
||||
for _, part := range strings.Split(path, ",") {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32)
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, strconv.IntSize)
|
||||
if err == nil && id > 0 && uint(id) != owner && !seen[uint(id)] {
|
||||
paths[owner] = append(paths[owner], uint(id))
|
||||
ids[uint(id)], seen[uint(id)] = true, true
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func migrateBindingStatusIndex(db *gorm.DB) error {
|
||||
const name = "idx_sys_user_bind_platform_status"
|
||||
if !db.Migrator().HasTable(&bindingPO{}) {
|
||||
return nil
|
||||
}
|
||||
if db.Migrator().HasIndex(&bindingPO{}, name) {
|
||||
return nil
|
||||
}
|
||||
return db.Migrator().CreateIndex(&bindingPO{}, name)
|
||||
}
|
||||
|
||||
// Preserve IDs and the high-water mark: lowering AUTO_INCREMENT could reuse
|
||||
// a deleted ID that still appears in historical records or external systems.
|
||||
func migratePromoteIDCapacity(db *gorm.DB) error {
|
||||
if db.Dialector.Name() != "mysql" || !db.Migrator().HasTable("tab_promote") {
|
||||
return nil
|
||||
}
|
||||
var column struct {
|
||||
DataType, ColumnType, IsNullable, Extra, ColumnComment string
|
||||
}
|
||||
if err := db.Raw("SELECT data_type,column_type,is_nullable,extra,column_comment FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&column).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if column.DataType == "bigint" {
|
||||
return nil
|
||||
}
|
||||
if column.DataType != "int" || column.IsNullable != "NO" || column.Extra != "auto_increment" {
|
||||
return fmt.Errorf("unexpected tab_promote.id definition; capacity migration stopped")
|
||||
}
|
||||
var next uint64
|
||||
if err := db.Raw("SELECT auto_increment FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='tab_promote'").Scan(&next).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
typeName := "BIGINT"
|
||||
if strings.Contains(column.ColumnType, "unsigned") {
|
||||
typeName += " UNSIGNED"
|
||||
}
|
||||
// MySQL 5.7 rebuilds the table when widening an integer. Execute before
|
||||
// starting writers and retain the original next ID and column comment.
|
||||
return db.Connection(func(conn *gorm.DB) error {
|
||||
var mode string
|
||||
if err := conn.Raw("SELECT @@session.sql_mode").Scan(&mode).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
comment := column.ColumnComment
|
||||
if !strings.Contains(mode, "NO_BACKSLASH_ESCAPES") {
|
||||
comment = strings.ReplaceAll(comment, `\`, `\\`)
|
||||
}
|
||||
comment = "'" + strings.ReplaceAll(comment, "'", "''") + "'"
|
||||
// MySQL does not permit a prepared-statement placeholder in COMMENT.
|
||||
return conn.Session(&gorm.Session{NewDB: true}).Exec(fmt.Sprintf("ALTER TABLE tab_promote MODIFY COLUMN id %s NOT NULL AUTO_INCREMENT COMMENT %s, AUTO_INCREMENT=%d", typeName, comment, next)).Error
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMySQLPromoteCapacityPreservesIDsAndNextValue(t *testing.T) {
|
||||
db := openRehearsalMySQL(t)
|
||||
if err := db.Exec("CREATE TABLE tab_promote(id INT NOT NULL AUTO_INCREMENT COMMENT '渠道主键',source_db VARCHAR(10) NOT NULL,PRIMARY KEY(id,source_db)) AUTO_INCREMENT=2147483647").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO tab_promote(id,source_db) VALUES(29701,'lx'),(29701,'hw')").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := migratePromoteIDCapacity(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var next uint64
|
||||
if err := db.Raw("SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='tab_promote'").Scan(&next).Error; err != nil || next != 2147483647 {
|
||||
t.Fatalf("high-water mark changed: %d, %v", next, err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO tab_promote(source_db) VALUES('lx'),('lx')").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ids []uint64
|
||||
if err := db.Table("tab_promote").Order("id").Pluck("id", &ids).Error; err != nil || len(ids) != 4 || ids[0] != 29701 || ids[1] != 29701 || ids[2] != 2147483647 || ids[3] != 2147483648 {
|
||||
t.Fatalf("IDs changed or overflowed: %v, %v", ids, err)
|
||||
}
|
||||
var comment string
|
||||
if err := db.Raw("SELECT column_comment FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&comment).Error; err != nil || comment != "渠道主键" {
|
||||
t.Fatalf("comment changed: %q, %v", comment, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingStatusIndexRepairsAnExistingLegacyTable(t *testing.T) {
|
||||
db := openRehearsalMySQL(t)
|
||||
const index = "idx_sys_user_bind_platform_status"
|
||||
if err := db.Migrator().DropIndex(&bindingPO{}, index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := migrateBindingStatusIndex(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !db.Migrator().HasIndex(&bindingPO{}, index) {
|
||||
t.Fatal("legacy status index was not created")
|
||||
}
|
||||
}
|
||||
@@ -385,7 +385,7 @@ func fillLedgerNames(db *gorm.DB, scope business.LedgerScope, filter business.Le
|
||||
paths := map[business.PlatformKey][]uint{}
|
||||
for key, binding := range names {
|
||||
for _, part := range strings.Split(binding.IDPath, ",") {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32)
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(part), 10, strconv.IntSize)
|
||||
if err == nil && id > 0 && uint(id) != binding.UserID {
|
||||
paths[key] = append(paths[key], uint(id))
|
||||
ancestorIDs[uint(id)] = true
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"kra/pkg/database/migration"
|
||||
)
|
||||
|
||||
// These source tables carry history. An incomplete copy must not be replaced
|
||||
// with empty tables. SDK and OA schemas belong to their respective databases.
|
||||
var importedTables = []string{
|
||||
"tab_game", "tab_game_server", "tab_promote", "tab_promote_apply",
|
||||
"tab_spend", "tab_user", "tab_user_day_login", "tab_user_play_info",
|
||||
"tab_sw_report_day", "tab_sw_report_day_key", "tab_sw_report_server_day", "tab_sw_report_server_day_key",
|
||||
}
|
||||
|
||||
func CheckPrerequisites(db *gorm.DB) error {
|
||||
if db.Dialector.Name() != "mysql" {
|
||||
return nil
|
||||
}
|
||||
if db.Migrator().HasTable("sys_user") && !db.Migrator().HasTable("sys_users") {
|
||||
return fmt.Errorf("legacy framework conversion must run before incremental migrations")
|
||||
}
|
||||
var missing []string
|
||||
for _, table := range importedTables {
|
||||
if !db.Migrator().HasTable(table) {
|
||||
missing = append(missing, table)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("incomplete legacy copy, missing tables: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckSchema(db *gorm.DB) error {
|
||||
if err := CheckPrerequisites(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migration.CheckModels(db,
|
||||
&preferencePO{}, &bindingPO{}, &bindingLockPO{}, &resetCodePO{}, &resetTokenPO{},
|
||||
&alertRulePO{}, &alertRecordPO{}, &alertLogPO{}, &alertGapPO{}, &alertExemptionPO{}, &alertChannelLockPO{},
|
||||
&alertTriggerLockPO{}, &reportBuildLockPO{}, &businessGroupPO{}, &businessGroupMemberPO{},
|
||||
&businessMemberProfilePO{}, &businessGroupStatePO{}, &performanceGoalPO{}, &effectivenessStatePO{}, &effectivenessPO{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if db.Dialector.Name() != "mysql" {
|
||||
return nil
|
||||
}
|
||||
indexes := append([]businessQueryIndex{}, businessQueryIndexes...)
|
||||
var idType string
|
||||
if err := db.Raw("SELECT data_type FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&idType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if idType != "bigint" {
|
||||
return fmt.Errorf("tab_promote.id must be BIGINT after capacity migration")
|
||||
}
|
||||
indexes = append(indexes, reportBuildQueryIndexes...)
|
||||
indexes = append(indexes, alertV2QueryIndexes...)
|
||||
indexes = append(indexes,
|
||||
businessQueryIndex{"tab_spend", "idx_sw_spend_effective_scan", []string{"promote_id", "source_db", "pay_status", "pay_time", "id", "pay_amount"}},
|
||||
businessQueryIndex{"tab_promote_apply", "idx_sw_apply_game_scope", []string{"game_id", "source_db", "status", "promote_id"}},
|
||||
businessQueryIndex{"tab_user", "idx_sw_user_registration", []string{"source_db", "puid", "sw_register_time_unix", "id"}},
|
||||
businessQueryIndex{"tab_user_play_info", "idx_sw_play_create_time", []string{"create_time"}},
|
||||
businessQueryIndex{"sys_alert_record", "uk_alert_record_v2_open", []string{"business_id", "platform_code", "target_promote_id", "rule_type", "v2_open_key"}},
|
||||
)
|
||||
for _, index := range indexes {
|
||||
var existing []struct {
|
||||
ColumnName string
|
||||
NonUnique int
|
||||
SubPart *int
|
||||
}
|
||||
if err := db.Raw("SELECT column_name,non_unique,sub_part FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND index_name=? ORDER BY seq_in_index", index.table, index.name).Scan(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
columns := []string{}
|
||||
for _, field := range existing {
|
||||
if field.SubPart != nil || index.name == "uk_alert_record_v2_open" && field.NonUnique != 0 {
|
||||
return fmt.Errorf("invalid index %s.%s", index.table, index.name)
|
||||
}
|
||||
columns = append(columns, field.ColumnName)
|
||||
}
|
||||
if strings.Join(columns, ",") != strings.Join(index.columns, ",") {
|
||||
return fmt.Errorf("missing or incompatible index %s.%s", index.table, index.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202611050020_business_alert_v2", Migrate: migrateAlertV2}, {ID: "202611040017_business_groups_goals_effectiveness", Migrate: migrateBusinessManagement}, {ID: "202611040018_business_effectiveness_query_index", Migrate: migrateEffectivenessQueryIndex}, {ID: "202611040015_business_approved_game_index", Migrate: migrateApprovedGameIndexes}, {ID: "202611040014_business_report_build_indexes", Migrate: migrateReportBuildQueryIndexes}, {ID: "202611040013_business_query_indexes", Migrate: migrateBusinessQueryIndexes}, {ID: "202611040012_business_server_report_index", Migrate: migrateServerReportIndex}, {ID: "202611040011_business_registration_index", Migrate: migrateRegistrationIndex}, {ID: "202611040010_business_alert_trigger_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &alertTriggerLockPO{}) }}, {ID: "202611040009_business_menu_icons", Migrate: migrateBusinessMenuIcons}, {ID: "202611040008_business_report_build_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &reportBuildLockPO{}) }}, {ID: "202611030007_business_alert_attachments", Migrate: migrateAlertAttachments}, {ID: "202611030006_business_alert_schema", Migrate: migrateAlertSchema}, {ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error {
|
||||
return []migration.Step{{ID: "202611060023_business_binding_status_index", Migrate: migrateBindingStatusIndex}, {ID: "202611060022_business_promote_id_capacity", Migrate: migratePromoteIDCapacity}, {ID: "202611050020_business_alert_v2", Migrate: migrateAlertV2}, {ID: "202611040017_business_groups_goals_effectiveness", Migrate: migrateBusinessManagement}, {ID: "202611040018_business_effectiveness_query_index", Migrate: migrateEffectivenessQueryIndex}, {ID: "202611040015_business_approved_game_index", Migrate: migrateApprovedGameIndexes}, {ID: "202611040014_business_report_build_indexes", Migrate: migrateReportBuildQueryIndexes}, {ID: "202611040013_business_query_indexes", Migrate: migrateBusinessQueryIndexes}, {ID: "202611040012_business_server_report_index", Migrate: migrateServerReportIndex}, {ID: "202611040011_business_registration_index", Migrate: migrateRegistrationIndex}, {ID: "202611040010_business_alert_trigger_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &alertTriggerLockPO{}) }}, {ID: "202611040009_business_menu_icons", Migrate: migrateBusinessMenuIcons}, {ID: "202611040008_business_report_build_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &reportBuildLockPO{}) }}, {ID: "202611030007_business_alert_attachments", Migrate: migrateAlertAttachments}, {ID: "202611030006_business_alert_schema", Migrate: migrateAlertSchema}, {ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &resetCodePO{}, &resetTokenPO{})
|
||||
}}, {ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &bindingLockPO{})
|
||||
|
||||
@@ -145,6 +145,9 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
var undo rollback
|
||||
defer undo.run()
|
||||
databaseReady := databaseConnectionConfigured(next.Data.Database)
|
||||
if !databaseReady && next.Admin.System != nil && next.Admin.System.DisableAutoMigrate {
|
||||
return fmt.Errorf("automatic migration is disabled; an existing migrated database is required")
|
||||
}
|
||||
var candidateDB *gorm.DB
|
||||
if databaseReady {
|
||||
candidateDB, err = openDatabase(next.Data.Database, false, "", d.logger())
|
||||
@@ -170,6 +173,11 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||
return fmt.Errorf("reload database migrations: %w", err)
|
||||
}
|
||||
}
|
||||
if databaseReady && next.Admin.System != nil && next.Admin.System.DisableAutoMigrate {
|
||||
if err = checkMigratedSchema(candidateDB.WithContext(ctx), d.catalog); err != nil {
|
||||
return fmt.Errorf("reload database schema: %w", err)
|
||||
}
|
||||
}
|
||||
storageConfig, err := dataintegration.ResolveStorageConfig(candidateDB.WithContext(ctx), next.Admin.Storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload storage configuration: %w", err)
|
||||
|
||||
+13
-5
@@ -246,12 +246,20 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor
|
||||
undo.add(cleanup)
|
||||
defer undo.run()
|
||||
usingFallback := !databaseConnectionConfigured(c.Database)
|
||||
admin := runtime.Admin()
|
||||
if admin == nil {
|
||||
admin = &config.Admin{}
|
||||
}
|
||||
disableAutoMigrate := admin.System != nil && admin.System.DisableAutoMigrate
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
if !usingFallback {
|
||||
db, err = openDatabase(c.Database, false, "", appLogger)
|
||||
}
|
||||
if usingFallback || err != nil {
|
||||
if disableAutoMigrate {
|
||||
return nil, nil, fmt.Errorf("automatic migration is disabled; an available migrated database is required")
|
||||
}
|
||||
// The initialization endpoint must remain available when the configured
|
||||
// target database has not been created yet.
|
||||
if err != nil {
|
||||
@@ -274,16 +282,16 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor
|
||||
for _, item := range d.dbList {
|
||||
registerDataScopeCallbacks(item, d.enqueueDataScopeAudit)
|
||||
}
|
||||
admin := runtime.Admin()
|
||||
if admin == nil {
|
||||
admin = &config.Admin{}
|
||||
}
|
||||
disableAutoMigrate := admin.System != nil && admin.System.DisableAutoMigrate
|
||||
if !usingFallback && !disableAutoMigrate {
|
||||
if err = migrateAll(db, catalog); err != nil {
|
||||
return nil, nil, fmt.Errorf("migrate tables: %w", err)
|
||||
}
|
||||
}
|
||||
if !usingFallback && disableAutoMigrate {
|
||||
if err = checkMigratedSchema(db, catalog); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if !usingFallback {
|
||||
storageConfig, storageErr := dataintegration.ResolveStorageConfig(db, admin.Storage)
|
||||
if storageErr != nil {
|
||||
|
||||
@@ -151,3 +151,5 @@ func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, &ConfigPO{}) }
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/config"
|
||||
"kra/pkg/database/migration"
|
||||
"kra/pkg/module"
|
||||
)
|
||||
|
||||
// MigrateDatabase opens only the configured primary database. It does not
|
||||
// create a database, start a scheduler, open SDK clients, or initialize users.
|
||||
func MigrateDatabase(ctx context.Context, c *config.Database, catalog module.Catalog, logger *slog.Logger, checkOnly bool) error {
|
||||
if !databaseConnectionConfigured(c) {
|
||||
return fmt.Errorf("migration requires an existing configured database")
|
||||
}
|
||||
migrationConfig := *c
|
||||
// One connection holds the advisory lock, a second executes migrations.
|
||||
if !checkOnly && migrationConfig.MaxOpenConns > 0 && migrationConfig.MaxOpenConns < 2 {
|
||||
migrationConfig.MaxOpenConns = 2
|
||||
}
|
||||
db, err := openDatabase(&migrationConfig, false, "", logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open migration database: %w", err)
|
||||
}
|
||||
defer closeGormDB(db)
|
||||
db = db.WithContext(ctx)
|
||||
if checkOnly {
|
||||
return checkMigratedSchema(db, catalog)
|
||||
}
|
||||
// A dedicated connection owns the lock even when a migration uses a
|
||||
// transaction or temporarily changes connection-local SQL compatibility.
|
||||
run := func() error {
|
||||
if err := catalog.CheckPrerequisites(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateAll(db, catalog); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMigratedSchema(db, catalog)
|
||||
}
|
||||
if db.Dialector.Name() != "mysql" {
|
||||
return run()
|
||||
}
|
||||
return db.Connection(func(conn *gorm.DB) error {
|
||||
var name string
|
||||
if err := conn.Raw("SELECT CONCAT('sw_to_kra:', MD5(DATABASE()))").Scan(&name).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var locked int
|
||||
if err := conn.Session(&gorm.Session{NewDB: true}).Raw("SELECT GET_LOCK(?, 0)", name).Scan(&locked).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if locked != 1 {
|
||||
return fmt.Errorf("another migration owns the database lock")
|
||||
}
|
||||
defer func() {
|
||||
cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer cancel()
|
||||
conn.Session(&gorm.Session{NewDB: true}).WithContext(cleanup).Exec("DO RELEASE_LOCK(?)", name)
|
||||
}()
|
||||
return run()
|
||||
})
|
||||
}
|
||||
|
||||
func checkMigratedSchema(db *gorm.DB, catalog module.Catalog) error {
|
||||
db = db.Session(&gorm.Session{NewDB: true})
|
||||
if err := migration.CheckApplied(db, catalog.MigrationSteps()); err != nil {
|
||||
return fmt.Errorf("run -migrate before starting the service: %w", err)
|
||||
}
|
||||
if err := catalog.CheckSchema(db); err != nil {
|
||||
return fmt.Errorf("database schema is incomplete: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -25,6 +25,9 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = migrateAll(db, testCatalog()); err != nil {
|
||||
t.Fatalf("second migration run: %v", err)
|
||||
}
|
||||
if err := checkMigratedSchema(db, testCatalog()); err != nil {
|
||||
t.Fatalf("complete migration must satisfy every module schema: %v", err)
|
||||
}
|
||||
for _, table := range []string{"sys_integration_configs", "sys_users", "sys_base_menus", "pay_orders", "pay_events"} {
|
||||
if !db.Migrator().HasTable(table) {
|
||||
t.Fatalf("migration did not create %s", table)
|
||||
|
||||
@@ -16,3 +16,7 @@ func Migrations() []migration.Step {
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func CheckSchema(db *gorm.DB) error {
|
||||
return migration.CheckModels(db, &paymentOrderPO{}, &paymentEventPO{})
|
||||
}
|
||||
|
||||
@@ -16,16 +16,7 @@ func Migrations() []migration.Step {
|
||||
{
|
||||
ID: "202608200002_system_schema",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db,
|
||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{},
|
||||
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
||||
&operationPO{}, &loginLogPO{}, &DataAccessLogPO{}, &errorRecordPO{},
|
||||
&mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
||||
&announcementPO{},
|
||||
)
|
||||
return migration.CreateMissingTables(db, schemaModels()...)
|
||||
},
|
||||
},
|
||||
{ID: "202608210002_communication_surface", Migrate: ensureCommunicationSurface},
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"kra/pkg/database/migration"
|
||||
)
|
||||
|
||||
func schemaModels() []any {
|
||||
return []any{
|
||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{},
|
||||
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
||||
&operationPO{}, &loginLogPO{}, &DataAccessLogPO{}, &errorRecordPO{},
|
||||
&mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{},
|
||||
}
|
||||
}
|
||||
|
||||
func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, schemaModels()...) }
|
||||
@@ -38,3 +38,5 @@ func SeedDefaults(ctx context.Context, db *gorm.DB, defaults []platformmodule.Ti
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, &taskPO{}, &taskLogPO{}) }
|
||||
|
||||
Reference in New Issue
Block a user