fix(migrations): 数据库升级 - 兼容旧工作台 v51 账本

This commit is contained in:
ddcat666
2026-10-13 13:38:08 +08:00
parent ea6676f449
commit 26dfb8d91b
5 changed files with 107 additions and 2 deletions

View File

@@ -29,6 +29,9 @@ const prefixedIDSequenceReconcileChecksum = "sha256:prefixed-id-sequence-reconci
const skillLibraryCategoriesChecksum = "sha256:skill-library-categories-v39-20260926"
const builtinSkillTombstonesChecksum = "sha256:builtin-skill-tombstones-v40-20260927"
const resourceThumbnailChecksum = "sha256:resource-thumbnail-v41-20260927"
const workbenchToolsMigrationName = "workbench_tools_and_presets"
const workbenchToolsMigrationChecksum = "sha256:workbench-tools-presets-v54-20261014"
const legacyWorkbenchToolsMigrationChecksum = "sha256:workbench-tools-presets-v51-20261014"
const postgresSchemaMigrationLockID int64 = 73123910420260830
@@ -178,7 +181,7 @@ var schemaMigrations = []migration{
{version: 53, name: "user_login_logs", checksum: "sha256:user-login-logs-v53", apply: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.LoginLog{})
}},
{version: 54, name: "workbench_tools_and_presets", checksum: "sha256:workbench-tools-presets-v54-20261014", apply: func(tx *gorm.DB) error {
{version: 54, name: workbenchToolsMigrationName, checksum: workbenchToolsMigrationChecksum, apply: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.WorkbenchTool{}, &model.WorkbenchPreset{}, &model.Task{})
}},
}
@@ -510,6 +513,47 @@ func migrateSchemaV12(tx *gorm.DB) error {
return nil
}
func normalizeLegacyWorkbenchMigration(tx *gorm.DB) error {
var legacy schemaMigration
if err := tx.First(&legacy, "version = ?", 51).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("读取旧工作台迁移记录:%w", err)
}
if legacy.Name != workbenchToolsMigrationName || legacy.Checksum != legacyWorkbenchToolsMigrationChecksum {
return nil
}
var later []schemaMigration
if err := tx.Where("version > ?", legacy.Version).Order("version asc").Find(&later).Error; err != nil {
return fmt.Errorf("读取旧工作台迁移后的记录:%w", err)
}
if len(later) > 0 {
return fmt.Errorf("数据库迁移 51 使用旧工作台身份,但已存在后续迁移记录 %d;拒绝自动改写迁移账本", later[0].Version)
}
if !tx.Migrator().HasTable(&model.WorkbenchTool{}) || !tx.Migrator().HasTable(&model.WorkbenchPreset{}) {
return fmt.Errorf("数据库迁移 51 记录为旧工作台迁移,但工作台表不完整;请从备份恢复后再升级")
}
result := tx.Model(&schemaMigration{}).
Where("version = ? AND name = ? AND checksum = ?", legacy.Version, legacy.Name, legacy.Checksum).
Updates(map[string]any{
"version": workbenchToolsMigrationVersion,
"name": workbenchToolsMigrationName,
"checksum": workbenchToolsMigrationChecksum,
})
if result.Error != nil {
return fmt.Errorf("归一化旧工作台迁移记录:%w", result.Error)
}
if result.RowsAffected != 1 {
return fmt.Errorf("归一化旧工作台迁移记录影响 %d 行,拒绝继续启动", result.RowsAffected)
}
return nil
}
const workbenchToolsMigrationVersion int64 = 54
func MigrateSchema(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if tx.Dialector.Name() == "postgres" {
@@ -520,6 +564,9 @@ func MigrateSchema(db *gorm.DB) error {
if err := tx.AutoMigrate(&schemaMigration{}); err != nil {
return fmt.Errorf("初始化数据库迁移记录:%w", err)
}
if err := normalizeLegacyWorkbenchMigration(tx); err != nil {
return err
}
plan, err := migrationsForDatabase(tx)
if err != nil {
return err

View File

@@ -98,6 +98,53 @@ func TestMigrateSchemaRecordsAndValidatesVersion(t *testing.T) {
}
}
func TestMigrateSchemaNormalizesLegacyWorkbenchMigrationV51(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-legacy-workbench-v51?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := MigrateSchema(db); err != nil {
t.Fatal(err)
}
if err := db.Where("version IN ?", []int64{51, 52, 53}).Delete(&schemaMigration{}).Error; err != nil {
t.Fatal(err)
}
if err := db.Model(&schemaMigration{}).Where("version = ?", 54).Updates(map[string]any{
"version": 51,
"name": workbenchToolsMigrationName,
"checksum": legacyWorkbenchToolsMigrationChecksum,
}).Error; err != nil {
t.Fatal(err)
}
if err := MigrateSchema(db); err != nil {
t.Fatalf("legacy workbench migration should be normalized: %v", err)
}
status, err := ReadSchemaStatus(db)
if err != nil {
t.Fatal(err)
}
if !status.Ready || status.Current != CurrentSchemaVersion {
t.Fatalf("unexpected normalized schema status: %#v", status)
}
var normalized schemaMigration
if err := db.First(&normalized, "version = ?", workbenchToolsMigrationVersion).Error; err != nil {
t.Fatal(err)
}
if normalized.Name != workbenchToolsMigrationName || normalized.Checksum != workbenchToolsMigrationChecksum {
t.Fatalf("normalized workbench migration = %#v", normalized)
}
for _, item := range schemaMigrations {
var count int64
if err := db.Model(&schemaMigration{}).Where("version = ? AND name = ? AND checksum = ?", item.version, item.name, item.checksum).Count(&count).Error; err != nil {
t.Fatal(err)
}
if item.version != workbenchToolsMigrationVersion && count != 1 {
t.Fatalf("migration %d was not restored: %#v", item.version, item)
}
}
}
func TestMigrateSchemaV35UpgradesExistingDatabase(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-auth-notifications-v35?mode=memory&cache=shared"})
if err != nil {