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") + } +}