mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-27 20:19:07 +00:00
sys_columns and sys_tables were left out of the soft-delete conversion in 1786700003000. Their runtime models embed common.ModelTime, which is the millisecond marker, so GORM queries them with deleted_at = 0 - against a nullable datetime column holding NULL. Every row was invisible. The repository carries two ModelTime types: the one under cmd/migrate/migration/models still has a nullable gorm.DeletedAt and is what builds the tables, while common/models has the marker and is what queries them. Nothing connected the two, so a table could be built one way and read the other with no signal at all. The test now walks app/ for models embedding the marker and requires a migration to cover each. tb_demo is exempt and says why: nothing reads it at runtime.
174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package version
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The repository carries two ModelTime types. The one in
|
|
// cmd/migrate/migration/models still has a nullable gorm.DeletedAt and is what
|
|
// builds the tables; the one in common/models is the millisecond marker and is
|
|
// what queries them. A table whose runtime model embeds the second but which no
|
|
// migration converts is queried with deleted_at = 0 against a datetime column,
|
|
// and every row is invisible - silently, and only in production.
|
|
//
|
|
// sys_columns and sys_tables were in exactly that state: the code generator
|
|
// listed no tables at all.
|
|
func TestEveryRuntimeSoftDeleteTableIsConverted(t *testing.T) {
|
|
converted := map[string]bool{}
|
|
for _, name := range append(append([]string{}, softDeleteTables...), generatorTables...) {
|
|
converted[name] = true
|
|
}
|
|
|
|
// tb_demo is built with the marker-less model and has no runtime model at
|
|
// all, so nothing ever queries it with deleted_at = 0.
|
|
const noRuntimeModel = "tb_demo"
|
|
|
|
for table, file := range runtimeSoftDeleteTables(t) {
|
|
if table == noRuntimeModel {
|
|
continue
|
|
}
|
|
if !converted[table] {
|
|
t.Errorf("%s (%s) embeds common.ModelTime but no migration converts its deleted_at;\n"+
|
|
" it will be queried with deleted_at = 0 against a nullable datetime and return nothing",
|
|
table, file)
|
|
}
|
|
}
|
|
}
|
|
|
|
// runtimeSoftDeleteTables maps table name to the file declaring it, for every
|
|
// model under app/ that embeds the marker-carrying ModelTime.
|
|
func runtimeSoftDeleteTables(t *testing.T) map[string]string {
|
|
t.Helper()
|
|
|
|
root := repoRoot(t)
|
|
found := map[string]string{}
|
|
|
|
err := filepath.Walk(filepath.Join(root, "app"), func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return nil // not this test's business
|
|
}
|
|
// Only files importing the runtime models package can embed its ModelTime.
|
|
if !importsRuntimeModels(f) {
|
|
return nil
|
|
}
|
|
for name, table := range tablesWithModelTime(f) {
|
|
_ = name
|
|
found[table] = strings.TrimPrefix(path, root+"/")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk app/: %v", err)
|
|
}
|
|
if len(found) == 0 {
|
|
t.Fatal("found no runtime models at all; the scan is broken, not the code")
|
|
}
|
|
return found
|
|
}
|
|
|
|
func importsRuntimeModels(f *ast.File) bool {
|
|
for _, imp := range f.Imports {
|
|
p, err := strconv.Unquote(imp.Path.Value)
|
|
if err == nil && p == "go-admin/common/models" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tablesWithModelTime returns struct name -> table name for structs that embed
|
|
// ModelTime and declare a TableName.
|
|
func tablesWithModelTime(f *ast.File) map[string]string {
|
|
embeds := map[string]bool{}
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
ts, ok := n.(*ast.TypeSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
st, ok := ts.Type.(*ast.StructType)
|
|
if !ok {
|
|
return true
|
|
}
|
|
for _, field := range st.Fields.List {
|
|
if len(field.Names) != 0 {
|
|
continue // named field, not an embed
|
|
}
|
|
if sel, ok := field.Type.(*ast.SelectorExpr); ok && sel.Sel.Name == "ModelTime" {
|
|
embeds[ts.Name.Name] = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
out := map[string]string{}
|
|
for name := range embeds {
|
|
if table := tableNameOf(f, name); table != "" {
|
|
out[name] = table
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// tableNameOf finds the string returned by func (T) TableName() string.
|
|
func tableNameOf(f *ast.File, structName string) string {
|
|
var table string
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
fn, ok := n.(*ast.FuncDecl)
|
|
if !ok || fn.Name.Name != "TableName" || fn.Recv == nil || len(fn.Recv.List) != 1 {
|
|
return true
|
|
}
|
|
if receiverName(fn.Recv.List[0].Type) != structName {
|
|
return true
|
|
}
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.BasicLit)
|
|
if ok && lit.Kind == token.STRING {
|
|
if s, err := strconv.Unquote(lit.Value); err == nil && table == "" {
|
|
table = s
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return true
|
|
})
|
|
return table
|
|
}
|
|
|
|
func receiverName(expr ast.Expr) string {
|
|
switch t := expr.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.StarExpr:
|
|
return receiverName(t.X)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func repoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 8; i++ {
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
return dir
|
|
}
|
|
dir = filepath.Dir(dir)
|
|
}
|
|
t.Fatal("go.mod not found above the test directory")
|
|
return ""
|
|
}
|