mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(migrate-db): preserve false-valued columns in SQLite to Postgres copy
GORM struct INSERT substitutes a column default tag for Go zero-values, so disabled rows (enable=false) silently re-enabled on the destination. Copy each batch through explicit per-column maps so every value is written verbatim. Adds a regression test.
This commit is contained in:
parent
e7c11c913a
commit
71cf22fa8d
2 changed files with 94 additions and 4 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -109,14 +110,15 @@ func copyTable(src, dst *gorm.DB, mdl any) (int, error) {
|
||||||
|
|
||||||
sliceType := reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(mdl).Elem()))
|
sliceType := reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(mdl).Elem()))
|
||||||
|
|
||||||
// Resolve primary-key columns so paging is deterministic across successive
|
|
||||||
// LIMIT/OFFSET reads. The model set is trusted (not user input).
|
|
||||||
stmt := &gorm.Statement{DB: src}
|
stmt := &gorm.Statement{DB: src}
|
||||||
if err := stmt.Parse(mdl); err != nil {
|
if err := stmt.Parse(mdl); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
order := strings.Join(stmt.Schema.PrimaryFieldDBNames, ", ")
|
order := strings.Join(stmt.Schema.PrimaryFieldDBNames, ", ")
|
||||||
|
table := stmt.Schema.Table
|
||||||
|
columns := stmt.Schema.DBNames
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
total := 0
|
total := 0
|
||||||
for offset := 0; ; offset += batchSize {
|
for offset := 0; ; offset += batchSize {
|
||||||
batchPtr := reflect.New(sliceType)
|
batchPtr := reflect.New(sliceType)
|
||||||
|
|
@ -127,11 +129,24 @@ func copyTable(src, dst *gorm.DB, mdl any) (int, error) {
|
||||||
if err := q.Find(batchPtr.Interface()).Error; err != nil {
|
if err := q.Find(batchPtr.Interface()).Error; err != nil {
|
||||||
return total, err
|
return total, err
|
||||||
}
|
}
|
||||||
n := batchPtr.Elem().Len()
|
slice := batchPtr.Elem()
|
||||||
|
n := slice.Len()
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := dst.CreateInBatches(batchPtr.Interface(), 200).Error; err != nil {
|
|
||||||
|
rows := make([]map[string]any, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
rv := reflect.Indirect(slice.Index(i))
|
||||||
|
row := make(map[string]any, len(columns))
|
||||||
|
for _, name := range columns {
|
||||||
|
value, _ := stmt.Schema.FieldsByDBName[name].ValueOf(ctx, rv)
|
||||||
|
row[name] = value
|
||||||
|
}
|
||||||
|
rows[i] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dst.Table(table).CreateInBatches(rows, 200).Error; err != nil {
|
||||||
return total, err
|
return total, err
|
||||||
}
|
}
|
||||||
total += n
|
total += n
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,78 @@ func TestMigrateData_CompositeKeyTableLargerThanBatch(t *testing.T) {
|
||||||
t.Fatalf("client_inbounds rows = %d, want %d", got, n)
|
t.Fatalf("client_inbounds rows = %d, want %d", got, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMigrateData_PreservesFalseDefaultedColumns(t *testing.T) {
|
||||||
|
dsn := os.Getenv("XUI_TEST_PG_DSN")
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("set XUI_TEST_PG_DSN to a reachable Postgres to run this test")
|
||||||
|
}
|
||||||
|
|
||||||
|
srcPath := t.TempDir() + "/x-ui.db"
|
||||||
|
src, err := gorm.Open(sqlite.Open(srcPath), &gorm.Config{Logger: logger.Discard})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
for _, m := range migrationModels() {
|
||||||
|
if err := src.AutoMigrate(m); err != nil {
|
||||||
|
t.Fatalf("automigrate %T: %v", m, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := src.Create([]*model.ClientRecord{
|
||||||
|
{Email: "on@example.com"},
|
||||||
|
{Email: "off@example.com"},
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed clients: %v", err)
|
||||||
|
}
|
||||||
|
if err := src.Model(&model.ClientRecord{}).Where("email = ?", "off@example.com").
|
||||||
|
Update("enable", false).Error; err != nil {
|
||||||
|
t.Fatalf("disable client: %v", err)
|
||||||
|
}
|
||||||
|
if err := src.Create(&model.Node{Name: "n-off", Address: "1.2.3.4", Port: 1, ApiToken: "tok"}).Error; err != nil {
|
||||||
|
t.Fatalf("seed node: %v", err)
|
||||||
|
}
|
||||||
|
if err := src.Model(&model.Node{}).Where("name = ?", "n-off").
|
||||||
|
Update("enable", false).Error; err != nil {
|
||||||
|
t.Fatalf("disable node: %v", err)
|
||||||
|
}
|
||||||
|
if sqlDB, err := src.DB(); err == nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
dst, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Discard})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open postgres: %v", err)
|
||||||
|
}
|
||||||
|
if err := dst.Migrator().DropTable(migrationModels()...); err != nil {
|
||||||
|
t.Fatalf("drop tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := MigrateData(srcPath, dsn); err != nil {
|
||||||
|
t.Fatalf("MigrateData: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var off model.ClientRecord
|
||||||
|
if err := dst.Where("email = ?", "off@example.com").First(&off).Error; err != nil {
|
||||||
|
t.Fatalf("load disabled client: %v", err)
|
||||||
|
}
|
||||||
|
if off.Enable {
|
||||||
|
t.Fatalf("disabled client re-enabled after migration (enable=%v)", off.Enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
var on model.ClientRecord
|
||||||
|
if err := dst.Where("email = ?", "on@example.com").First(&on).Error; err != nil {
|
||||||
|
t.Fatalf("load enabled client: %v", err)
|
||||||
|
}
|
||||||
|
if !on.Enable {
|
||||||
|
t.Fatalf("enabled client wrongly disabled after migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
var node model.Node
|
||||||
|
if err := dst.Where("name = ?", "n-off").First(&node).Error; err != nil {
|
||||||
|
t.Fatalf("load node: %v", err)
|
||||||
|
}
|
||||||
|
if node.Enable {
|
||||||
|
t.Fatalf("disabled node re-enabled after migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue