156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
package swmigration
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
driver "github.com/go-sql-driver/mysql"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
assets "kra/deploy/migrations"
|
|
)
|
|
|
|
func backupTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
dsn := os.Getenv("SW_TEST_MYSQL_DSN")
|
|
if dsn == "" {
|
|
t.Skip("isolated MySQL required")
|
|
}
|
|
cfg, err := driver.ParseDSN(dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Net != "tcp" || !strings.HasPrefix(cfg.Addr, "127.0.0.1:") {
|
|
t.Fatal("local MySQL required")
|
|
}
|
|
cfg.DBName = ""
|
|
cfg.ParseTime = true
|
|
admin, err := sql.Open("mysql", cfg.FormatDSN())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
name := fmt.Sprintf("sw_test_backup_%d", time.Now().UnixNano())
|
|
if _, err := admin.Exec("CREATE DATABASE `" + name + "` CHARACTER SET utf8mb4"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.DBName = name
|
|
db, err := gorm.Open(mysql.Open(cfg.FormatDSN()), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pool, _ := db.DB()
|
|
pool.SetMaxOpenConns(1)
|
|
t.Cleanup(func() {
|
|
pool.Close()
|
|
_, err := admin.Exec("DROP DATABASE `" + name + "`")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
admin.Close()
|
|
})
|
|
return db
|
|
}
|
|
|
|
func TestMySQLBackupRoundTripPreservesStorageValues(t *testing.T) {
|
|
source, target := backupTestDB(t), backupTestDB(t)
|
|
for _, statement := range []string{
|
|
"SET SESSION time_zone='+08:00'",
|
|
"CREATE TABLE sys_fixture(id BIGINT UNSIGNED PRIMARY KEY,txt LONGTEXT,raw VARBINARY(16),amount DECIMAL(30,6),payload JSON,flags BIT(9),stamp TIMESTAMP(6) NULL,clock DATETIME(6),computed DECIMAL(31,6) GENERATED ALWAYS AS(amount*2) VIRTUAL)",
|
|
"INSERT INTO sys_fixture(id,txt,raw,amount,payload,flags,stamp,clock) VALUES(18446744073709551615,'中文😀 quotes '' slash \\\\ line',X'00FF27225C','123456789012345678901234.123456','{\"id\":\"9007199254740993\"}',b'100000001','2026-02-03 04:05:06.123456','2026-02-03 04:05:06.123456'),(1,'',X'',NULL,NULL,b'0',NULL,NULL)",
|
|
"CREATE TABLE tab_user(id INT PRIMARY KEY)", "INSERT INTO tab_user VALUES(1)",
|
|
"CREATE TABLE tab_promote(id BIGINT PRIMARY KEY)", "INSERT INTO tab_promote VALUES(9007199254740993)",
|
|
} {
|
|
if err := source.Exec(statement).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := target.Exec("CREATE TABLE tab_user(id INT PRIMARY KEY)").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
target.Exec("INSERT INTO tab_user VALUES(77)")
|
|
root := t.TempDir()
|
|
path, digest, err := backupDatabase(context.Background(), source, root, "test-db", "source", nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := verifyBackup(path, digest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := io.ReadAll(reader)
|
|
reader.Close()
|
|
file.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
statements, err := assets.Statements(string(raw))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, statement := range statements {
|
|
if err := target.Exec(statement).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
query := "SELECT CAST(id AS CHAR) AS id,txt,HEX(raw) AS raw,CAST(amount AS CHAR) AS amount,CAST(payload AS CHAR) AS payload,CAST(flags AS UNSIGNED) AS flags,UNIX_TIMESTAMP(stamp) AS stamp,CAST(clock AS CHAR) AS clock,CAST(computed AS CHAR) AS computed FROM sys_fixture ORDER BY id"
|
|
var before, after []map[string]any
|
|
if err := source.Raw(query).Scan(&before).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := target.Raw(query).Scan(&after).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprint(before) != fmt.Sprint(after) {
|
|
t.Fatalf("SQL backup round trip changed storage values: before=%v after=%v", before, after)
|
|
}
|
|
var id uint64
|
|
target.Table("tab_promote").Select("id").Scan(&id)
|
|
if id != 9007199254740993 {
|
|
t.Fatal("large promote ID changed")
|
|
}
|
|
target.Table("tab_user").Select("id").Scan(&id)
|
|
if id != 77 {
|
|
t.Fatal("schema-only raw table was overwritten")
|
|
}
|
|
var timezone string
|
|
source.Raw("SELECT @@SESSION.time_zone").Scan(&timezone)
|
|
if timezone != "+08:00" {
|
|
t.Fatalf("backup leaked connection time zone: %s", timezone)
|
|
}
|
|
appendFile, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appendFile.WriteString("corrupt")
|
|
appendFile.Close()
|
|
if err := verifyBackup(path, digest); err == nil {
|
|
t.Fatal("modified backup checksum accepted")
|
|
}
|
|
cancelled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, _, err := backupDatabase(cancelled, source, root, "test-db", "cancelled", nil, slog.New(slog.NewTextHandler(io.Discard, nil))); err == nil {
|
|
t.Fatal("cancelled backup succeeded")
|
|
}
|
|
partial, _ := filepath.Glob(filepath.Join(root, "*.partial"))
|
|
if len(partial) != 0 {
|
|
t.Fatalf("partial backup left behind: %v", partial)
|
|
}
|
|
}
|