package models import ( "fmt" "testing" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *testing.T) { database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{TranslateError: true}) require.NoError(t, err) legacySchema := []string{ `CREATE TABLE senlin_agent_projects (id integer primary key, owner_id integer not null, name text not null, identifier text)`, `CREATE TABLE senlin_agent_tags (id integer primary key, identity text, project_id integer not null, name text not null)`, `CREATE TABLE senlin_agent_tasks (id integer primary key, project_id integer not null, created_by integer not null, tag_id integer, tag_identity text, title text not null, status text not null)`, `INSERT INTO senlin_agent_projects (id, owner_id, name, identifier) VALUES (1, 7, 'Blank one', ''), (2, 7, 'Blank two', NULL), (3, 7, 'Keeper', 'DUP'), (4, 7, 'Duplicate', 'DUP'), (5, 7, 'Reserved suffix', 'DUP-4'), (6, 8, 'Other owner', 'DUP')`, `INSERT INTO senlin_agent_tags (id, identity, project_id, name) VALUES (10, 'tag-keeper', 1, 'UI'), (11, 'tag-duplicate', 1, 'UI'), (12, 'tag-other-project', 2, 'UI')`, `INSERT INTO senlin_agent_tasks (id, project_id, created_by, tag_id, tag_identity, title, status) VALUES (20, 1, 7, 11, 'tag-duplicate', 'Keep association', 'open')`, } for _, statement := range legacySchema { require.NoError(t, database.Exec(statement).Error) } // PostgreSQL migrations run through database.Connection, whose callback DB can // carry an initialized Gorm statement. Exercise that mode so model state from // the migration-version lookup cannot leak into the data-repair transaction. require.NoError(t, AutoMigrate(database.Session(&gorm.Session{Initialized: true}))) require.False(t, database.Migrator().HasTable("senlin_agent_projects")) require.False(t, database.Migrator().HasTable("senlin_agent_tags")) require.False(t, database.Migrator().HasTable("senlin_agent_tasks")) require.True(t, database.Migrator().HasTable("sa_projects")) require.True(t, database.Migrator().HasTable("sa_tags")) require.True(t, database.Migrator().HasTable("sa_tasks")) var projects []SaProject require.NoError(t, database.Order("id asc").Find(&projects).Error) require.Len(t, projects, 6, "项目迁移不得删除 legacy 行") require.Equal(t, []string{"project-1", "project-2", "DUP", "DUP-4-2", "DUP-4", "DUP"}, projectIdentifiers(projects)) var tags []SaTag require.NoError(t, database.Order("id asc").Find(&tags).Error) require.Equal(t, []uint{10, 12}, tagIDs(tags), "同项目同名标签应保留最早记录,不影响其他项目") var task SaTask require.NoError(t, database.First(&task, 20).Error) require.NotNil(t, task.TagID) require.Equal(t, uint(10), *task.TagID) require.NotNil(t, task.TagIdentity) require.Equal(t, "tag-keeper", *task.TagIdentity) var migrationCount int64 require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error) require.Equal(t, int64(3), migrationCount) var auditDetails []string require.NoError(t, database.Table("sa_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error) require.Contains(t, auditDetails[0], `"updated":3`) require.Contains(t, auditDetails[1], `"deduplicated":1`) require.NoError(t, AutoMigrate(database), "versioned migrations must be safe to run again") require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error) require.Equal(t, int64(3), migrationCount) require.Error(t, database.Exec(`INSERT INTO sa_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error) require.Error(t, database.Exec(`INSERT INTO sa_tags (project_id, name) VALUES (1, 'UI')`).Error) } func TestAutoMigrateRejectsAmbiguousLegacyAndCurrentTables(t *testing.T) { database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) require.NoError(t, database.Exec(`CREATE TABLE senlin_agent_users (id integer primary key)`).Error) require.NoError(t, database.Exec(`CREATE TABLE sa_users (id integer primary key)`).Error) err = AutoMigrate(database) require.EqualError(t, err, "cannot rename legacy table senlin_agent_users: target table sa_users already exists") } func TestRenameLegacyTablesRenamesDocumentOwnerColumns(t *testing.T) { database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) require.NoError(t, err) require.NoError(t, database.Exec(` CREATE TABLE sa_documents ( id integer primary key, created_by integer not null, created_by_identity text ) `).Error) require.NoError(t, database.Exec(` INSERT INTO sa_documents (id, created_by, created_by_identity) VALUES (1, 7, 'owner-identity') `).Error) require.NoError(t, renameLegacyTables(database)) require.False(t, database.Migrator().HasColumn("sa_documents", "created_by")) require.False(t, database.Migrator().HasColumn("sa_documents", "created_by_identity")) require.True(t, database.Migrator().HasColumn("sa_documents", "owner_id")) require.True(t, database.Migrator().HasColumn("sa_documents", "owner_identity")) var stored struct { OwnerID uint OwnerIdentity string } require.NoError(t, database.Table("sa_documents").Where("id = ?", 1).Take(&stored).Error) require.Equal(t, uint(7), stored.OwnerID) require.Equal(t, "owner-identity", stored.OwnerIdentity) } func projectIdentifiers(projects []SaProject) []string { result := make([]string, 0, len(projects)) for _, project := range projects { result = append(result, project.Identifier) } return result } func tagIDs(tags []SaTag) []uint { result := make([]uint, 0, len(tags)) for _, tag := range tags { result = append(result, tag.ID) } return result }