Files
go-admin/app/other/models/tools/sys_tables_atomic_test.go
T
zhangwenjian 88b14a5e0f fix🐛: write a table's configuration and its columns in one transaction
SysTables.Create and Update wrote each column with _, _ = …, so a column
the database rejected was dropped while the request reported success. And
because neither ran in a transaction, returning that error alone would not
have been enough: the table row and every column before the failing one
were already committed, leaving a table the config page lists with columns
missing and code generated from fewer columns than the table has.

Both now run the table and its columns in one transaction and return the
first column's error, named. Create rolls back to nothing; Update leaves the
table and every column as they were. Both callers already answer an error
with a 500.
2026-09-26 14:17:19 +08:00

103 lines
3.0 KiB
Go

package tools
import (
"errors"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// A named, shared in-memory database: with a bare ":memory:" every pooled
// connection opens its own empty database, and a row written inside a
// transaction would be invisible to the query that checks for it.
func openAtomicDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(new(SysTables), new(SysColumns)); err != nil {
t.Fatalf("migrate: %v", err)
}
return db
}
// rejectNthColumnWrite makes the n-th write of a sys_columns row fail, the way
// it does when the database refuses it.
func rejectNthColumnWrite(register func(name string, fn func(*gorm.DB)) error, n int) error {
seen := 0
return register("test:reject_column", func(tx *gorm.DB) {
if tx.Statement.Table != "sys_columns" {
return
}
seen++
if seen == n {
_ = tx.AddError(errors.New("column rejected"))
}
})
}
func threeColumns() []SysColumns {
return []SysColumns{{ColumnName: "id"}, {ColumnName: "name"}, {ColumnName: "note"}}
}
func TestSysTablesCreateWritesAllColumnsOrNone(t *testing.T) {
db := openAtomicDB(t)
if err := rejectNthColumnWrite(db.Callback().Create().Before("gorm:create").Register, 2); err != nil {
t.Fatal(err)
}
table := SysTables{TBName: "orders", Columns: threeColumns()}
if _, err := table.Create(db); err == nil {
t.Fatal("Create reported success although a column was rejected")
}
var tables, columns int64
db.Model(new(SysTables)).Where("table_name = ?", "orders").Count(&tables)
db.Model(new(SysColumns)).Count(&columns)
if tables != 0 || columns != 0 {
t.Errorf("left %d table row(s) and %d column row(s) behind; want none", tables, columns)
}
}
func TestSysTablesUpdateWritesAllColumnsOrNone(t *testing.T) {
db := openAtomicDB(t)
table := SysTables{TBName: "orders", TableComment: "before", Columns: threeColumns()}
created, err := table.Create(db)
if err != nil {
t.Fatal(err)
}
if err := rejectNthColumnWrite(db.Callback().Update().Before("gorm:update").Register, 2); err != nil {
t.Fatal(err)
}
edited, err := (&SysTables{TableId: created.TableId}).Get(db, false)
if err != nil {
t.Fatal(err)
}
edited.TableComment = "after"
for i := range edited.Columns {
edited.Columns[i].ColumnComment = "after"
}
if _, err := edited.Update(db); err == nil {
t.Fatal("Update reported success although a column was rejected")
}
stored, err := (&SysTables{TableId: created.TableId}).Get(db, false)
if err != nil {
t.Fatal(err)
}
if stored.TableComment != "before" {
t.Errorf("table comment = %q; the table row was kept although a column failed", stored.TableComment)
}
for _, c := range stored.Columns {
if c.ColumnComment != "" {
t.Errorf("column %s comment = %q; a column write was kept although another failed", c.ColumnName, c.ColumnComment)
}
}
}