From a6cc1c6d5dcbbdd4b461ef69b2537d8b311bdc8f Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sat, 29 Aug 2026 08:47:06 +0800 Subject: [PATCH] fix(migrations): support MySQL 8.4 checks (#140) --- server/app/goauto/migrations/migrate.go | 33 ++++++++++---- .../migrations/mysql_constraint_test.go | 43 +++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 server/app/goauto/migrations/mysql_constraint_test.go diff --git a/server/app/goauto/migrations/migrate.go b/server/app/goauto/migrations/migrate.go index ccffe69..4a00f3b 100644 --- a/server/app/goauto/migrations/migrate.go +++ b/server/app/goauto/migrations/migrate.go @@ -1,11 +1,26 @@ package migrations import ( + "strings" + "go-admin/app/goauto/models" "gorm.io/gorm" ) +const mysqlCheckConstraintQuery = ` +SELECT cc.check_clause +FROM information_schema.table_constraints AS tc +JOIN information_schema.check_constraints AS cc + ON cc.constraint_catalog = tc.constraint_catalog + AND cc.constraint_schema = tc.constraint_schema + AND cc.constraint_name = tc.constraint_name +WHERE tc.constraint_schema = DATABASE() + AND tc.table_name = ? + AND tc.constraint_type = 'CHECK' + AND tc.constraint_name = ? +LIMIT 1` + // Migrate creates the complete GoAuto MVP schema. AutoMigrate is intentionally // kept behind this package so the CLI migration and tests use the same model set. // MigratedModels is the single source of truth for which models make up the @@ -68,21 +83,23 @@ func ensureMySQLDirectSelectConstraint(db *gorm.DB) error { return nil } const name = "ck_purchase_task_spec_source" - var total, current int64 - base := `SELECT COUNT(*) FROM information_schema.check_constraints WHERE constraint_schema = DATABASE() AND table_name = 'purchase_task' AND constraint_name = ?` - if err := db.Raw(base, name).Scan(&total).Error; err != nil { + var constraints []struct { + CheckClause string `gorm:"column:check_clause"` + } + if err := db.Raw(mysqlCheckConstraintQuery, "purchase_task", name).Scan(&constraints).Error; err != nil { return err } - if err := db.Raw(base+` AND check_clause LIKE '%direct_select%'`, name).Scan(¤t).Error; err != nil { - return err - } - if current > 0 { + if len(constraints) > 0 && checkConstraintAllowsDirectSelect(constraints[0].CheckClause) { return nil } - if total > 0 { + if len(constraints) > 0 { if err := db.Exec("ALTER TABLE purchase_task DROP CHECK " + name).Error; err != nil { return err } } return db.Exec("ALTER TABLE purchase_task ADD CONSTRAINT " + name + " CHECK (spec_source IN ('unresolved','manual_mapping','exact_match','ai_match','direct_select'))").Error } + +func checkConstraintAllowsDirectSelect(clause string) bool { + return strings.Contains(strings.ToLower(clause), "direct_select") +} diff --git a/server/app/goauto/migrations/mysql_constraint_test.go b/server/app/goauto/migrations/mysql_constraint_test.go new file mode 100644 index 0000000..e1d055e --- /dev/null +++ b/server/app/goauto/migrations/mysql_constraint_test.go @@ -0,0 +1,43 @@ +package migrations + +import ( + "strings" + "testing" +) + +func TestMySQLCheckConstraintQueryUsesTableConstraintsForTableName(t *testing.T) { + query := strings.ToLower(mysqlCheckConstraintQuery) + for _, required := range []string{ + "from information_schema.table_constraints as tc", + "join information_schema.check_constraints as cc", + "tc.table_name = ?", + "tc.constraint_type = 'check'", + "cc.constraint_schema = tc.constraint_schema", + "cc.constraint_name = tc.constraint_name", + } { + if !strings.Contains(query, required) { + t.Fatalf("metadata query missing %q: %s", required, mysqlCheckConstraintQuery) + } + } + if strings.Contains(query, "cc.table_name") { + t.Fatalf("MySQL 8.4 CHECK_CONSTRAINTS has no TABLE_NAME column: %s", mysqlCheckConstraintQuery) + } +} + +func TestCheckConstraintAllowsDirectSelect(t *testing.T) { + for _, test := range []struct { + name string + clause string + want bool + }{ + {name: "legacy constraint", clause: "spec_source in ('unresolved','manual_mapping','exact_match','ai_match')", want: false}, + {name: "upgraded constraint", clause: "spec_source in ('unresolved','manual_mapping','exact_match','ai_match','direct_select')", want: true}, + {name: "mysql charset prefix", clause: "(`spec_source` in (_utf8mb4'DIRECT_SELECT',_utf8mb4'unresolved'))", want: true}, + } { + t.Run(test.name, func(t *testing.T) { + if got := checkConstraintAllowsDirectSelect(test.clause); got != test.want { + t.Fatalf("checkConstraintAllowsDirectSelect(%q) = %v, want %v", test.clause, got, test.want) + } + }) + } +}