mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-21 02:04:09 +00:00
fix🐛: the candidate table query assumed one database
The exclusion list was a subquery against `$GenConfig.DBName`.sys_tables, so it named the schema by hand. Generating from a schema that is not the one holding sys_tables made the whole query fail, and because the subquery read the table directly it also counted soft-deleted entries: deleting a generator entry never handed its table back. Read the registrations through the model on this connection instead. An empty list skips the clause - NOT IN (NULL) is unknown for every row, which would leave a fresh install with nothing to generate from.
This commit is contained in:
@@ -25,9 +25,22 @@ func (e *DBTables) GetPage(tx *gorm.DB, pageSize int, pageIndex int) ([]DBTables
|
||||
var doc []DBTables
|
||||
var count int64
|
||||
|
||||
// Tables already registered with the generator are not candidates. Read them
|
||||
// through the model on this connection: the subquery used to spell the
|
||||
// schema out by hand, so it only resolved when sys_tables happened to live
|
||||
// in the schema being generated from, and it counted soft-deleted rows.
|
||||
var generated []string
|
||||
if err := tx.Model(&SysTables{}).Pluck("table_name", &generated).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
table := tx.Table("information_schema.tables")
|
||||
table = table.Where("TABLE_NAME not in (select table_name from `" + config2.GenConfig.DBName + "`.sys_tables) ")
|
||||
table = table.Where("table_schema= ? ", config2.GenConfig.DBName)
|
||||
if len(generated) > 0 {
|
||||
// NOT IN (NULL) is unknown for every row, so an empty list has to skip
|
||||
// the clause instead of rendering it.
|
||||
table = table.Where("TABLE_NAME not in (?)", generated)
|
||||
}
|
||||
|
||||
if e.TableName != "" {
|
||||
table = table.Where("TABLE_NAME = ?", e.TableName)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
config2 "github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const generatorSchema = "go_admin_test"
|
||||
|
||||
// newCandidateDB stands in for MySQL: sqlite is given an attached database
|
||||
// called information_schema so the same query runs, and sys_tables lives on the
|
||||
// connection the way it does in production.
|
||||
func newCandidateDB(t *testing.T, tables ...string) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.Exec(`ATTACH DATABASE ':memory:' AS information_schema`).Error; err != nil {
|
||||
t.Fatalf("attach information_schema: %v", err)
|
||||
}
|
||||
if err := db.Exec("CREATE TABLE information_schema.`tables` (" +
|
||||
"TABLE_NAME text, TABLE_SCHEMA text, `ENGINE` text, TABLE_ROWS text," +
|
||||
"TABLE_COLLATION text, CREATE_TIME text, UPDATE_TIME text, TABLE_COMMENT text)").Error; err != nil {
|
||||
t.Fatalf("create information_schema.tables: %v", err)
|
||||
}
|
||||
for _, name := range tables {
|
||||
if err := db.Exec("INSERT INTO information_schema.`tables` (TABLE_NAME, TABLE_SCHEMA) VALUES (?, ?)",
|
||||
name, generatorSchema).Error; err != nil {
|
||||
t.Fatalf("insert %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if err := db.AutoMigrate(new(SysTables)); err != nil {
|
||||
t.Fatalf("migrate sys_tables: %v", err)
|
||||
}
|
||||
|
||||
previousDriver := config2.DatabaseConfig.Driver
|
||||
previousName := config2.GenConfig.DBName
|
||||
config2.DatabaseConfig.Driver = "mysql"
|
||||
config2.GenConfig.DBName = generatorSchema
|
||||
t.Cleanup(func() {
|
||||
config2.DatabaseConfig.Driver = previousDriver
|
||||
config2.GenConfig.DBName = previousName
|
||||
})
|
||||
return db
|
||||
}
|
||||
|
||||
func candidateNames(t *testing.T, db *gorm.DB) []string {
|
||||
t.Helper()
|
||||
found, _, err := new(DBTables).GetPage(db, 100, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPage: %v", err)
|
||||
}
|
||||
names := make([]string, 0, len(found))
|
||||
for _, row := range found {
|
||||
names = append(names, row.TableName)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func contains(names []string, want string) bool {
|
||||
for _, name := range names {
|
||||
if name == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// An empty sys_tables must not filter everything out - that is what a bare
|
||||
// NOT IN (empty set) does, and a fresh install is exactly the case where the
|
||||
// list matters most.
|
||||
func TestGetPageListsEveryTableWhenNoneAreRegistered(t *testing.T) {
|
||||
db := newCandidateDB(t, "sys_user", "sys_role")
|
||||
|
||||
names := candidateNames(t, db)
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("want both tables offered on a fresh install, got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPageSkipsAlreadyRegisteredTables(t *testing.T) {
|
||||
db := newCandidateDB(t, "sys_user", "sys_role")
|
||||
if err := db.Create(&SysTables{TBName: "sys_user"}).Error; err != nil {
|
||||
t.Fatalf("register sys_user: %v", err)
|
||||
}
|
||||
|
||||
names := candidateNames(t, db)
|
||||
if contains(names, "sys_user") {
|
||||
t.Errorf("sys_user is already registered and was offered again: %v", names)
|
||||
}
|
||||
if !contains(names, "sys_role") {
|
||||
t.Errorf("sys_role is not registered and was withheld: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting the generator entry has to hand the table back, which the raw
|
||||
// subquery never did: it read the row whether or not it was soft-deleted.
|
||||
func TestGetPageOffersTablesWhoseEntryWasDeleted(t *testing.T) {
|
||||
db := newCandidateDB(t, "sys_user")
|
||||
registered := SysTables{TBName: "sys_user"}
|
||||
if err := db.Create(®istered).Error; err != nil {
|
||||
t.Fatalf("register sys_user: %v", err)
|
||||
}
|
||||
if err := db.Delete(®istered).Error; err != nil {
|
||||
t.Fatalf("delete the entry: %v", err)
|
||||
}
|
||||
|
||||
if names := candidateNames(t, db); !contains(names, "sys_user") {
|
||||
t.Errorf("the generator entry is deleted, sys_user should be a candidate again: %v", names)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user