From 2628ab8e3e2c705e1e793d835297b4953e5b4d37 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Thu, 27 Aug 2026 12:12:00 +0800 Subject: [PATCH] =?UTF-8?q?fix=F0=9F=90=9B:=20a=20seeded=20menu=20overflow?= =?UTF-8?q?ed=20its=20column=20and=20stopped=20the=20migration=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sort is gorm:"size:4", which MySQL builds as a tinyint holding -128..127. The demo menu seeded Sort: 900, so on MySQL the run stopped at 1786700001000 with Error 1264, and every migration after it - including the soft-delete conversion - never ran. deleted_at therefore stayed NULL while the code queries deleted_at = 0, and the login returned 'incorrect Username or Password' on a database whose password hash was correct all along. sqlite ignores the declared width, so a fresh install there passed and the fault only appeared on MySQL. --- .../version/1786700001000_demo_menu.go | 5 +- .../migration/version/column_width_test.go | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 cmd/migrate/migration/version/column_width_test.go diff --git a/cmd/migrate/migration/version/1786700001000_demo_menu.go b/cmd/migrate/migration/version/1786700001000_demo_menu.go index 193fdbcb..4e00a477 100644 --- a/cmd/migrate/migration/version/1786700001000_demo_menu.go +++ b/cmd/migrate/migration/version/1786700001000_demo_menu.go @@ -56,7 +56,10 @@ func _1786700001000DemoMenu(db *gorm.DB, version string) error { dir := models.SysMenu{ MenuId: demoMenuId, MenuName: "Demo", Title: "示例模块", Icon: "example", Path: "/demo", Paths: "/0/9000", MenuType: "M", ParentId: 0, - Component: "Layout", Sort: 900, Visible: "0", IsFrame: "1", + // sort is `gorm:"size:4"`, which MySQL builds as a tinyint - anything + // over 127 is rejected outright. The seeded menus run to 100, so 110 + // still puts this last. + Component: "Layout", Sort: 110, Visible: "0", IsFrame: "1", } if err := upsert(tx, &models.SysMenu{}, "menu_id = ?", dir.MenuId, &dir); err != nil { return err diff --git a/cmd/migrate/migration/version/column_width_test.go b/cmd/migrate/migration/version/column_width_test.go new file mode 100644 index 00000000..99bd4623 --- /dev/null +++ b/cmd/migrate/migration/version/column_width_test.go @@ -0,0 +1,82 @@ +package version + +import ( + "go/ast" + "go/parser" + "go/token" + "math" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// Fields tagged gorm:"size:4" become a tinyint on MySQL, which holds -128..127. +// sqlite ignores the width, so a value that overflows passes every local test +// and fails on a real install - and because the migration is not transactional, +// it fails partway, leaving later migrations unapplied. +// +// That is what happened: a seeded menu with Sort: 900 stopped the run at +// 1786700001000, so the soft-delete conversion never ran, deleted_at stayed +// NULL, and nobody could log in. +var narrowColumns = map[string]struct{ min, max int64 }{ + "Sort": {math.MinInt8, math.MaxInt8}, + "Status": {math.MinInt8, math.MaxInt8}, +} + +func TestSeededValuesFitTheirColumns(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + files, err := filepath.Glob(filepath.Join(dir, "*.go")) + if err != nil { + t.Fatal(err) + } + + checked := 0 + for _, path := range files { + if strings.HasSuffix(path, "_test.go") { + continue + } + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + ast.Inspect(f, func(n ast.Node) bool { + kv, ok := n.(*ast.KeyValueExpr) + if !ok { + return true + } + key, ok := kv.Key.(*ast.Ident) + if !ok { + return true + } + limits, watched := narrowColumns[key.Name] + if !watched { + return true + } + lit, ok := kv.Value.(*ast.BasicLit) + if !ok || lit.Kind != token.INT { + return true + } + v, err := strconv.ParseInt(lit.Value, 10, 64) + if err != nil { + return true + } + checked++ + if v < limits.min || v > limits.max { + t.Errorf("%s:%d: %s: %d does not fit a tinyint (%d..%d);\n"+ + " MySQL rejects it with Error 1264 and the migration stops there", + filepath.Base(path), fset.Position(lit.Pos()).Line, key.Name, v, limits.min, limits.max) + } + return true + }) + } + + if checked == 0 { + t.Fatal("no seeded values were examined; the scan is broken, not the code") + } +}