Files
go-admin/cmd/migrate/migration/version/column_width_test.go
zhangwenjian 2628ab8e3e fix🐛: a seeded menu overflowed its column and stopped the migration run
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.
2026-08-27 12:12:00 +08:00

83 lines
2.0 KiB
Go

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